В этой статье мы поговорим о новой концепции в готовящемся к выходу 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
От переводчика
Я так же являюсь и автором оригинальной статьи, так что вопросы можно задавать в комментариях.
Комментарии (42)
Hixon10
10.09.2017 23:10+1Будет интересно, как в конечном итоге 5-й спринг заинтегрируется с 9-й джавой в разрезе java.util.concurrent.Flow. Много статей пивотал пишет про это, но пока не было релизов — не ясно.
alek_sys Автор
11.09.2017 12:42Spring использует Project Reactor для реактивности, а Реактор реализует стандартные API Reactive Streams из Java 9 (именно java.util.concurrent.Flow). Т.е. Java 9 предоставляет API, Реактор реализацию, Спринг использует Реактор и таким образом интегрируется с Java 9.
ExplosiveZ
11.09.2017 00:49-4NodeJS какой-то, не представляю, во что это может превратиться в большом проекте.
Я вот например начал знакомство со спринг и закончил почти сразу же. Какие-то конфигураторы для конфигураторов и ко всему этому фреймворк для запуска. Жуть какая-то, spring превратился в огромный комбайн и переплюнул java ee.
Сейчас например остановился на JSF. Для сайтов уровня hello world — самое то. Что-то крупнее hello world еще не пробовал писать. Может в этом случае я выиграл бы от такого комбайна как spring?AndreyRubankov
11.09.2017 09:38На самом деле, это именно то, что нужно.
Бекенд в чистом видел – это API сервер, который ничего не должен знать про фронтенд. Сейчас все больше и больше мобильных клиентов, которым нужно отдавать только данные. Веб-сайты постепенно отходят на задний план.
Да серверный ренден еще остается, но для этого будут нужны «тяжеловесные» фреймворки. Для остальних же, нужны крайне легковесные, которым хватало бы каких-нибудь дешевых t1.micro.
Я сам не сильно люблю Spring, но Spring WebFlux – это совсем другое дело, если все так, как выглядит, то это замечательный инструмент (пока они не превратят его в Spring WebFlux Boot).alek_sys Автор
11.09.2017 12:35Думаю, не превратят. Он и планируется как легковесная микрофреймворковая штука для желающих. Хотя точно так же он может работать и в рамках обычного Spring приложения, просто
@Controller
может вернуть не ответ, а Flux / Mono в качестве ответа — и все, реактивный контроллер готов. И даже можно объявить бин с типомRouterFunction
и привязать роутер к существующему приложению, а@Controller
аннотации не использовать вообще. Более того, чтобы оценить масштаб — по-умолчанию, Spring WebFlux даже не тащит зависимость на Spring MVC, они прям вообще разные.AndreyRubankov
11.09.2017 12:49Думаю, не превратят.
Это же Spring, не стоит их недооценивать :-)
Кстати, попробовал потыкать webflux из примера на ApacheBenchmark – он вообще не отвечает на запросы. Ну а по консоли видно, что это сейчас на стадии альфы или в лучшем случае беты.IoannGolovko
11.09.2017 14:42Webflux вполне вменяемо функционирует. На запросы отвечает, фильтры работают, конвертеры работают. Можно линк на пример?
AndreyRubankov
11.09.2017 14:53Я брал код из статьи: github.com/alek-sys/spring-functional-microframework
и пробовал выполнить:
ab -k -c 10 -n 1000 «localhost:8080/»
ab -k -c 1 -n 1 «localhost:8080/»
Оба зависают.
А через браузер отлично отдает ответ.alek_sys Автор
11.09.2017 15:46Интересно,
ab
действительно не работает, аwrk
вполне себе.
wrk -t 10 -c 1000 -d 10s http://localhost:8080/
alek_sys Автор
11.09.2017 14:44+1Команда Spring это одна из самых прагматичных и профессиональных, которые я встречал, так что не стоит их недооценивать ;)
Текущий статус Release Candidate, релиз будет до конца этого года.
alek_sys Автор
11.09.2017 12:39+2Собственно, в этом и замысел — если приложение простое и экспортирует только пару эндпоинтов, то можно его написать супер просто, без магии вообще, но оставаться в рамках одного фреймворка.
Комбайн как Spring рулит когда в приложении много разных аспектов, типа интеграции, облачных штук, безопасности. В общем случае (не в крайних) их проще и быстрее сделать на Спринге, чем писать самому. Если приложение просто отдает html / json по данным из базы — его теперь можно сделать очень просто (хотя и раньше было просто, если уж откровенно).
pmcode
11.09.2017 08:27Что-то даже в официальных доках спринга очень мало информации. Где бы как почитать как в этот реактив прикручивается DI и декларативные транзакции?
alek_sys Автор
11.09.2017 12:32Например, здесь https://docs.spring.io/spring/docs/5.0.0.RC3/spring-framework-reference/web.html#web-reactive
Если же нужно просто взять Spring (Boot) как есть с его DI и транзакциями, и прикрутить роутер, все делается просто регистрацией бина в конфигурации:
@Bean public RounterFunction routerFunction() { return route(...) }
pmcode
11.09.2017 13:36Т.е. это старый-добрый спринг, просто они вместо слоя контроллеров реактив прикрутили? Другими словами spring-web-flux — это замена spring-webmvc а все остальное как и было?
alek_sys Автор
11.09.2017 14:50Есть несколько вариантов использования реактивного веба в Spring:
- Без Application context, DI, конфигураций и т.п. — как описано в статье
- Используя
@Controller
аннотации и возвращаяFlux<T>
илиMono<T>
из методов контроллера - Не используя контроллеры, определить бин типа
RouterFunction
с явно заданной таблицей машрутов и хендлерами.
Да, все верно, это старый-добрый Спринг, WebFlux это новая парадигма для веб-разработки, даже не замена MVC, а дополнение. А в качестве приятного бонуса, теперь на Spring можно писать microframework-style приложения a la Sparkframework, Ninja etc. без контекста приложений, DI и автоконфигураций.
i_visseri
11.09.2017 12:29Выглядит пока все очень классно, просто и удобочитаемо. Захотелось попробовать. Автору спасибо за статью)
kanarisru
11.09.2017 12:29-2Раньше порталы писались без всяких фреймворков.
Все говорят о каком то облегчении жизни, типа так быстрее.
Два месяца и 1 человек — готовый портал без фреймворка с нуля.
Неужели сейчас делается еще быстрее?alix_ginger
11.09.2017 13:25+2Может, и не быстрее, но тот самый 1 человек обойдется дешевле за счет более низкого порога вхождения, допустит меньше багов, а через два месяца другой такой же человек сможет быстрее войти в курс дела.
Scf
11.09.2017 16:59Хорошая попытка, Спринг, но нет, это плохая попытка. Получается, для того, чтобы вывести JSON вместо строки, нужно указать content-type:
.contentType(APPLICATION_JSON)
А дальше начинается счастье и проклятие спринга — спринговая магия. Какой сериализатор мы используем? Как его настроить? А если я хочу выдавать не json с content-type application/json? А если я хочу свой сериализатор вкрутить? И вообще зачем веб-серверу зависеть от спринга?
21 век на дворе, сейчас всем нужны:
- http server, где спрингом бы и не пахло
- сериализатор json, где спрингом бы и не пахло
- возможность скомпоновать 1 и 2, вне зависмости от релиализации.
esin
11.09.2017 17:14Думаю, тут магии пока нет. Следующая строка (как я предполагаю) возвращает сериализованный ответ:
.body(fromObject(new Hello("world")))
И там уже (как я очень надеюсь) можно использовать любой сериализатор.
IoannGolovko
11.09.2017 20:06Content-type указывать необязательно, достаточно передать какой-нибудь отличный от базовых типов объект, например ...body(fromObject(myDto)). По факту на выходе автоматом получится json. А вот берет ли на себя webflux корректную расстановку хидеров; это нужно проверить.
Borz
11.09.2017 20:23А если я хочу свой сериализатор вкрутить?
берите и вкручивайте и даже свой ContentType можете в виде строки передать — там никакой магии — сплошная документация
sshikov
11.09.2017 20:38http server, где спрингом бы и не пахло
Вообще говоря, это неоднозначное желание. Т.е. я могу вас понять, и представить такие запросы. Но я также могу представить свои запросы, где скажем сервлеты (ну или в более общем виде обработчики http запросов) это spring бины, и я хочу, чтобы сервер знал про спринговые контексты, и умел подключать оттуда обработчиков. И вообще говоря — не только спринговые.
Иными словами — как вы в общем виде компоновать-то 1 и 2 собираетесь, если у вас нет контейнера, где лежит сериализатор, http сервер, и что-нибудь еще?
AndreyRubankov
11.09.2017 22:33До DI с этим без особых усилий жили. Да, кода было больше, но код был явный.
Что мешает написать:
Или что-то подобное…HttpServer .withCodecRegistry( new CodecRegistry() .bind(APPLICATION_JSON, new JsonCodec( ... )) .bind(APPLICATION_XML, new XmlCodec( ... )) ) .withRouteRegistry( new RouteRegistry() .bind(GET("/"), (r) -> Response.ok()); ) ... .run(8080);
Более того, чтобы файл не был на тысячи строк, можно этот конфиг разбить на несколько: RouteConfig, ErrorHandlerConfig, CodecConfig, etc.
Для проектов, у которых меньше 50-100 эндпоинтов это будет вполне приемлемый подход.
Это не будет слишком избыточным кодом или страшным бойлерплейтом, это будет вполне удобный и красивый конфиг.Scf
11.09.2017 22:49Лямбды и чистые функции позвляют значительно улучшать конфиги. В этом плане инновационные языки — Scala и Javascript. Вкратце, идея в том, что любой сетевой обработчик, фильтр и собственно сервер — это функция
Request -> Response
. Т.е. приложение можно собирать функциональной композицией индивидуальных обработчиков, фильтров и роутеров.sshikov
12.09.2017 19:56Ну да. И что вы хотели этим сказать, что вам DI не нужно?
Scf
12.09.2017 20:17DI нужен, не нужна конфигурация через данные. Конфигурация кодом — лучше.
sshikov
12.09.2017 20:29Конфигурация кодом как правило меняется только при перекомпиляции. Это не плохо и не хорошо — но бывает что нужно и в рантайме. Ну т.е. функциональная композиция — это хорошо, но иногда хочется собрать без компиляции.
alek_sys Автор
12.09.2017 22:24Конфигурация DI кодом уже есть в Spring 5 и называется Programmatic Bean Registration и даже есть удобный Kotlin DSL для него.
sshikov
12.09.2017 19:55Это нормально, но я не могу сказать, что это однозначно лучше. Или хуже. Во-первых, кроме явного кода вы выбрали инициализацию статическую, где все определяется при компиляции. Где-то это хорошо, где-то не факт. Это не лучше и не хуже — это разные потребности.
Скажем прямо — у меня и так не видно, чтобы скажем http server где-то зависел от спринга. От вот этих самых сериализаторов и обработчиков — да, зависит. Они возможно даже инжектятся спрингом — только сервер про спринг (который DI) не знает.
AndreyRubankov
12.09.2017 21:22Во-первых, кроме явного кода вы выбрали инициализацию статическую, где все определяется при компиляции.
Никто не мешает использовать конфиги – поменял конфиг и рестартовал и погнали.
Спринг крутой, он позволяет быстро написать то, что будет работать.
Но вот я, к примеру, всегда хотел выкинуть из спринга все ненужное мне в конкретном проекте. У меня API сервер, который умеет только JSON и ничего больше. Но спринг такой классный, что подсовывает мне дополнительно кучу кода, который нужен для поддержки xml, html, а еще загружает классы для AOP и кучу своих связок для других вещей, которые нафик не нужны. А ведь RAM то ограничен.
Но это уже offtop с моей стороны пошел.sshikov
12.09.2017 21:38Знаете, у меня в прошлом проекте были "аналитики", которые тоже говорили: "А чо, поменял конфиги, рестартовал и погнали". А то что на входе непрерывный поток данных (финансовых), который можно потерять пока рестартуем — это им в голову не приходило. Всякие проекты бывают, попросту говоря.
Что же до "загружает всякие классы для AOP" — ну так вообще говоря, это не является обязательным.
Единственное что меня лично в этом вопросе расстраивает — это тот факт, что не всегда очевидно, зачем конкретно тут вот появился AOP (или что-то еще). Хотя если подумать, обычно ответ недалеко — включил аннотацию @Cacheable для кеширования результатов метода — так оно что, святым духом что-ли будет вызовы метода перехватывать? Нет же, будет либо Proxy, либо AOP, со всеми вытекающими. Ну т.е., нужно иногда понимать, как оно там внутри, что за неонки.
AndreyRubankov
12.09.2017 23:18Знаете, у меня в прошлом проекте были «аналитики», которые тоже говорили: «А чо, поменял конфиги, рестартовал и погнали». А то что на входе непрерывный поток данных (финансовых), который можно потерять пока рестартуем — это им в голову не приходило. Всякие проекты бывают, попросту говоря.
Инструмент выбирается под проект. Я не говорил, что Spring – это абсолютное зло, он хорош для много. Я лишь сказал, что для большинства задач, с которыми я сталкивался в своей карьере он излишне «жирный».
Кстати, а как вы решали такую задачу, когда необходимо поменять конфиг на живом потоке данных?
alek_sys Автор
12.09.2017 22:33Только в битве за RAM не забывайте, что вообще весь Spring сам по себе, занимает не так уж и много памяти — до 32Мб. Есть интересное исследование на эту тему от Дейва Сайера.
pilot911
12.09.2017 22:25У нас в проекте используется Spring для REST API. Интересно мнение профи — будут ли они переписывать REST на Spring WebFlux?
alek_sys Автор
12.09.2017 22:27Если "они" это Spring, а не команда вашего проекта — то нет, не будут. Более того, REST и WebFlux они ортогональны, т.е. REST не надо "переписывать" на WebFlux, REST можно запускать поверх WebFlux, но не обязательно. Все старые парадигмы остаются.
Shpital
12.09.2017 22:25Пока для микросервисов использую Dropwizard и что-то пока не вижу особых стимулов для переезда на модную реактивщину. Кто нибудь сравнивал производительность?
alek_sys Автор
12.09.2017 22:29Реактивные подходы они скорее не для производительности (она теоретически может быть даже хуже для отдельных запросов), а для конкурентности — т.е. обработать больше клиентов одновременно.
stasmarkin
12.09.2017 22:25Я так понимаю, что текущий интерфейс хэндлера не предполагает асинхронных задач. Тогда что неблокирующего в этом вебсервере?
alek_sys Автор
12.09.2017 22:26Предполагает. Это не очень видно, но он возвращает не ответ типа
T
, аMono<T>
илиFlux<T>
— реактивный тип из Project Reactor.
vlanko
Сюда б еще сравнения легковесности с другими.