Я надеюсь, что в этом году у вас будет отличное Java Advent! Сегодня мы рассмотрим обновленную, простую, приятную и прагматичную среду для написания REST-приложений на Java. Это будет так просто, что даже не будет выглядеть как Java.
Мы собираемся заглянуть в веб-фреймворк Spark . Нет, это не связано с Apache Spark. Да, к сожалению, они имеют одинаковое имя.
Я думаю, что лучший способ понять эту среду — это создать простое приложение, поэтому мы создадим простой сервис для выполнения математических операций.
Мы могли бы использовать это так:
Обратите внимание, что служба работает на локальном узле в порту 4567, а запрошенный ресурс — «/ 10 / add / 8».
Настройте проект с помощью Gradle ( что такое Gradle? )
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | apply plugin: "java"apply plugin: "idea"sourceCompatibility = 1.8repositories {    mavenCentral()}dependencies {    compile "com.javaslang:javaslang:2.0.0-RC1"    compile "com.sparkjava:spark-core:2.3"    compile "com.google.guava:guava:19.0-rc2"    compile "org.projectlombok:lombok:1.16.6"    testCompile group: 'junit', name: 'junit', version: '4.+'}task launch(type:JavaExec) {    main = "me.tomassetti.javaadvent.SparkService"    classpath = sourceSets.main.runtimeClasspath} | 
Теперь мы можем запустить:
- , / Gradlew идея для создания проекта IntelliJ IDEA
- , / gradlew test для запуска тестов
- , / gradlew собрать, чтобы построить проект
- , / Gradlew запуска, чтобы начать наш сервис
Отлично. Теперь давайте встретимся с Spark
Как вы думаете, мы можем написать полнофункциональный веб-сервис, который выполняет основные математические операции менее чем в 25 строках Java-кода? Ни за что? Ну, подумай еще раз
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // imports omittedclassCalculator implementsRoute {    privateMap<String, Function2<Long, Long, Long>> functions = ImmutableMap.of(            "add", (a, b) -> a + b,            "mul", (a, b) -> a * b,            "div", (a, b) -> a / b,            "sub", (a, b) -> a - b);    @Override    publicObject handle(Request request, Response response) throwsException {        longleft = Long.parseLong(request.params(":left"));        String operatorName = request.params(":operator");        longright = Long.parseLong(request.params(":right"));        returnfunctions.get(operatorName).apply(left, right);    }}publicclassSparkService {    publicstaticvoidmain(String[] args) {        get("/:left/:operator/:right", newCalculator());    }} | 
В нашем основном методе мы просто говорим, что когда мы получаем запрос, который состоит из трех частей (разделенных косой чертой), мы должны использовать маршрут Калькулятора , который является нашим единственным маршрутом. Маршрут в Spark — это модуль, который принимает запрос, обрабатывает его и выдает ответ.
В нашем калькуляторе происходит волшебство. Это выглядит в запросе для параметров «left», «operatorName» и «right». Слева и справа анализируются как длинные значения, в то время как operatorName используется для поиска операции. Для каждой операции у нас есть функция (Function2 <Long, Long>), которую мы затем применяем к нашим значениям (слева и справа). Круто, а?
Function2 — это интерфейс, созданный в проекте Javaslang .
Теперь вы можете запустить службу ( ./gradlew launch, помните?) И поиграть.
В прошлый раз, когда я проверял Java, он был более многословным, избыточным, медленным … что ж, теперь он лечит
Хорошо, а как насчет тестов?
Так что Java на самом деле может быть довольно лаконичным, и как инженер-программист я отмечаю это в течение минуты или двух, но вскоре после этого я начинаю чувствовать себя неловко… у этого материала нет тестов! Хуже того, это не выглядит проверяемым на всех. Логика в нашем классе калькулятора, но он принимает запрос и выдает ответ. Я не хочу создавать запрос только для того, чтобы проверить, работает ли мой калькулятор по назначению. Давайте немного рефакторинг:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | classTestableCalculator implementsRoute {    privateMap<String, Function2<Long, Long, Long>> functions = ImmutableMap.of(            "add", (a, b) -> a + b,            "mul", (a, b) -> a * b,            "div", (a, b) -> a / b,            "sub", (a, b) -> a - b);    publiclongcalculate(String operatorName, longleft, longright) {        returnfunctions.get(operatorName).apply(left, right);    }    @Override    publicObject handle(Request request, Response response) throwsException {        longleft = Long.parseLong(request.params(":left"));        String operatorName = request.params(":operator");        longright = Long.parseLong(request.params(":right"));        returncalculate(operatorName, left, right);    }} | 
Мы просто отделяем сантехнику (беря значения из запроса) от логики и помещаем ее в свой собственный метод: вычисление . Теперь мы можем проверить расчет.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | publicclassTestableLogicCalculatorTest {    @Test    publicvoidtestLogic() {        assertEquals(10, newTestableCalculator().calculate("add", 3, 7));        assertEquals(-6, newTestableCalculator().calculate("sub", 7, 13));        assertEquals(3, newTestableCalculator().calculate("mul", 3, 1));        assertEquals(0, newTestableCalculator().calculate("div", 0, 7));    }    @Test(expected = ArithmeticException.class)    publicvoidtestInvalidInputs() {        assertEquals(0, newTestableCalculator().calculate("div", 0, 0));    }} | 
Теперь я чувствую себя лучше: наши тесты доказывают, что это работает. Конечно, это приведет к исключению, если мы попытаемся разделить на ноль, но это так.
Что это значит для пользователя, хотя?
Это означает: 500. А что будет, если пользователь попытается использовать операцию, которая не существует?
Что делать, если значения не являются правильными числами?
Ок, это не очень профессионально. Давайте это исправим.
Обработка ошибок, функциональный стиль
Чтобы исправить два случая, нам просто нужно использовать одну особенность Spark: мы можем сопоставить конкретные исключения с конкретными маршрутами. Наши маршруты произведут значимый код состояния HTTP и правильное сообщение.
| 01 02 03 04 05 06 07 08 09 10 | publicclassSparkService {    publicstaticvoidmain(String[] args) {        exception(NumberFormatException.class, (e, req, res) -> res.status(404));        exception(ArithmeticException.class, (e, req, res) -> {            res.status(400);            res.body("This does not seem like a good idea");        });        get("/:left/:operator/:right", newReallyTestableCalculator());    }} | 
Нам еще предстоит обработать случай несуществующей операции, и это то, что мы собираемся сделать в ReallyTestableCalculator .
Для этого мы будем использовать типичный шаблон функции: мы вернем Either . Either — это коллекция, которая может иметь левое или правое значение. Слева обычно представляет некоторую информацию об ошибке, такую как код ошибки или сообщение об ошибке. Если ничего не пойдет не так, то Either будет содержать правильное значение, которое может быть чем угодно. В нашем случае мы вернем Error (класс, который мы определили), если операция не может быть выполнена, в противном случае мы вернем результат операции в Long. Поэтому мы вернем Either <Error, Long>.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | packageme.tomassetti.javaadvent.calculators;importjavaslang.Function2;importjavaslang.Tuple2;importjavaslang.collection.Map;importjavaslang.collection.HashMap;importjavaslang.control.Either;importspark.Request;importspark.Response;importspark.Route;publicclassReallyTestableCalculator implementsRoute {        privatestaticfinalintNOT_FOUND = 404;    privateMap<String, Function2<Long, Long, Long>> functions = HashMap.ofAll(            newTuple2<>("add", (a, b) -> a + b),            newTuple2<>("mul", (a, b) -> a * b),            newTuple2<>("div", (a, b) -> a / b),            newTuple2<>("sub", (a, b) -> a - b));    publicEither<Error, Long> calculate(String operatorName, longleft, longright) {        Either<Error, Long> unknownOp = Either.<Error, Long>left(newError(NOT_FOUND, "Unknown math operation"));        returnfunctions.get(operatorName).map(f -> Either.<Error, Long>right(f.apply(left, right)))                .orElse(unknownOp);    }    @Override    publicObject handle(Request request, Response response) throwsException {        longleft = Long.parseLong(request.params(":left"));        String operatorName = request.params(":operator");        longright = Long.parseLong(request.params(":right"));        Either<Error, Long> res =  calculate(operatorName, left, right);        if(res.isRight()) {            returnres.get();        } else{            response.status(res.left().get().getHttpCode());            returnnull;        }    }} | 
Давайте проверим это:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | packageme.tomassetti.javaadvent;importjavaslang.control.Either;importme.tomassetti.javaadvent.calculators.ReallyTestableCalculator;importorg.junit.Test;importstaticorg.junit.Assert.assertEquals;publicclassReallyTestableLogicCalculatorTest {    @Test    publicvoidtestLogic() {        assertEquals(Either.right(10L), newReallyTestableCalculator().calculate("add", 3, 7));        assertEquals(Either.right(-6L), newReallyTestableCalculator().calculate("sub", 7, 13));        assertEquals(Either.right(3L), newReallyTestableCalculator().calculate("mul", 3, 1));        assertEquals(Either.right(0L), newReallyTestableCalculator().calculate("div", 0, 7));    }    @Test(expected = ArithmeticException.class)    publicvoidtestInvalidOperation() {        Either<me.tomassetti.javaadvent.calculators.Error, Long> res = newReallyTestableCalculator().calculate("div", 0, 0);        assertEquals(true, res.isLeft());        assertEquals(400, res.left().get().getHttpCode());    }    @Test    publicvoidtestUnknownOperation() {        Either<me.tomassetti.javaadvent.calculators.Error, Long> res = newReallyTestableCalculator().calculate("foo", 0, 0);        assertEquals(true, res.isLeft());        assertEquals(404, res.left().get().getHttpCode());    }} | 
Результат
Мы получили услугу, которую легко проверить. Выполняет математические операции. Он поддерживает четыре основные операции, но его можно легко расширить, чтобы поддерживать больше. Ошибки обрабатываются и используются соответствующие коды HTTP: 400 для неверных входных данных и 404 для неизвестных операций или значений.
Выводы
Когда я впервые увидел Java 8, я был рад новым функциям, но не очень взволнован. Тем не менее, через несколько месяцев я вижу, как появляются новые фреймворки, основанные на этих новых функциях и способные реально изменить способ программирования на Java. Такие вещи, как Spark и Javaslang, делают разницу. Я думаю, что теперь Java может оставаться простой и надежной, в то же время становясь более гибкой и продуктивной.
- Вы можете найти много других учебных пособий на веб-сайте учебных пособий Spark или в моем блоге tomassetti.me .
| Ссылка: | Введение в Spark, вашу следующую REST Framework для Java, от нашего партнера по JCG Федерико Томассетти в блоге Java Advent Calendar . | 



