В этой статье мы поговорим о новой концепции в готовящемся к выходу 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
Ссылки
- New in Spring 5: Functional Web Framework
- Notes on Reactive Programming Part II: Writing Some Code
- Very nice intro into Spring Functional Web from Baeldung — Introduction to the Functional Web Framework in Spring 5
- Reactive Programming with Spring 5.0 M1
- Обсуждение оригинальной статьи на Reddit
От переводчика
Я так же являюсь и автором оригинальной статьи, так что вопросы можно задавать в комментариях.
Автор: alek_sys