Spring Web MVC містить WebMvc.fn, легку функціональну модель програмування, в якій функції використовуються для
маршрутизації та обробки запитів, а контракти розроблені для забезпечення незмінності. Вона є альтернативою моделі
програмування на основі анотацій, але в іншому працює на тому ж DispatcherServlet. code>: функція, яка приймає
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 -> {
// Save the sseBuilder object somewhere...
}));
}
// У якомусь іншому потоці, що відправляє рядок
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, як буде описано пізніше. Зверни увагу, що така логіка роботи
відрізняється від моделі програмування на основі анотацій, де "найконкретніший" метод контролера вибирається
автоматично, яка повертається з 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
@EnableMvcclass 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
.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ