Spring: ваш следующий Java микрофреймворк

в 18:06, , рубрики: java, spring, spring 5

В этой статье мы поговорим о новой концепции в готовящемся к выходу Spring Framework 5 которая называется функциональный веб-фреймворк и посмотрим, как она может помочь при разработке легковесных приложений и микросервисов.

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

  • Лаконичный — минимум бойлерплейта, минимум настройки
  • Простой — без магии
  • Простой в деплойменте — один артефакт для деплоймента
  • Простой в запуске — без дополнительных зависимостей
  • Легковесный — минимальное использование памяти / CPU
  • Неблокирующий — для написания конкуррентных неблокирующих приложений

Несмотря на то, что некоторые из этих пунктов актуальны при использовании Spring Boot, он сам по себе добавляет дополнительную магию поверх самого Spring Framework. Даже такие базовые аннотации, как @Controller не совсем прямолинейны, что уж говорить про авто-конфигурации и сканирование компонентов. В общем-то, для крупномасштабных приложений, просто незаменимо то, что Spring берет на себя заботу о DI, роутинге, конфигурации и т.п. Однако, в мире микросервисов, где приложения это просто шестеренки в одной больной машине, вся мощь Spring Boot может быть немного лишней.

Для решения этой проблемы, команда Spring представила новую фичу, которая называется функциональный веб-фреймворк — и именно о ней мы и будем говорить. В целом, это часть большего под-проекта Spring WebFlux, который раньше назывался Spring Reactive Web.

Для начала, давайте вернемся к основам и посмотрим, что такое веб-приложение и какие компоненты мы ожидаем иметь в нем. Несомненно, есть базовая вещь — веб-сервер. Чтобы избежать ручной обработки запросов и вызова методов приложения, нам пригодится роутер. И, наконец, нам нужен обработчик — кусок кода, который принимает запрос и отдает ответ. По сути, это все, что нужно! И именно эти компоненты предоставляет функциональный веб-фреймворк Spring, убирая всю магию и фокусируясь на фундаментальном минимуме. Отмечу, что это вовсе не значит, что Spring резко меняет направление и уходит от Spring MVC, функциональный веб просто дает еще одну возможность создавать приложения на Spring.

Обработчик

Давайте рассмотрим пример. Для начала, пойдем на Spring Initializr и создадим новый проект используя Spring Boot 2.0 и Reactive Web как единственную зависимость. Теперь мы можем написать наш первый обработчик — функцию которая принимает запрос и отдает ответ.

HandlerFunction hello = new HandlerFunction() {
    @Override
    public Mono handle(ServerRequest request) {
        return ServerResponse.ok().body(fromObject("Hello"));
    }
};

Итак, наш обработчик это просто реализация интерфейса HandlerFunction который принимает параметр request (типа ServerRequest) и возвращает объект типа ServerResponse с текстом "Hello". Spring так же предоставляет удобные билдеры чтобы создать ответ от сервера. В нашем случае, мы используем ok() которые автоматически возвращают HTTP код ответа 200. Чтобы вернуть ответ, нам потребуется еще один хелпер — fromObject, чтобы сформировать ответ из предоставленного объекта.

Мы так же можем сделать код немного более лаконичным и использовать лямбды из Java 8 и т.к. HandlerFunction это интерфейс одного метода (single abstract method interface, SAM), мы можем записать нашу функцию как:

HandlerFunction hello = request -> ServerResponse.ok().body(fromObject("Hello"));

Роутер

Теперь, когда у нас есть хендлер, пора определить роутер. Например, мы хотим вызвать наш обработчик когда URL "/" был вызван с помощью HTTP метода GET. Чтобы этого добиться, определим объект типа RouterFunction который мапит функцию-обработчик, на маршрут:

RouterFunction router = route(GET("/"), hello);

route и GET это статические методы из классов RequestPredicates и RouterFunctions, они позволяют создать так называемую RouterFunction. Такая функция принимает запрос, проверяет, соответствует ли он все предикатам (URL, метод, content type, etc) и вызывает нужную функцию-обработчик. В данном случае, предикат это http метод GET и URL '/', а функция обработчик это hello, которая определена выше.

Веб-сервер

А сейчас пришло время собрать все вместе в единое приложение. Мы используем легковесный и простой сервер Reactive Netty. Чтобы интегрировать наш роутер с веб-сервером, необходимо превратить его в HttpHandler. После этого можно запустить сервер:

HttpServer
    .create("localhost", 8080)
    .newHandler(new ReactorHttpHandlerAdapter(httpHandler))
    .block();

ReactorHttpHandlerAdapter это класс предоставленный Netty, который принимает HttpHandler, остальной код, думаю, не требует пояснений. Мы создаем новые веб-сервер привязанный к хосту localhost и на порту 8080 и предоставляем httpHandler созданный из нашего роутера.

И это все, приложение готово! И его полный код:

public static void main(String[] args)
          throws IOException, LifecycleException, InterruptedException {

    HandlerFunction hello = request -> ServerResponse.ok().body(fromObject("Hello"));

    RouterFunction router = route(GET("/"), hello);

    HttpHandler httpHandler = RouterFunctions.toHttpHandler(router);

    HttpServer
            .create("localhost", 8080)
            .newHandler(new ReactorHttpHandlerAdapter(httpHandler))
            .block();

    Thread.currentThread().join();
}

Последняя строчка нужна только чтобы держать JVM процесс живым, т.к. сам HttpServer его не блокирует. Вы возможно сразу обратите внимание, что приложение стартует мгновенно — там нет ни сканирования компонентов, ни авто-конфигурации.

Мы так же может запустить это приложение как обычное Java приложение, не требуется никаких контейнеров приложений и прочего.

Чтобы запаковать приложение для деплоймента, мы можем воспользоваться преимуществами Maven плагина Spring и просто вызвать

./mvnw package

Эта команда создаст так называемый fat JAR со всеми зависимостями, включенными в JAR. Это файл может быть задеплоен и запущен не имея ничего, кроме установленной JRE

java -jar target/functional-web-0.0.1-SNAPSHOT.jar

Так же, если мы проверим использование памяти приложением, то увидим, что оно держится примерно в районе 32 Мб — 22 Мб использовано на metaspace (классы) и около 10 Мб занято непосредственно в куче. Разумеется, наше приложение ничего и не делает — но тем не менее, это просто показатель, что фреймворк и рантайм сами по себе требуют минимум системных ресурсов.

Поддержка JSON

В нашем примере, мы возвращали строку, но вернуть JSON ответ так же просто. Давайте расширим наше приложение новым endpoint-ом, который вернет JSON. Наша модель будет очень простой — всего одно строковое поле под названием name. Чтобы избежать ненужного boilerplate кода, мы воспользуемся фичей из проекта Lombok, аннотацией @Data. Наличие этой аннотации автоматически создаст геттеры, сеттеры, методы equals и hashCode, так что нам не придется релизовывать их вручную.

@Data
class Hello {
    private final String name;
}

Теперь, нам нужно расширить наш роутер чтобы вернуть JSON ответ при обращении к URL /json. Это можно сделать вызвав andRoute(...) метод на существующем роуте. Также, давайте вынесем код роутер в отдельную функцию, чтобы отделить его от кода приложения и позволить использовать позже в тестах.

static RouterFunction getRouter() {
    HandlerFunction hello = request -> ok().body(fromObject("Hello"));

    return
        route(
            GET("/"), hello)
        .andRoute(
            GET("/json"), req ->
                ok()
                .contentType(APPLICATION_JSON)
                .body(fromObject(new Hello("world")));
}

После перезапуска, приложение вернет { "name": "world" } при обращении к URL /json при запросе контента с типом application/json.

Контекст приложения

Вы, возможно, заметили, что мы не определили контекст приложения — он нам просто не нужен! Несмотря на то, что мы можем объявить RouterFunction как бин (bean) в контексте Spring WebFlux приложения, и он точно так же будет обрабатывать запросы на определенные URL, роутер можно запустить просто поверх Netty Server чтобы создавать простые и легковесные JSON сервисы.

Тестирование

Для тестирования реактивных приложений, Spring предоставляет новый клиент под названием WebTestClient (подобно MockMvc). Его можно создать для существующего контекста приложения, но так же можно определить его и для RouterFunction.

public class FunctionalWebApplicationTests {

    private final WebTestClient webTestClient =
            WebTestClient
                .bindToRouterFunction(
                    FunctionalWebApplication.getRouter())
                .build();

    @Test
    public void indexPage_WhenRequested_SaysHello() {
        webTestClient.get().uri("/").exchange()
                .expectStatus().is2xxSuccessful()
                .expectBody(String.class)
                .isEqualTo("Hello");
    }

    @Test
    public void jsonPage_WhenRequested_SaysHello() {
        webTestClient.get().uri("/json").exchange()
                .expectStatus().is2xxSuccessful()
                .expectHeader().contentType(APPLICATION_JSON)
                .expectBody(Hello.class)
                .isEqualTo(new Hello("world"));
    }
}

WebTestClient включает ряд assert-ов, которые можно применить к полученному ответу, чтобы провалидировать HTTP код, содержимое ответа, тип ответа и т.п.

В заключение

Spring 5 представляет новую парадигму для разработки маленьких и легковесных microservice-style веб-приложений. Такие приложения могут работать без контекста приложений, автоконфигурации и в целом использовать подход микрофреймворков, когда роутер и функции-обработчики и веб-сервер опеределены явно в теле приложения.

Код

Доступен на GitHub

Ссылки

От переводчика

Я так же являюсь и автором оригинальной статьи, так что вопросы можно задавать в комментариях.

Автор: alek_sys

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js