Spring WebFlux містить WebFlux.fn, полегшену модель функціонального програмування, в якій функції використовуються для маршрутизації та обробки запитів, а контракти розроблені таким чином, щоб забезпечувати незмінність. Вона є альтернативою моделі програмування на основі анотацій, але в іншому працює на тій же основі з Reactive Core. HandlerFunction: функції, яка приймає ServerRequest і повертає відкладений ServerResponse (тобто Mono<ServerResponse>). І об'єкт-запит, і об'єкт-відповідь мають незмінні контракти, які забезпечують доступ до запиту та відповіді HTTP, сумісний з JDK 8. HandlerFunction – це еквівалент тіла методу з анотацією @RequestMapping у моделі програмування на основі анотацій.

Вхідні запити надсилаються у функцію-обробник за допомогою RouterFunction: функції, яка приймає ServerRequest і повертає відкладену HandlerFunction (тобто Mono<HandlerFunction>). Якщо функція маршрутизатора збігається, функція-обробник повертається; інакше повертається порожня функція Mono. RouterFunction – це еквівалент анотації @RequestMapping, але з тією істотною відмінністю, що функції маршрутизатора надають не лише дані, а й логіку роботи.

RouterFunctions.route() надає засіб складання маршрутизаторів, який полегшує створення маршрутизаторів, як показано в наступному прикладі:

Java
import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.web.reactive.function.server.RequestPredicates.*; import static org.springframework.web.reactive.function.server.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  Mono<ServerResponse> listPeople(ServerRequest request) { // ... } public Mono<ServerResponse> createPerson(ServerRequest request) { // ... } public Mono<ServerResponse> getPerson(ServerRequest request) { // ... } }
Kotlin
 val repository: PersonRepository = ... val handler = PersonHandler(repository) val route = coRouter {  accept(APPLICATION_JSON).nest { GET("/person/{id}" , handler::getPerson) GET("/person", handler::listPeople) } POST(" /person", handler::createPerson) } class PersonHandler (private val repository: PersonRepository) { // ... suspend fun listPeople(request: ServerRequest): ServerResponse { // ... } suspend fun createPerson(request: ServerRequest): ServerResponse { // ... } suspend fun getPerson(request: ServerRequest): ServerResponse { // ... } }
  1. Create router using Coroutines router DSL, а Reactive alternative is also available via router { }.

Один із способів запустити RouterFunction – перетворити її на HttpHandler і встановити через один із вбудованих серверних адаптерів:

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

Більшість програм можна запускати через Java-конфігурацію WebFlux.

HandlerFunction

ServerRequest та ServerResponse - це незмінні інтерфейси, які забезпечують доступ до HTTP-запиту та відповіді, сумісний з JDK 8. І запит, і відповідь забезпечують зворотну реакцію Reactive Streams для потоків тіла. Тіло запиту представлене за допомогою Flux або Mono з Reactor. Тіло відповіді представляється за допомогою будь-якого Publisher з Reactive Streams, включаючи Flux та Mono.

ServerRequest

ServerRequest надає доступ до HTTP-методу, URI-ідентифікатора, заголовків і параметрів запиту, а доступ до тіла надається через методи body.

в наступному прикладі тіло запиту витягується в Mono<String>:

Java
Mono<String> string = request.bodyToMono(String.class);
Kotlin
val string = request.awaitBody<String>()

У наступному прикладі тіло витягується в Flux<Person> (або Flow<Person> у Kotlin), де об'єкти Person декодуються з будь-якої серіалізованої форми, наприклад JSON або XML:

Java
Flux<Person> people = request.bodyToFlux(Person.class);
Kotlin
val people = request.bodyToFlow<Person>()

Попередні приклади є скороченнями, що використовують більш загальний ServerRequest.body (BodyExtractor), який приймає інтерфейс функціональної стратегії BodyExtractor. Допоміжний клас BodyExtractors надає доступ до кількох екземплярів. Наприклад, попередні приклади можна записати так:

Java
Mono<String> string = request.body(BodyExtractors.toMono(String.class)); Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
Kotlin
 val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle() val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()

У наступному прикладі показано, як отримати доступ до даних форми:

Java
Mono< ;MultiValueMap<String, String>> map = request.formData();
Kotlin
val  map = request.awaitFormData()

У наступному прикладі показано, як отримати доступ до багатокомпонентних даних у вигляді Map:

Java
Mono<MultiValueMap<String, Part>> map = request.multipartData();
Kotlin
val  map = request.awaitMultipartData()

У наступному прикладі показано, як отримати доступ до кількох компонентів, по одному за раз, у потоковому режимі :

Java
Flux<Part> parts = request.body(BodyExtractors.toParts());
Kotlin
val parts = request.body(BodyExtractors.toParts()).asFlow()

ServerResponse

ServerResponse забезпечує доступ до HTTP-відповіді, і, оскільки він незмінний, то можна використовувати метод build для його створення. Ви можете використовувати засіб збирання для встановлення статусу відповіді, додавання заголовків відповіді або надання тіла відповіді. У наступному прикладі створюється відповідь 200 (OK) із вмістом у форматі JSON:

Java
Mono<Person> person = ... ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
Kotlin
val людина: Person = ... ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)

У наступному прикладі показано, як створити відповідь 201 (CREATED) із заголовком Location і без тіла:

Java
URI location = ... ServerResponse.created(location).build();
Kotlin
val location: URI = ... ServerResponse.created(location).build()

Залежно від кодека, що використовується, можна передати параметри підказки, щоб кастомно налаштувати спосіб серіалізації або десеріалізації тіла. Наприклад, щоб задати подання на основі Jackson JSON:

Java
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
Kotlin
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java) .body(...)

Класи обробників

Можна написати функцію-обробник у вигляді лямбда-виразу, як показано в наступному приклад:

Java
HandlerFunction<ServerResponse> helloWorld = request -> ServerResponse.ok().bodyValue("Hello World");
Kotlin
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }

Це зручно , але в додатку нам потрібна наявність кількох функцій, а кілька вбудованих лямбда-виразів можуть призвести до безладдя. Тому корисно згрупувати пов'язані функції-обробники в клас обробника, який відіграє таку ж роль, як інструкція @Controller у додатку, заснованому на інструкціях. Наприклад, наступний клас представляє реактивне сховище Person:

Java
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 Mono<ServerResponse> listPeople(ServerRequest request) {  Flux<Person> people = repository.allPeople(); return ok().contentType(APPLICATION_JSON).body(people, Person.class); } public Mono<ServerResponse> createPerson(ServerRequest request) {  Mono<Person> person = request.bodyToMono(Person.class); return ok().build(repository.savePerson(person)); } public Mono<ServerResponse> getPerson(ServerRequest request) {  int personId = Integer.valueOf(request.pathVariable("id")); return repository.getPerson(personId) .flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person)) .switchIfEmpty(ServerResponse.) .build()); } }
  1. listPeople – це функція-обробник, яка повертає всі об'єкти Person, знайдені у сховищі, у форматі JSON.
  2. createPerson – це функція-обробник, яка зберігає новий об'єкт Person, який міститься в тілі запиту. Зверніть увагу, що PersonRepository.savePerson(Person) повертає Mono<Void>: порожній Mono, який генерує сигнал завершення, якщо Person був прочитаний з запиту та збережений. Тому метод build(Publisher<Void>) використовується для надсилання відповіді, коли буде отримано сигнал завершення (тобто коли Person буде збережено).
  3. getPerson – це функція-обробник, яка повертає один об'єкт Person, ідентифікований змінною дорогою id. Ми виймаємо цей об'єкт Person зі сховища та створюємо відповідь у форматі JSON, якщо його буде знайдено. Якщо його не буде знайдено, використовується switchIfEmpty(Mono<T>), щоб повернути відповідь 404 Not Found.
Kotlin
class PersonHandler(private val repository: PersonRepository) { suspend fun listPeople(request: ServerRequest): ServerResponse {  val людей: Flow<Person> = repository.allPeople() return ok().contentType(APPLICATION_JSON).bodyAndAwait(people); } suspend fun createPerson(request: ServerRequest): ServerResponse {  val person = request.awaitBody<Person>() repository.savePerson(person) return ok().buildAndAwait() } suspend fun getPerson(request: ServerRequest ): ServerResponse {  val personId = request.pathVariable("id").toInt() return repository.getPerson(personId )?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) } ?: ServerResponse.notFound().buildAndAwait() } }
  1. listPeople - це функція-обробник, яка повертає всі об'єкти Person, знайдені у сховищі, у форматі JSON.
  2. createPerson – це функція-обробник, яка зберігає новий об'єкт Person, який міститься в тілі запиту. Зверніть увагу, що PersonRepository.savePerson(Person) повертає Mono<Void>: порожній Mono, який генерує сигнал завершення, якщо Person був прочитаний з запиту та збережений. Тому метод build(Publisher<Void>) використовується для надсилання відповіді, коли буде отримано сигнал завершення (тобто коли Person буде збережено).
  3. getPerson – це функція-обробник, яка повертає один об'єкт Person, ідентифікований змінною дорогою id. Ми виймаємо цей об'єкт Person зі сховища та створюємо відповідь у форматі JSON, якщо його буде знайдено. Якщо його не буде знайдено, використовується switchIfEmpty(Mono<T>), щоб повернути відповідь 404 Not Found.

Валідація

Функціональна кінцева точка може використовувати засоби перевірки Spring для застосування валідації до тіла запиту. Наприклад, дана кастомна реалізація Validator з Spring для об'єкта Person:

Java
public class PersonHandler {private final Validator validator = new PersonValidator();  // ...  public Mono<ServerResponse> createPerson(ServerRequest request) { Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate);  return ok().build(repository.savePerson(person)); } private void validate(Person person) { Errors errors = new BeanPropertyBindingResult(person , "person"); validator.validate(person, errors); if (errors.hasErrors()) { throw new ServerWebInputException(errors.toString());  } } }
  1. Створюємо екземпляр Validator.
  2. Застосовуємо валідацію.
  3. Генеруємо виняток при відповіді 400.
Kotlin
class PersonHandler(private val repository: PersonRepository) { private val validator = PersonValidator()  // ... suspend fun createPerson(request: ServerRequest ): ServerResponse { val person = request.awaitBody<Person>() validate(person)  repository.savePerson(person) return ok().buildAndAwait() } private fun validate(person: Person) { val errors : Errors = BeanPropertyBindingResult(person, "person"); validator.validate(person, errors); if (errors.hasErrors()) { throw ServerWebInputException(errors.toString())  } } }
  1. Створюємо екземпляр Validator.
  2. Застосовуємо валідацію.
  3. Генеруємо виняток при відповіді 400.

Обробники також можуть використовувати стандартний API валідації бінів (JSR-303), створюючи та впроваджуючи глобальний екземпляр Validator на основі 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:

Java
RouterFunction<ServerResponse> route = RouterFunctions.route() .GET("/hello-world", accept(MediaType.TEXT_PLAIN), request -> ServerResponse.ok().bodyValue( "Hello World")).build();
Kotlin
val route = coRouter { GET("/hello-world", accept( TEXT_PLAIN)) { ServerResponse.ok().bodyValueAndAwait("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) on the RouterFunctions.route() builder

  • RouterFunction.and(RouterFunction)

  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) - скорочення для RouterFunction.and() з вкладеними RouterFunctions.route().

У цьому прикладі показано компонування з чотирьох маршрутів:

Java
>
import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.web.reactive.function.server.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();
  1. GET /person/{id} with an Accept header that matches JSON is routed to PersonHandler.getPerson
  2. GET /person із заголовком Accept, який відповідає формату JSON, направляється до PersonHandler.listPeople
  3. POST /person без додаткових предикатів відображається на PersonHandler.createPerson, та
  4. otherRoute - це функція-маршрутизатор, яка створюється в іншому місці і додається до побудованого маршруту.
Kotlin
 import org.springframework.http.MediaType.APPLICATION_JSON val repository: PersonRepository = ... val handler = PersonHandler(repository); val otherRoute: RouterFunction<ServerResponse> = coRouter { } val route = coRouter { GET("/person/{id}", accept( APPLICATION_JSON), handler::getPerson)  GET("/person", accept(APPLICATION_JSON), handler::listPeople)  POST("/person", handler::createPerson)  }.and(otherRoute) 
  1. GET /person/{id} with an Accept header that matches JSON is routed to PersonHandler.getPerson
  2. GET /person із заголовком Accept, який відповідає формату JSON, надсилається в PersonHandler.listPeople
  3. POST /person без додаткових предикатів відображається на PersonHandler.createPerson,
  4. та
  5. otherRoute – це функція-маршрутизатор, яка створюється в іншому місці та додається до побудованого маршруту.

Вкладені маршрути

Зазвичай група функцій-маршрутизаторів має загальний предикат, наприклад, загальний шлях. У наведеному вище прикладі загальним предикатом буде предикат шляху, який відповідає /person, який використовується трьома маршрутами. При використанні анотацій можна усунути це дублювання, використовуючи анотацію @RequestMapping на рівні типу, що відображається на /person. У WebFlux.fn предикати шляху можуть спільно використовуватися за допомогою методу path у конструкторі функцій-маршрутизаторів. Наприклад, останні кілька рядків наведеного вище прикладу можна доповнити таким чином, використовуючи вкладені маршрути:

Java
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();
  1. Зверніть увагу, що другий параметр path – це одержувач, який приймає засіб складання маршрутизатора.
="spring-block-title">Kotlin
val route = coRouter { "/person".nest { GET("/{id}" , accept(APPLICATION_JSON), handler::getPerson) GET(accept(APPLICATION_JSON), handler::listPeople) POST(handler::createPerson) } }