Статьи

Введение в Spark, ваш следующий REST Framework для Java

Я надеюсь, что в этом году у вас будет отличное Java Advent! Сегодня мы рассмотрим обновленную, простую, приятную и прагматичную среду для написания REST-приложений на Java. Это будет так просто, что даже не будет выглядеть как Java.

Мы собираемся заглянуть в веб-фреймворк Spark . Нет, это не связано с Apache Spark. Да, к сожалению, они имеют одинаковое имя.

Я думаю, что лучший способ понять эту среду — это создать простое приложение, поэтому мы создадим простой сервис для выполнения математических операций.

Мы могли бы использовать это так:

spark1

Обратите внимание, что служба работает на локальном узле в порту 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.8
 
repositories {
    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 omitted
 
class Calculator implements Route {
 
    private Map<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
    public Object handle(Request request, Response response) throws Exception {
        long left = Long.parseLong(request.params(":left"));
        String operatorName = request.params(":operator");
        long right = Long.parseLong(request.params(":right"));
        return functions.get(operatorName).apply(left, right);
    }
}
 
public class SparkService {
    public static void main(String[] args) {
        get("/:left/:operator/:right", new Calculator());
    }
}

В нашем основном методе мы просто говорим, что когда мы получаем запрос, который состоит из трех частей (разделенных косой чертой), мы должны использовать маршрут Калькулятора , который является нашим единственным маршрутом. Маршрут в 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
class TestableCalculator implements Route {
 
    private Map<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);
 
    public long calculate(String operatorName, long left, long right) {
        return functions.get(operatorName).apply(left, right);
    }
 
    @Override
    public Object handle(Request request, Response response) throws Exception {
        long left = Long.parseLong(request.params(":left"));
        String operatorName = request.params(":operator");
        long right = Long.parseLong(request.params(":right"));
        return calculate(operatorName, left, right);
    }
}

Мы просто отделяем сантехнику (беря значения из запроса) от логики и помещаем ее в свой собственный метод: вычисление . Теперь мы можем проверить расчет.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class TestableLogicCalculatorTest {
 
    @Test
    public void testLogic() {
        assertEquals(10, new TestableCalculator().calculate("add", 3, 7));
        assertEquals(-6, new TestableCalculator().calculate("sub", 7, 13));
        assertEquals(3, new TestableCalculator().calculate("mul", 3, 1));
        assertEquals(0, new TestableCalculator().calculate("div", 0, 7));
    }
 
    @Test(expected = ArithmeticException.class)
    public void testInvalidInputs() {
        assertEquals(0, new TestableCalculator().calculate("div", 0, 0));
    }
 
}

Теперь я чувствую себя лучше: наши тесты доказывают, что это работает. Конечно, это приведет к исключению, если мы попытаемся разделить на ноль, но это так.

Что это значит для пользователя, хотя?

spark2

Это означает: 500. А что будет, если пользователь попытается использовать операцию, которая не существует?

spark3

Что делать, если значения не являются правильными числами?

spark4

Ок, это не очень профессионально. Давайте это исправим.

Обработка ошибок, функциональный стиль

Чтобы исправить два случая, нам просто нужно использовать одну особенность Spark: мы можем сопоставить конкретные исключения с конкретными маршрутами. Наши маршруты произведут значимый код состояния HTTP и правильное сообщение.

01
02
03
04
05
06
07
08
09
10
public class SparkService {
    public static void main(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", new ReallyTestableCalculator());
    }
}

Нам еще предстоит обработать случай несуществующей операции, и это то, что мы собираемся сделать в 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
package me.tomassetti.javaadvent.calculators;
 
import javaslang.Function2;
import javaslang.Tuple2;
import javaslang.collection.Map;
import javaslang.collection.HashMap;
import javaslang.control.Either;
import spark.Request;
import spark.Response;
import spark.Route;
 
public class ReallyTestableCalculator implements Route {
     
    private static final int NOT_FOUND = 404;
 
    private Map<String, Function2<Long, Long, Long>> functions = HashMap.ofAll(
            new Tuple2<>("add", (a, b) -> a + b),
            new Tuple2<>("mul", (a, b) -> a * b),
            new Tuple2<>("div", (a, b) -> a / b),
            new Tuple2<>("sub", (a, b) -> a - b));
 
    public Either<Error, Long> calculate(String operatorName, long left, long right) {
        Either<Error, Long> unknownOp = Either.<Error, Long>left(new Error(NOT_FOUND, "Unknown math operation"));
        return functions.get(operatorName).map(f -> Either.<Error, Long>right(f.apply(left, right)))
                .orElse(unknownOp);
    }
 
    @Override
    public Object handle(Request request, Response response) throws Exception {
        long left = Long.parseLong(request.params(":left"));
        String operatorName = request.params(":operator");
        long right = Long.parseLong(request.params(":right"));
        Either<Error, Long> res =  calculate(operatorName, left, right);
        if (res.isRight()) {
            return res.get();
        } else {
            response.status(res.left().get().getHttpCode());
            return null;
        }
    }
}

Давайте проверим это:

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
package me.tomassetti.javaadvent;
 
import javaslang.control.Either;
import me.tomassetti.javaadvent.calculators.ReallyTestableCalculator;
import org.junit.Test;
 
import static org.junit.Assert.assertEquals;
 
public class ReallyTestableLogicCalculatorTest {
 
    @Test
    public void testLogic() {
        assertEquals(Either.right(10L), new ReallyTestableCalculator().calculate("add", 3, 7));
        assertEquals(Either.right(-6L), new ReallyTestableCalculator().calculate("sub", 7, 13));
        assertEquals(Either.right(3L), new ReallyTestableCalculator().calculate("mul", 3, 1));
        assertEquals(Either.right(0L), new ReallyTestableCalculator().calculate("div", 0, 7));
    }
 
    @Test(expected = ArithmeticException.class)
    public void testInvalidOperation() {
        Either<me.tomassetti.javaadvent.calculators.Error, Long> res = new ReallyTestableCalculator().calculate("div", 0, 0);
        assertEquals(true, res.isLeft());
        assertEquals(400, res.left().get().getHttpCode());
    }
 
    @Test
    public void testUnknownOperation() {
        Either<me.tomassetti.javaadvent.calculators.Error, Long> res = new ReallyTestableCalculator().calculate("foo", 0, 0);
        assertEquals(true, res.isLeft());
        assertEquals(404, res.left().get().getHttpCode());
    }
 
}

Результат

Мы получили услугу, которую легко проверить. Выполняет математические операции. Он поддерживает четыре основные операции, но его можно легко расширить, чтобы поддерживать больше. Ошибки обрабатываются и используются соответствующие коды HTTP: 400 для неверных входных данных и 404 для неизвестных операций или значений.

Выводы

Когда я впервые увидел Java 8, я был рад новым функциям, но не очень взволнован. Тем не менее, через несколько месяцев я вижу, как появляются новые фреймворки, основанные на этих новых функциях и способные реально изменить способ программирования на Java. Такие вещи, как Spark и Javaslang, делают разницу. Я думаю, что теперь Java может оставаться простой и надежной, в то же время становясь более гибкой и продуктивной.

Ссылка: Введение в Spark, вашу следующую REST Framework для Java, от нашего партнера по JCG Федерико Томассетти в блоге Java Advent Calendar .