- PVSM.RU - https://www.pvsm.ru -
В этой статье мы поговорим о новой концепции в готовящемся к выходу Spring Framework 5 которая называется функциональный веб-фреймворк и посмотрим, как она может помочь при разработке легковесных приложений и микросервисов.
Вы, возможно, удивлены видеть Spring и микрофреймворк в одном предложении. Но все верно, Spring вполне может стать вашим следующим Java микрофреймворком. Чтобы избежать недоразумений, давайте определим, что им имеем в виду под микро:
Несмотря на то, что некоторые из этих пунктов актуальны при использовании Spring Boot, он сам по себе добавляет дополнительную магию поверх самого Spring Framework. Даже такие базовые аннотации, как @Controller
не совсем прямолинейны, что уж говорить про авто-конфигурации и сканирование компонентов. В общем-то, для крупномасштабных приложений, просто незаменимо то, что Spring берет на себя заботу о DI, роутинге, конфигурации и т.п. Однако, в мире микросервисов, где приложения это просто шестеренки в одной больной машине, вся мощь Spring Boot может быть немного лишней.
Для решения этой проблемы, команда Spring представила новую фичу, которая называется функциональный веб-фреймворк — и именно о ней мы и будем говорить. В целом, это часть большего под-проекта Spring WebFlux, который раньше назывался Spring Reactive Web.
Для начала, давайте вернемся к основам и посмотрим, что такое веб-приложение и какие компоненты мы ожидаем иметь в нем. Несомненно, есть базовая вещь — веб-сервер. Чтобы избежать ручной обработки запросов и вызова методов приложения, нам пригодится роутер. И, наконец, нам нужен обработчик — кусок кода, который принимает запрос и отдает ответ. По сути, это все, что нужно! И именно эти компоненты предоставляет функциональный веб-фреймворк Spring, убирая всю магию и фокусируясь на фундаментальном минимуме. Отмечу, что это вовсе не значит, что Spring резко меняет направление и уходит от Spring MVC, функциональный веб просто дает еще одну возможность создавать приложения на Spring.
Давайте рассмотрим пример. Для начала, пойдем на Spring Initializr [1] и создадим новый проект используя 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 ответ так же просто. Давайте расширим наше приложение новым endpoint-ом, который вернет JSON. Наша модель будет очень простой — всего одно строковое поле под названием name
. Чтобы избежать ненужного boilerplate кода, мы воспользуемся фичей из проекта Lombok [2], аннотацией @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 [3]
Я так же являюсь и автором оригинальной статьи, так что вопросы можно задавать в комментариях.
Автор: alek_sys
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/263589
Ссылки в тексте:
[1] Spring Initializr: https://start.spring.io
[2] Lombok: https://projectlombok.org/
[3] GitHub: https://github.com/alek-sys/spring-functional-microframework
[4] New in Spring 5: Functional Web Framework: https://spring.io/blog/2016/09/22/new-in-spring-5-functional-web-framework
[5] Notes on Reactive Programming Part II: Writing Some Code: https://spring.io/blog/2016/06/13/notes-on-reactive-programming-part-ii-writing-some-code
[6] Introduction to the Functional Web Framework in Spring 5: http://www.baeldung.com/spring-5-functional-web
[7] Reactive Programming with Spring 5.0 M1: https://spring.io/blog/2016/07/28/reactive-programming-with-spring-5-0-m1
[8] Reddit: https://www.reddit.com/r/programming/comments/65rdta/spring_your_next_java_microframework/
[9] Источник: https://habrahabr.ru/post/337604/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.