Spring Web MVC содержит WebMvc.fn, легкую функциональную модель программирования, в которой функции используются для маршрутизации и обработки запросов, а контракты разработаны для обеспечения неизменяемости. Она является альтернативой модели программирования на основе аннотаций, но в остальном работает на том же DispatcherServlet.
Краткое описание
В WebMvc.fn HTTP-запрос обрабатывается с помощью HandlerFunction
: функции, которая принимает ServerRequest
и возвращает ServerResponse
. И запрос, и объект-ответ имеют неизменяемые контракты, которые обеспечивают доступ к HTTP-запросу и ответу, удобный для JDK 8. HandlerFunction
– это эквивалент тела метода, помеченного аннотацией @RequestMapping
, в модели программирования на основе аннотаций.
Входящие запросы направляются в функцию-обработчик с помощью RouterFunction
: функции, которая принимает ServerRequest
и возвращает необязательную HandlerFunction
(т.е. Optional<HandlerFunction>
). Если функция-маршрутизатор совпадает, возвращается функция-обработчик, в противном случае – пустой Optional. RouterFunction
– это эквивалент аннотации @RequestMapping
, но с тем существенным отличием, что функции-маршрутизаторы передают не только данные, но и логику работы.
RouterFunctions.route()
предоставляет средство сборки маршрутизаторов, которое облегчает создание маршрутизаторов, как показано в следующем примере:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public ServerResponse listPeople(ServerRequest request) {
// ...
}
public ServerResponse createPerson(ServerRequest request) {
// ...
}
public ServerResponse getPerson(ServerRequest request) {
// ...
}
}
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = router {
accept(APPLICATION_JSON).nest {
GET("/person/{id}", handler::getPerson)
GET("/person", handler::listPeople)
}
POST("/person", handler::createPerson)
}
class PersonHandler(private val repository: PersonRepository) {
// ...
fun listPeople(request: ServerRequest): ServerResponse {
// ...
}
fun createPerson(request: ServerRequest): ServerResponse {
// ...
}
fun getPerson(request: ServerRequest): ServerResponse {
// ...
}
}
- Create router using the router DSL.
Если зарегистрировать RouterFunction
как бин, например, открыв его в классе с аннотацией @Configuration
, он будет автоматически обнаружен сервлетом.
HandlerFunction
ServerRequest
и ServerResponse
– это неизменяемые интерфейсы, которые предоставляют доступ к HTTP-запросу и ответу, включая заголовки, тело, метод и код состояния, удобоваримый для JDK 8.
ServerRequest
ServerRequest
предоставляет доступ к HTTP-методу, URI-идентификатору, заголовкам и параметрам запроса, а доступ к телу предоставляется через методы body
.
В следующем примере тело запроса извлекается в String
:
String string = request.body(String.class);
val string = request.body<String>()
В следующем примере тело извлекается в List<Person>
, где объекты Person
декодируются из сериализованной формы, такой как формат JSON или XML:
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
val people = request.body<Person>()
В следующем примере показано, как получить доступ к параметрам:
MultiValueMap<String, String> params = request.params();
val map = request.params()
ServerResponse
ServerResponse
предоставляет доступ к HTTP-ответу, и, поскольку он неизменяем, можно использовать метод build для его создания. Можно использовать средство сборки для настройки статуса ответа, добавления заголовков ответа или передачи тела ответа. В следующем примере создается ответ 200 (OK) с содержимым формата JSON:
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
В следующем примере показано, как создать ответ 201 (CREATED) с заголовком Location
и без тела:
URI location = ...
ServerResponse.created(location).build();
val location: URI = ...
ServerResponse.created(location).build()
Также можно использовать в качестве тела асинхронный результат в виде CompletableFuture
, Publisher
или любого другого типа, поддерживаемого ReactiveAdapterRegistry
. Например:
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
val person = webClient.get().retrieve().awaitBody<Person>()
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
Если не только тело, но также статус или заголовки основаны на асинхронном типе, можно использовать статический метод async
для ServerResponse
, который принимает CompletableFuture<ServerResponse>
, Publisher<ServerResponse>
, или любой другой асинхронный тип, поддерживаемый ReactiveAdapterRegistry
. Например:
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
.map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);
События, посылаемые сервером, могут быть переданы с помощью статического метода sse
для ServerResponse
. Средство сборки, предоставляемое этим методом, позволяет отправлять строки или другие объекты в формате JSON. Например:
public RouterFunction<ServerResponse> sse() {
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
// Сохраняем объект sseBuilder где-нибудь...
}));
}
// В каком-то другом потоке, отправляющем строку
sseBuilder.send("Hello world");
// Или объекте, который будет преобразован в JSON
Person person = ...
sseBuilder.send(person);
// Настраиваем событие с помощью других методов
sseBuilder.id("42")
.event("sse event")
.data(person);
// и в какой-то момент будет готово
sseBuilder.complete();
fun sse(): RouterFunction<ServerResponse> = router {
GET("/sse") { request -> ServerResponse.sse { sseBuilder ->
// Save the sseBuilder object somewhere..
}
}
// В каком-то другом потоке, отправляющем строку
sseBuilder.send("Hello world")
// Или объекте, который будет преобразован в JSON
val person = ...
sseBuilder.send(person)
// Настраиваем событие с помощью других методов
sseBuilder.id("42")
.event("sse event")
.data(person)
// и в какой-то момент будет готово
sseBuilder.complete()
Классы обработчиков
Мы можем написать функцию-обработчик в виде лямбда-выражения, как показано в следующем примере:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body("Hello World");
val helloWorld: (ServerRequest) -> ServerResponse =
{ ServerResponse.ok().body("Hello World") }
Это удобно, но в приложении нам требуются несколько функций, а несколько встроенных лямбда-выражений могут все запутать. Поэтому полезно будет сгруппировать связанные функции-обработчики в класс обработчика, который играет такую же роль, как и аннотация @Controller
в приложении, основанном на аннотациях. Например, следующий класс представляет реактивное хранилище Person
:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public ServerResponse listPeople(ServerRequest request) {
List<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people);
}
public ServerResponse createPerson(ServerRequest request) throws Exception {
Person person = request.body(Person.class);
repository.savePerson(person);
return ok().build();
}
public ServerResponse getPerson(ServerRequest request) {
int personId = Integer.parseInt(request.pathVariable("id"));
Person person = repository.getPerson(personId);
if (person != null) {
return ok().contentType(APPLICATION_JSON).body(person);
}
else {
return ServerResponse.notFound().build();
}
}
}
listPeople
– это функция-обработчик, которая возвращает все объектыPerson
, найденные в репозитории, в формате JSON.createPerson
– это функция-обработчик, которая сохраняет новый объектPerson
, содержащийся в теле запроса.getPerson
– это функция-обработчик, которая возвращает одного человека, идентифицированного переменной путиid
. Мы извлекаем этот объектPerson
из хранилища и создаем ответ в формате JSON, если он будет найден. Если он не будет найден, мы возвращаем ответ 404 Not Found.
class PersonHandler(private val repository: PersonRepository) {
fun listPeople(request: ServerRequest): ServerResponse {
val people: List<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).body(people);
}
fun createPerson(request: ServerRequest): ServerResponse {
val person = request.body<Person>()
repository.savePerson(person)
return ok().build()
}
fun getPerson(request: ServerRequest): ServerResponse {
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) }
?: ServerResponse.notFound().build()
}
}
listPeople
– это функция-обработчик, которая возвращает все объектыPerson
, найденные в репозитории, в формате JSON.createPerson
– это функция-обработчик, которая сохраняет новый объектPerson
, содержащийся в теле запроса.getPerson
– это функция-обработчик, которая возвращает одного человека, идентифицированного переменной путиid
. Мы извлекаем этот объектPerson
из хранилища и создаем ответ в формате JSON, если он будет найден. Если он не будет найден, мы возвращаем ответ 404 Not Found.
Валидация
Функциональная конечная точка может использовать средства валидации Spring для применения валидации к телу запроса. Например, дана пользовательская реализация Validator из Spring для Person
:
public class PersonHandler {
private final Validator validator = new PersonValidator();
// ...
public ServerResponse createPerson(ServerRequest request) {
Person person = request.body(Person.class);
validate(person);
repository.savePerson(person);
return ok().build();
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString());
}
}
}
- Создаем экземпляр
Validator
. - Применяем валидацию.
- Генерируется исключение для ответа 400.
class PersonHandler(private val repository: PersonRepository) {
private val validator = PersonValidator()
// ...
fun createPerson(request: ServerRequest): ServerResponse {
val person = request.body<Person>()
validate(person)
repository.savePerson(person)
return ok().build()
}
private fun validate(person: Person) {
val errors: Errors = BeanPropertyBindingResult(person, "person")
validator.validate(person, errors)
if (errors.hasErrors()) {
throw ServerWebInputException(errors.toString())
}
}
}
- Создаем экземпляр
Validator
. - Применяем валидацию.
- Генерируется исключение для ответа 400.
Обработчики также могут использовать стандартный API валидации бинов (JSR-303), создавая и внедряя глобальный экземпляр валидатора
на основе LocalValidatorFactoryBean
.
RouterFunction
Функции-маршрутизаторы используются для маршрутизации запросов к соответствующим HandlerFunction
. Как правило, функции-маршрутизаторы пишутся не самостоятельно, а для их создания используется метод для вспомогательного класса RouterFunctions
. RouterFunctions.route()
(без параметров) предоставляет удобное средство сборки для создания функции-маршрутизатора, в то время как RouterFunctions.route(RequestPredicate, HandlerFunction)
предлагает прямой способ создания маршрутизатора.
Как правильно, рекомендуется использовать средство сборки route()
, поскольку оно обеспечивает удобные сокращения для типичных сценариев отображения, не требуя импортирования статических элементов, которые трудно обнаружить. Например, средство сборки функций-маршрутизаторов предлагает метод GET(String, HandlerFunction)
для создания Map для GET-запросов; и POST(String, HandlerFunction)
– для POST-запросов.
Помимо отображения на основе HTTP-методов, средство сборки маршрутов предлагает способ введения дополнительных предикатов при отображении на запросы. Для каждого HTTP-метода существует перегруженный вариант, который принимает RequestPredicate
в качестве параметра, через который могут быть выражены дополнительные ограничения.
Предикаты
Вы можете написать свой собственный RequestPredicate
, но вспомогательный класс RequestPredicates
предусматривает часто используемые реализации, основанные на пути запроса, HTTP-методе, типе содержимого и так далее. В следующем примере используется предикат запроса для создания ограничения на основе заголовка Accept
:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().body("Hello World")).build();
import org.springframework.web.servlet.function.router
val route = router {
GET("/hello-world", accept(TEXT_PLAIN)) {
ServerResponse.ok().body("Hello World")
}
}
Можно скомпоновать несколько предикатов запроса вместе при помощи:
-
RequestPredicate.and(RequestPredicate)
– оба должны совпадать. -
RequestPredicate.or(RequestPredicate)
– любой из них может совпадать.
Многие предикаты из RequestPredicates
являются составными. Например, RequestPredicates.GET(String)
состоит из RequestPredicates.method(HttpMethod)
и RequestPredicates.path(String)
. В приведенном примере также используются два предиката запроса, так как средство сборки использует RequestPredicates.GET
на внутреннем уровне и комбинирует его с предикатом accept
.
Маршруты
Функции-маршрутизаторы вычисляются упорядоченно: если первый маршрут не совпадает, то вычисляется второй, и так далее. Поэтому имеет смысл объявлять более конкретные маршруты перед обобщенными. Это также важно при регистрации функций-маршрутизаторов в качестве бинов Spring, как будет описано позже. Обратите внимание, что такая логика работы отличается от модели программирования на основе аннотаций, где "наиболее конкретный" метод контроллера выбирается автоматически.
При использовании средства сборки функций-маршрутизаторов все определенные маршруты компонуются в одну RouterFunction
, которая возвращается из build()
. Существуют и другие способы компоновки нескольких функций маршрутизатора в одну:
-
add(RouterFunction)
к средству сборкиRouterFunctions.route()
-
RouterFunction.and(RouterFunction)
-
RouterFunction.andRoute(RequestPredicate, HandlerFunction)
- сокращение дляRouterFunction.and()
с вложеннымиRouterFunctions.route()
.
В следующем примере показана компоновка из четырех маршрутов:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.add(otherRoute)
.build();
GET /person/{id}
с заголовкомAccept
, который соответствует формату JSON, маршрутизируется вPersonHandler.getPerson
GET /person
с заголовкомAccept
, который соответствует формату JSON, маршрутизируется вPersonHandler.listPeople
POST /person
без дополнительных предикатов отображается наPersonHandler.createPerson
, иotherRoute
– это функция-маршрутизатор, которая создается в другом месте и добавляется к построенному маршруту.
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute = router { }
val route = router {
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET("/person", accept(APPLICATION_JSON), handler::listPeople)
POST("/person", handler::createPerson)
}.and(otherRoute)
GET /person/{id}
с заголовкомAccept
, который соответствует формату JSON, маршрутизируется вPersonHandler.getPerson
GET /person
с заголовкомAccept
, который соответствует формату JSON, маршрутизируется вPersonHandler.listPeople
POST /person
без дополнительных предикатов отображается наPersonHandler.createPerson
, иotherRoute
– это функция-маршрутизатор, которая создается в другом месте и добавляется к построенному маршруту.
Вложенные маршруты
Обычно группа функций-маршрутизаторов имеет общий предикат, например, общий путь. В приведенном выше примере общим предикатом будет предикат пути, который соответствует /person
, используемый тремя маршрутами. При использовании аннотаций можно устранить это дублирование, используя аннотацию @RequestMapping
на уровне типа, которая отображается на /person
. В WebMvc.fn предикаты пути могут совместно использоваться с помощью метода path
в средстве сборки функций-маршрутизаторов. Например, последние несколько строк приведенного выше примера можно дополнить следующим образом, используя вложенные маршруты:
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST(handler::createPerson))
.build();
- Обратите внимание, что второй параметр
path
– это получатель, который принимает средство сборки маршрутизатора.
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET(accept(APPLICATION_JSON), handler::listPeople)
POST(handler::createPerson)
}
}
Хотя вложение на основе пути является наиболее распространенным, можно вложить предикат любого типа, используя метод nest
в средстве сборки. Вышеприведенный вариант все еще содержит некоторое дублирование в виде общего предиката Accept-header
. Мы продолжить дополнять код, используя метод nest
вместе с accept
:
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.build();
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
accept(APPLICATION_JSON).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST(handler::createPerson)
}
}
}
Запуск сервера
Обычно функции маршрутизатора в конфигурации на основе DispatcherHandler
запускаются через конфигурацию MVC, которая задействует конфигурацию Spring для объявления компонентов, необходимых для обработки запросов. Конфигурация MVC на Java объявляет следующие компоненты инфраструктуры для поддержки функциональных конечных точек:
-
RouterFunctionMapping
: Обнаруживает один или несколько биновRouterFunction<?>
в конфигурации Spring, упорядочивает их, объединяет с помощьюRouterFunction.andOther
и маршрутизирует запросы к результирующей составной функцииRouterFunction
. -
HandlerFunctionAdapter
: Простой адаптер, позволяющийDispatcherHandler
вызыватьHandlerFunction,
которая была отображена на запрос.
Предыдущие компоненты позволяют функциональным конечным точкам "вписаться" в жизненный цикл обработки запросов DispatcherServlet
, а также (потенциально) работать бок о бок с аннотированными контроллерами, если таковые объявлены. Это также способ активации функциональных конечных точек в пусковой системе Spring Boot Web.
В следующем примере показана конфигурация WebFlux Java:
@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// конфигурируем преобразование сообщений...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// конфигурируем CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// конфигурируем разрешение представления для HTML-визуализации...
}
}
@Configuration
@EnableMvc
class WebConfig : WebMvcConfigurer {
@Bean
fun routerFunctionA(): RouterFunction<*> {
// ...
}
@Bean
fun routerFunctionB(): RouterFunction<*> {
// ...
}
// ...
override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
// конфигурируем преобразование сообщений...
}
override fun addCorsMappings(registry: CorsRegistry) {
// конфигурируем CORS...
}
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// конфигурируем разрешение представления для HTML-визуализации...
}
}
Функции обработчика фильтрации
Можно фильтровать функции-обработчики с помощью методов before
, after
или filter
в средстве сборки функций маршрутизации. С помощью аннотаций можно добиться аналогичной функциональности, используя @ControllerAdvice
, ServletFilter
или и то, и другое. Фильтр будет применен ко всем маршрутам, построенным средством сборки. Это означает, что фильтры, определенные во вложенных маршрутах, не применяются к маршрутам "верхнего уровня". Например, рассмотрим следующий пример:
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople)
.before(request -> ServerRequest.from(request)
.header("X-RequestHeader", "Value")
.build()))
.POST(handler::createPerson))
.after((request, response) -> logResponse(response))
.build();
- Фильтр
before
, добавляющий кастомный заголовок запроса, применяется только к двум маршрутам GET. - Фильтр
after
, регистрирующий ответ, применяется ко всем маршрутам, включая вложенные.
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
GET("/{id}", handler::getPerson)
GET(handler::listPeople)
before {
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
}
}
POST(handler::createPerson)
after { _, response ->
logResponse(response)
}
}
- Фильтр
before
, добавляющий кастомный заголовок запроса, применяется только к двум маршрутам GET. - Фильтр
after
, регистрирующий ответ, применяется ко всем маршрутам, включая вложенные.
Метод filter
в средстве сборки маршрутизатора принимает HandlerFilterFunction
: функцию, которая принимает ServerRequest
и HandlerFunction
и возвращает ServerResponse
. Параметр функции-обработчика представляет собой следующий элемент в цепочке. Обычно это обработчик, к которому маршрутизируется запрос, но это может быть и другой фильтр, если применяется несколько.
Теперь можно добавить простой фильтр безопасности к нашему маршруту, предположив, что у нас есть SecurityManager
, который может определить, допустим ли определенный путь. В следующем примере показано, как это сделать:
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
import org.springframework.web.servlet.function.router
val securityManager: SecurityManager = ...
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST(handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
status(UNAUTHORIZED).build();
}
}
}
}
В предыдущем примере продемонстрировано, что вызов next.handle(ServerRequest)
является необязательным. Мы позволяем выполняться функции-обработчику только тогда, когда доступ разрешен.
Помимо использования метода filter
в средстве сборки функций-маршрутизаторов, можно применить фильтр к существующей функции-маршрутизатору через RouterFunction.filter(HandlerFilterFunction)
.
CorsFilter
.