Spring WebFlux передбачає модель програмування на основі анотацій, де компоненти @Controller та @RestController використовують анотації для вираження відображень запитів, введення запитів, обробки винятків та багато чого іншого. Анотовані контролери мають гнучкі сигнатури методів і не обов'язково повинні розширювати базові класи або реалізовувати певні інтерфейси. У наступному лістингу показаний базовий приклад:

Java
@RestController
public classHelloController {
@GetMapping("/hello") 
public String handle (){
return "Hello WebFlux";
}
}
Kotlin
@RestController
class HelloController {
@GetMapping("/hello")
fun handle() = "Hello WebFlux"
}

У попередньому прикладі метод повертає String для запису у відповідь.

@Controller

Ти можеш визначати біни контролера, використовуючи стандартне визначення бінів Spring. Стереотип @Controller дозволяє робити автоматичне виявлення та узгоджується із загальними засобами підтримки Spring для виявлення класів з анотацією @Component у classpath та автоматичної реєстрації визначень бінів для них. Він також працює як стереотип для анотованого класу, вказуючи на його роль як вебкомпонента.

Щоб активувати автоматичне виявлення таких бінів з анотацією @Controller, можна додати сканування компонентів до конфігурації Java, як показано в наступному прикладі:

Java
@Configuration
@ComponentScan("org.example.web") 
public class WebConfig {
// ...
}
  1. Скануємо пакет org.example .web.
Kotlin
@Configuration
 @ComponentScan("org.example.web") 
class WebConfig {
// ...
}
  1. Скануємо пакет org.example.web.

@RestController — це складена анотація, яка сама мета-анотується анотаціями @Controller та @ResponseBody, вказуючи на контролер, кожен метод якого успадковує анотацію @ResponseBody на рівні типів і, отже, здійснює запис безпосередньо в тіло відповіді замість розпізнавання подання та візуалізації за допомогою HTML-шаблону.

Відображення запитів

Анотація @RequestMapping використовується для відображення запитів на методи контролерів. Вона має різні атрибути для зіставлення за URL-адресою, HTTP-методом, параметрами запиту, заголовками та типами середовища передачі даних. Ти можеш використовувати її на рівні класу для вираження загальних відображень або на рівні методу для звуження до конкретного відображення кінцевої точки. Існують також специфічні для HTTP-методу варіанти скорочення анотації @RequestMapping

  • @GetMapping

  • @PostMapping

  • @PutMapping

  • @DeleteMapping

  • @PatchMapping

Попередні анотації є кастомними анотаціями, які вказуються тому, що, ймовірно, більшість методів контролера потрібно буде відобразити на визначений HTTP-метод, а не використовувати анотацію @RequestMapping, яка за замовчуванням зіставляється з усіма HTTP-методами. У той же час, анотація @RequestMapping, як і раніше, необхідна на рівні класу для вираження загальних відображень.

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

Java
@RestController
@RequestMapping("/persons") 
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id)  {
// ...
}
@PostMapping
@ResponseStatus( HttpStatus.CREATED) 
public void add(@RequestBody Person person) {
// ...
}
}
Kotlin
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}") 
fun getPerson(@PathVariable id: Long): Person {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun add(@RequestBody person: Person) {
 // ...
}
}

URI шаблони

Ти можеш відобразити запити, використовуючи стандартні маски (вони ж — шаблон пошуку (glob patterns)) і знаки підстановки:

Шаблон Опис Приклад

?

Виконує зіставлення з одним символом

"/pages/t?st.html" відповідає "/pages/test.html" та "/pages/t3st.html"

*

Виконує зіставлення з нулем або більше символами в сегменті шляху

"/resources/*.png" відповідає "/resources/file.png"

"/projects/*/versions" відповідає "/projects/spring/versions" але не відповідає "/projects/spring/boot /versions"

**

Виконує зіставлення з нулем або більше сегментами шляху до кінця шляху

"/resources/**" відповідає "/resources/file. png" та "/resources/images/file.png"

"/resources/**/file.png" недійсний, оскільки ** допускається тільки в кінці шляху.

{name}

Виконує зіставлення з сегментом шляху і записує його до змінної з ім'ям "name".

"/projects/{project}/versions" відповідає "/projects/spring/versions" і захоплює project=spring

{name:[a-z]+}

Виконує зіставлення з виразом "[a-z]+ " як змінного шляху з ім'ям "name".

"/projects/{project:[a-z]+}/versions" відповідає "/projects/spring/versions" але не "/projects/spring1/versions"

{*path}

Виконує зіставлення з нулем або більше сегментами шляху до кінця шляху і записує його до змінної з ім'ям "path".

"/resources/{*file}" відповідає "/resources/images/file.png" та захоплює file=/images/file.png

До захоплених URI-змінних можна отримати доступ за допомогою анотації @PathVariable, як це показано в наступному прикладі:

Java
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
Kotlin
@GetMapping("/owners/{ownerId}/pets/{petId}") 
fun findPet(@PathVariable ownerId: Long , @PathVariable petId: Long): Pet {
// ...
}

Можна оголошувати URI-змінні на рівні класів і методів, як показано в наступному прикладі:

Java
@Controller
@RequestMapping("/owners/{ownerId}") 
public class OwnerController {
@GetMapping("/pets/{petId}")  
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
 // ...
}
}
  1. Відображення URI-ідентифікатора на рівні класів.
  2. Відображення URI-ідентифікатор на рівні методу.
Kotlin
@Controller 
@RequestMapping("/owners/{ownerId}") 
class OwnerController {
@GetMapping("/pets/{petId}")   
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
// ...
}
}
  1. Відображення URI на рівні класів.
  2. Відображення URI на рівні методу.

Змінні URI-ідентифікатори перетворюються на відповідний тип, інакше виникає помилка TypeMismatchException. Прості типи (int, long, Date тощо) підтримуються за замовчуванням, але ти можеш зареєструвати підтримку будь-якого іншого типу даних.

Можна явно присвоїти імена URI-змінним (наприклад, @PathVariable("customId")), але також можна і не вказувати ці відомості, якщо імена однакові, а твій код компілюється з використанням налагоджувальної інформації або з прапором компілятора -parameters на Java 8.

Синтаксис {*varName} оголошує URI-змінну, яка відповідає нулю або більшій кількості сегментів, що залишилися в шляху. Наприклад, /resources/{*path} відповідає всім файлам у каталозі /resources/, а змінна "path" відображає повний шлях у каталозі /resources.

Синтаксис {varName:regex} повідомляє URI-змінну з регулярним виразом, який має синтаксис: {varName:regex}. Наприклад, при URL-адресі /spring-web-3.0.5.jar наступний метод отримує ім'я, версію та розширення файлу:

Java
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\ \.[a-z]+}") 
public void handle(@PathVariable String version, @PathVariable String ext) {
// ...
} 
Kotlin
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") 
fun handle(@PathVariable version: String, @PathVariable ext: String) {
// ... }

Шаблони шляхів URI-ідентифікатора також можуть містити вбудовані плейсхолдери ${…}, які розпізнаються під час запуску за допомогою PropertySourcesPlaceholderConfigurer у локальних, системних джерелах властивостей, джерелах властивостей оточення тощо. Можна використовувати це, наприклад, для параметризації базової URL-адреси на основі певної зовнішньої конфігурації.

Spring WebFlux використовує PathPattern та PathPatternParser для підтримки зіставлення шляхів URI-ідентифікаторів. Обидва класи знаходяться у spring-web і спеціально розроблені для використання з HTTP-шляхами URL-адрес у вебдодатках, де велика кількість шаблонів URI-шляхів зіставляється під час виконання.

Spring WebFlux не підтримує суфіксальне зіставлення з чином — на відміну від Spring MVC, де таке відображення як /person також відповідає /person.*. Для узгодження вмісту на основі URL-адреси, якщо це необхідно, рекомендуємо використовувати параметр запиту, який є більш простим, явним і менш уразливим для експлойтів на основі шляху URL-адреси.

Порівняння шаблонів

Якщо кілька зразків збігаються з URL-адресою, їх необхідно порівняти, щоб знайти найкращий збіг. Це робиться за допомогою PathPattern.SPECIFICITY_COMPARATOR, який шукає зразки, що є більш конкретними.

Для кожного зразка обчислюється оцінка, заснована на кількості змінних URI-ідентифікатора та підстановочних знаків, де URI-змінна оцінюється нижче, ніж підстановочний знак. Перемагає зразок із меншим загальним рахунком. Якщо два зразки мають однакові показники, вибирається довший.

Зразки catch-all (наприклад, **, {*varName}) виключаються з підрахунку і завжди сортуються останніми. Якщо два зразки є одночасно catch-all, вибирається довший.

Типи споживаних середовищ передачі даних

Ти можеш звузити відображення запитів на основі Content-Type запиту, як показано в наступному прикладі:

Java
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") 
fun handle(@PathVariable version: String, @PathVariable ext: String) {
// ... }
Kotlin
 @PostMapping("/pets", consumes = ["application/json"])
fun addPet(@RequestBody pet: Pet) {
// ...
}

Атрибут "consumes" також підтримує вирази заперечення: наприклад, !text/plain означає будь-який тип вмісту, відмінний від text/plain.

Ти можеш оголосити загальний атрибут consumes на рівні класу. Однак, на відміну від більшості інших атрибутів відображення запитів, при використанні на рівні класу, атрибут consumes на рівні методу швидше перевизначає, а не розширює оголошення на рівні класу.

MediaType надає константи для часто використовуваних типів середовищ передачі даних, таких як APPLICATION_JSON_VALUE та APPLICATION_XML_VALUE.

Вироблені типи середовищ передачі даних

Можна звузити діапазон відображення запитів на основі заголовка запиту Accept та списку типів вмісту, які виробляє метод контролера, як показано в наступному прикладі:

Java
@GetMapping(path = "/pets/{petId}", produces = "application/json" )
@ResponseBody
public Pet getPet(@PathVariable String petId)  {
// ...
}
Kotlin
@GetMapping("/pets/{petId}", produces = ["application/json"])
@ResponseBody
fun getPet(@PathVariable String petId): Pet {
// ...
}

Тип середовища передачі даних може визначати набір символів. Підтримуються заперечені вирази: наприклад, !text/plain означає будь-який тип вмісту, відмінний від text/plain.

Можна оголошувати загальний атрибут produces на рівні класу. Однак, на відміну від більшості інших атрибутів відображення запитів, при використанні на рівні класу атрибут produces на рівні методу швидше перевизначає, а не розширює оголошення на рівні класу.

MediaType надає константи для часто використовуваних типів середовищ передачі даних, таких як, наприклад, APPLICATION_JSON_VALUE та APPLICATION_XML_VALUE.

Параметри та заголовки

Можна звузити діапазон відображення запитів на основі умов параметрів запиту. Можна перевірити наявність параметра запиту myParam), його відсутність (!myParam) або наявність певного значення (myParam=myValue). У наступних прикладах перевіряється наявність параметра зі значенням:

Java
@GetMapping(path = "/pets/{petId} ", params = "myParam=myValue") 
public void findPet (@PathVariable String petId) {
// ...
}
  1. Переконайся, що myParam дорівнює myValue.
Kotlin
@GetMapping("/pets/{petId}", params = ["myParam=myValue"])  
fun findPet(@PathVariable petId: String) {
// ...
}
  1. Переконайся, що myParam дорівнює myValue.

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

Java
@GetMapping(path = "/pets", headers = "myHeader=myValue")  
public  void findPet(@PathVariable String petId) {
// ...
}
  1. Переконайся, що myHeader дорівнює myValue.
Kotlin
@GetMapping("/pets", headers = ["myHeader=myValue"]) 
fun findPet(@PathVariable petId: String) {
// ... }
  1. Переконайся, що myHeader дорівнює myValue.

HTTP-методи HEAD, OPTIONS

@GetMapping@RequestMapping(method=HttpMethod.GET)) прозоро підтримують HTTP-метод HEAD для відображення запитів. Методи контролера не потрібно змінювати. Функція-обгортка відповіді, що використовується в HttpHandler, забезпечує встановлення заголовка Content-Length на кількість записаних байт (без фактичного запису у відповідь).

За замовчуванням HTTP-метод OPTIONS обробляється шляхом встановлення заголовка відповіді Allow до списку HTTP-методів, перерахованих у всіх методах, позначених анотацією @RequestMapping.

При використанні анотації @RequestMapping без оголошення HTTP-методів, заголовок Allow встановлюється в GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS. Методи контролера завжди повинні оголошувати підтримувані HTTP-методи (наприклад, за допомогою варіантів, специфічних для HTTP-методів: @GetMapping, @PostMapping та інші).

Ти можеш явно відобразити метод, анотований @RequestMapping, на HTTP-метод HEAD та HTTP-метод OPTIONS, але в рядовому випадку це робити не обов'язково.

Кастомні анотації

Spring WebFlux підтримує використання складових анотацій для відображення запитів. Це анотації, які є мета-анотаціями @RequestMapping і складені для повторного оголошення підмножини (або всіх) атрибутів, позначених анотацією @RequestMapping, з більш вузькою, більш конкретною метою.

@GetMapping, @PostMapping, @PutMapping, @DeleteMapping та @PatchMapping є прикладами складових анотацій. Вони передбачаються тому, що, мабуть, більшість методів контролера потрібно буде зіставити з конкретним HTTP-методом замість використання анотації @RequestMapping, яка за замовчуванням здійснює зіставлення з усіма HTTP-методами. Якщо тобі потрібний приклад складених анотацій, переглянь, як вони оголошуються.

Spring WebFlux також підтримує кастомні атрибути відображення запитів із кастомно вказаною логікою відображення запитів. Це більш розширений варіант, який вимагає створення підкласу RequestMappingHandlerMapping та перевизначення методу getCustomMethodCondition, в якому можна перевірити атрибут користувача і повернути власний RequestCondition.

Явна реєстрація

Ти можеш програмно реєструвати методи обробника, які можна використовувати для динамічної реєстрації або при більш складних випадках, наприклад, якщо є різні екземпляри одного і того ж обробника під різними URL-адресами. У цьому прикладі показано, як це зробити:

Java
@Configuration
public class MyConfig {
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
    throws NoSuchMethodException {
RequestMappingInfo info = RequestMappingInfo
        .paths("/user/{id}").methods(RequestMethod.GET).build(); 
Method method = UserHandler.class.getMethod("getUser", Long.class); 
mapping.registerMapping(info, handler, method); 
}
}
  1. Впроваджуємо цільовий обробник та відображення обробника для контролерів.
  2. Підготовлюємо метадані відображення запиту.
  3. Отримуємо метод обробника.
  4. Додаємо реєстрацію.
Kotlin
@Configuration
class MyConfig {
@Autowired
fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { 
val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() 
val method = UserHandler::class.java.getMethod("getUser", Long::class.java) 
mapping.registerMapping(info, handler, method) 
}
}
  1. Впроваджуємо цільовий обробник та відображення обробника для контролерів.
  2. Підготовлюємо метадані відображення запиту.
  3. Отримуємо метод обробника.
  4. Додаємо реєстрацію .

Методи обробника

Методи обробника з анотацією @RequestMapping мають гнучку сигнатуру і можуть вибирати з низки підтримуваних аргументів і значень методу контролера, що повертаються.

Аргументи методу

У наступній таблиці наведено підтримувані аргументи методу контролера.

Реактивні типи (Reactor, RxJava або інші) підтримуються в аргументах, для вирішення яких потрібне блокування введення-виведення (наприклад, читання тіла запиту). Це зазначено у колонці "Опис". Реактивні типи не очікуються для аргументів, які не вимагають блокування.

Аргумент java.util.Optional з JDK 1.8 підтримується як аргумент методу у поєднанні з анотаціями, що мають атрибут required (наприклад, @RequestParam, @RequestHeader та інші), і еквівалентний required=false.

Аргумент методу контролера Опис

ServerWebExchange

Забезпечує доступ до повного ServerWebExchange — контейнер для запиту та відповіді HTTP, атрибути запиту та сесії, методи checkNotModified тощо.

ServerHttpRequest, ServerHttpResponse

Забезпечує доступ до запиту або відповіді HTTP.

WebSession

Забезпечує доступ до сесії. Це не призводить до початку нової сесії, якщо не додано атрибути. Підтримує реактивні типи.

java.security.Principal

Поточний автентифікований користувач — можливо, конкретний клас реалізації Principal, якщо він відомий. Підтримує реактивні типи.

org.springframework.http.HttpMethod

HTTP-метод запиту.

java.util.Locale

Поточні регіональні налаштування запиту, що визначаються найбільш конкретним доступним LocaleResolver — по суті, налаштованим LocaleResolver або LocaleContextResolver.

java.util.TimeZone + java.time.ZoneId

Часовий пояс, пов'язаний з поточним запитом, визначений LocaleContextResolver.

@PathVariable

Призначений для забезпечення доступу до змінних шаблонів URI-ідентифікаторів.

@MatrixVariable

Призначений для забезпечення доступу до пар "ім'я-значення" в сегментах шляху URI-ідентифікаторів.

@RequestParam

Забезпечує доступ до параметрів запиту. Значення параметрів перетворюються на оголошений тип аргументів методу.

Зверни увагу, що використовувати анотацію @RequestParam необов'язково — наприклад, для встановлення його атрибутів. Див. розділ "Будь-який інший аргумент" у цій таблиці.

@RequestHeader

Призначений для забезпечення доступу до заголовків запитів. Значення заголовків перетворюються на оголошений тип аргументу методу.

Призначений для доступу до cookie. Значення cookie перетворюються на оголошений тип аргументу методу.

@RequestBody

Забезпечує доступ до HTTP-запиту. Вміст тіла перетворюється на оголошений тип аргументу методу за допомогою екземплярів HttpMessageReader. Підтримує реактивні типи.

HttpEntity<B>

Призначений для забезпечення доступу до заголовків та тіла запиту. Тіло перетворюється за допомогою екземплярів HttpMessageReader. Підтримує реактивні типи.

@RequestPart

Забезпечує доступ до компоненту у запиті multipart/form-data. Підтримує реактивні типи.

java.util.Map, org.springframework.ui.Model, і org.springframework.ui.ModelMap.

Призначені для забезпечення доступу до моделі, яка використовується в HTML-контролерах і відображається у шаблонах як частина візуалізації подання.

@ModelAttribute

Призначений для забезпечення доступу до існуючого атрибуту в моделі (примірник якого створюється, якщо він відсутній) з прив'язкою даних та валідацією. Зверни увагу, що використовувати анотацію @ModelAttribute необов'язково: наприклад, для встановлення його атрибутів. Див. розділ "Будь-який інший аргумент" у цій таблиці.

Errors, BindingResult

Забезпечують доступ до помилок валідації та прив'язки даних об'єкта команди, тобто аргумент @ModelAttribute. Аргумент Errors або BindingResult потрібно оголошувати відразу після аргументу методу, що валідується.

SessionStatus + class-level @SessionAttributes

Призначений для позначення завершення обробки форми, що викликає очищення атрибутів сесії, оголошених через анотацію @SessionAttributes на рівні класу. Детальнішу інформацію див. у розділі, присвяченому анотації @SessionAttributes.

UriComponentsBuilder

Призначений для підготовки URL-адреси, пов'язаної з хостом, портом, схемою та контекстним шляхом поточного запиту.

@SessionAttribute

Призначений для забезпечення доступу до будь-якого атрибуту сесії, на відміну від атрибутів моделі, що зберігаються в сесії в результаті оголошення анотації @SessionAttributes на рівні класу.

@RequestAttribute

Призначений для забезпечення доступу до атрибутів запиту.

Будь-який інший аргумент

Якщо аргумент методу не відповідає жодному з перелічених вище, він за замовчуванням дозволяється як @RequestParam, якщо це простий тип, що визначається BeanUtils#isSimplePropert, або як @ModelAttribute, в іншому випадку.

Значення, що повертаються

У наступній таблиці наведено підтримувані значення методу контролера. Зверни увагу, що реактивні типи з таких бібліотек, як Reactor, RxJava або інших, зазвичай підтримуються для всіх значень, що повертаються.

Значення методу контролера, що обертається Опис

@ResponseBody

Значення, що повертається, кодується через екземпляри HttpMessageWriter і записується у відповідь.

HttpEntity<B>, ResponseEntity<B>

Значення, що повертається, вказує, що повна відповідь, включно з HTTP-заголовками і тілом кодуватимуться через екземпляри HttpMessageWriter і записуватимуться у відповідь.

HttpHeaders

Призначено для повернення відповіді із заголовками і без тіла.

String

Ім'я подання, яке має бути розпізнане за допомогою екземплярів ViewResolver та використовуватися разом з неявною моделлю – визначається через об'єкти команд та методи, позначені анотацією @ModelAttribute. Метод обробника також може програмно доопрацювати модель, оголосивши аргумент Model.

View

Екземпляр View, призначений для візуалізації разом з неявною моделлю — визначається через об'єкти команд та методи з анотацією @ModelAttribute. Метод обробника також може програмно доопрацювати модель, оголосивши аргумент Model.

java.util.Map, org.springframework.ui.Model

Атрибути для додавання до неявної моделі, при цьому ім'я подання неявно визначається на основі шляху запиту.

@ModelAttribute

Атрибут, що додається до моделі, якщо при цьому маючи уявлення неявно визначається на основі шляху запиту.

Зверни увагу, що анотація @ModelAttribute є необов'язковою. Див. розділ "Будь-яке інше значення, що повертається" далі в цій таблиці.

Rendering

API для сценаріїв візуалізації моделей та уявлень.

void

Вважається, що метод з типом, що повертається void, який, ймовірно, є асинхронним (наприклад, Mono<Void>) (або з значенням, що повертається) null), повністю обробив відповідь, якщо вона також має аргумент ServerHttpResponse, ServerWebExchange або анотацію @ResponseStatus. Те саме вірно, якщо контролер виконав позитивну перевірку ETag або тимчасової мітки lastModified.

Якщо жодне зперелічених вище значень не вірне, повертається тип void може також вказувати на "відсутність тіла відповіді" для REST-контролерів або вибір імені подання за промовчанням для HTML-контролерів.

Flux<ServerSentEvent> ;, Observable<ServerSentEvent>, або інший реактивний тип

Пов'язаний з генерацією подій, що посилаються сервером. Функцію-обгортку ServerSentEvent можна опустити, якщо необхідно записати тільки дані (проте, text/event-stream має бути запитаний або оголошений у відображенні через атрибут produces).

Будь-яке інше значення, що повертається

Якщо значення, що повертається, не відповідає жодному з вищезазначених, воно за замовчуванням обробляється як ім'я подання, якщо це String або void (застосовується вибір імені подання за умовчанням), або як атрибут моделі, який має бути доданий до моделі, якщо тільки це не простий тип, як визначено BeanUtils#isSimpleProperty, і в цьому випадку він залишається невирішеним.

Перетворення типів

Деякі анотовані аргументи методу контролера, які представляють введення запиту на основі рядків (такі як @RequestParam, @RequestHeader, @ PathVariable, @MatrixVariable і @CookieValue))), можуть вимагати перетворення типу, якщо аргумент оголошений не як String.

У таких випадках перетворення типів застосовується автоматично на основі конфігурованих перетворювачів. За замовчуванням підтримуються прості типи (такі як int, long, Date та інші). Перетворення типів можна налаштувати за допомогою WebDataBinder або шляхом реєстрації Formatters за допомогою FormattingConversionService.

Практичним питанням при перетворенні типів є обробка типів порожнього вихідного значення рядка. Таке значення розглядається як відсутнє, якщо воно стає null у результаті перетворення типу. Це може бути характерним для Long, UUID та інших цільових типів. Якщо вам потрібно допустити впровадження null, або використовуйте прапор required в анотації аргументу, або оголосіть аргумент як @Nullable.

Матричні змінні

У RFC 3986 описані пари "ім'я -значення" у сегментах шляху. У Spring WebFlux ми називаємо їх "матричними змінними", ґрунтуючись на "старому пості" Тіма Бернерса-Лі, але їх також можна назвати параметрами URI-шляху.

Матричні змінні можуть з'являтися в будь-якому сегменті шляху, водночас кожна змінна відокремлюється крапкою з комою, а кілька значень розділяються комами — наприклад, " /cars;color=red,green;year=2012". Множинні значення також можуть бути задані через імена змінних, що повторюються, наприклад, color=red;color=green;color=blue).

На відміну від Spring MVC, у WebFlux наявність або відсутність матричних змінних в URL не впливає на відображення запитів. Іншими словами, тобі не обов'язково використовувати URI-змінну для маскування вмісту змінної. Тим не менш, якщо потрібно отримати доступ до матричних змінних з методу контролера, то потрібно додати URI-змінну сегмент шляху, де очікуються матричні змінні. У цьому прикладі показано, як це зробити:

Java
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
Kotlin
 // GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {
// petId == 42
// q == 11
}

Враховуючи, що всі сегменти шляху можуть містити матричні змінні, іноді може знадобитися визначити, в якому змінному шляху має бути матрична змінна, як показано в наступному прикладі:

Java
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable(name="q", pathVar="ownerId") int q1,
@MatrixVariable(name="q", pathVar="petId") int q2) {
// q1 == 11
// q2 == 22
}
Kotlin
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
@MatrixVariable(name = "q", pathVar = "petId") q2: Int) {
// q1 == 11
// q2 == 22
}

Можна визначити матричну змінну як необов'язкову та вказати значення за замовчуванням, як показано в наступному прикладі:

Java
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
Kotlin
// GET /pets/42
@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {
// q == 1
}

Щоб отримати всі змінні матриці, використовуй MultiValueMap, як показано на прикладі:

Java
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable MultiValueMap<String, String> matrixVars,
@MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
Kotlin

// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable matrixVars: MultiValueMap<String, String>,
@MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}

@RequestParam

Можна використовувати анотацію @RequestParam для прив'язування параметрів запиту до аргументу методу в контролері. Наступний фрагмент коду демонструє використання:

Java
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) { 
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
  1. Використання анотації @ RequestParam.
Kotlin
import org.springframework.ui.set
@Controller
@RequestMapping("/pets")
class EditPetForm {
// ...
@GetMapping
fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { 
val pet = clinic.loadPet(petId)
model["pet"] = pet
return "petForm"
}
// ...
}
  1. Використання анотації @RequestParam.
Поняття "параметр запиту" в Servlet API поєднує параметри запиту, дані форми та багатокомпонентні дані в одне ціле. Однак у WebFlux доступ до кожного з них здійснюється окремо через ServerWebExchange. Хоча анотація @RequestParam прив'язується лише до параметрів запиту, можна використовувати прив'язку даних для застосування параметрів запиту, даних форми та багатокомпонентних елементів до об'єкта команди.

Параметри методу, що використовують анотацію @RequestParam, за замовчуванням є обов'язковими, але також можна зазначити, щоб параметр методу не був обов'язковим, встановивши відповідний прапор для @RequestParam у false або оголосивши аргумент з за допомогою обгортки java.util.Optional.

Якщо тип параметра цільового методу не є String, перетворення типів застосовується автоматично.

Якщо анотація @RequestParam оголошена для аргументу Map<String, String> або MultiValueMap<String, String>, Map заповнюється всіма параметрами запиту.

Зверни увагу, що використовувати анотацію @RequestParam необов'язково — наприклад, для встановлення його атрибутів. За промовчанням будь-який аргумент, який є простим типом значення (як визначено BeanUtils#isSimpleProperty) і не дозволений ніяким іншим розпізнавальником аргументів, обробляється так, якби він був анотований @RequestParam.

@RequestHeader

Ти можеш використовувати анотацію @RequestHeader для прив'язки заголовка запиту до аргументу методу в контролері.

У наступному прикладі показаний запит із заголовками:

Host                    localhost:8080
Accept                  text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language         fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding         gzip,deflate
Accept-Charset          ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive              300      

Код з наступного прикладу дозволяє отримати значення заголовків Accept- Encoding та Keep-Alive:

Java
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding, (1)
@RequestHeader("Keep-Alive") long keepAlive) { 
//...
}
  1. Отримуємо значення заголовка Accept-Encoding.
  2. Отримуємо значення заголовка Keep-Alive.
Kotlin
@GetMapping("/demo")
fun handle(
@RequestHeader("Accept-Encoding") encoding: String, (1)
@RequestHeader("Keep-Alive") keepAlive: Long) { 
//...
}
  1. Отримуємо значення заголовка Accept-Encoding.
  2. Отримуємо значення заголовка Keep-Alive.

Якщо тип параметра цільового методу не є String, перетворення типів застосовується автоматично.

Якщо анотація @RequestHeader використовується в аргументі Map<String, String>, MultiValueMap<String, String> або HttpHeaders, Map заповнюється всіма значеннями заголовків.

Є вбудована підтримка для перетворення рядка, розділеної комою, на масив або колекцію рядків або інших типів, відомих системі перетворення типів. Наприклад, параметр методу, анотований @RequestHeader("Accept"), може мати тип String, а також String[] або List< ;String>.

@CookieValue

Ти можеш використовувати анотацію @CookieValue для прив'язки значення HTTP cookie до аргументу методу в контролері.

У наступному прикладі показаний запит із файлом cookie:

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

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

Java
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { 
//...
}
  1. Отримання значення cookie.
Kotlin
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { 
//...
}
  1. Отримання значення cookie.

Якщо тип параметра цільового методу не є String, перетворення типів застосовується автоматично.

@ModelAttribute

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

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { }
  1. Прив'язуємо екземпляр Pet.
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { }
  1. Прив'язуємо екземпляр Pet.

Екземпляр Pet у попередньому прикладі дозволяється таким чином:

  • З моделі, якщо вона вже додана через Model.

  • З HTTP-сесії через анотацію @SessionAttributes.

  • З виклику конструктора за замовчуванням.

  • З виклику "головного конструктора" з аргументами, що відповідають параметрам запиту або полям форми. Імена аргументів визначаються через анотацію @ConstructorProperties для JavaBeans або через імена параметрів, що зберігаються під час виконання, в байт-коді.

Після отримання екземпляра атрибута моделі застосовується прив'язування даних. Клас WebExchangeDataBinder зіставляє імена параметрів запиту та полів форми з іменами полів цільового Object. Відповідні поля заповнюються після перетворення типів, де це необхідно.

Прив'язка даних може призвести до помилок. За замовчуванням генерується WebExchangeBindException, але для перевірки таких помилок у методі контролера можна додати аргумент BindingResult безпосередньо поряд з анотацією @ModelAttribute, як показано в наступному прикладі :

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { 
if (result.hasErrors()) {
return "petForm";
}
// ...
}
  1. Додати BindingResult.
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { 
if (result.hasErrors()) {
return "petForm"
}
// ...
}
  1. Додати BindingResult.

Можна автоматично застосовувати валідацію після прив'язки даних, додавши анотацію javax.validation.Valid або анотацію @Validated із Spring. У цьому прикладі використовується анотація @Valid:

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { 
if (result.hasErrors()) {
return "petForm";
}
// ...
}
  1. Використання анотації @ Valid для аргументу атрибута моделі.
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { 
if (result.hasErrors()) {
return "petForm"
}
// ...
}
  1. Використання анотації @Valid для аргументу атрибуту моделі.

Spring WebFlux, на відміну від Spring MVC, підтримує реактивні типи в моделі — наприклад, Mono<Account> або io.reactivex.Single<Account>. Ти можеш оголосити аргумент, позначений анотацією @ModelAttribute, з функцією-обгорткою реактивного типу або без неї, після чого він буде дозволений відповідним чином у фактичне значення, якщо це необхідно. Однак зверни увагу, що для використання аргументу BindingResult перед ним необхідно оголосити аргумент з анотацією @ModelAttribute без функції-обгортки реактивного типу, як було показано раніше. До того ж, можна обробляти будь-які помилки через реактивний тип, як це показано в наступному прикладі:

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
return petMono
.flatMap(pet -> {
    // ...
})
.onErrorResume(ex -> {
    // ...
});
}
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") petMono: Mono<Pet>): Mono<String> {
return petMono
    .flatMap { pet ->
        // ...
    }
    .onErrorResume{ ex ->
        // ...
    }
}

Зверни увагу, що використовувати анотацію @ ModelAttribute необов'язково — наприклад, для встановлення його атрибутів. За замовчуванням будь-який аргумент, який не є простим типом значення (як визначено BeanUtils#isSimpleProperty) і не дозволяється жодним іншим розпізнавальником аргументів, обробляється так, якби він був анотований @ModelAttribute.

@SessionAttributes

@SessionAttributes використовується для зберігання атрибутів моделі в WebSession між запитами. Це анотація лише на рівні типу, яка оголошує атрибути сесії, використовувані конкретним контролером. Тут зазвичай перераховуються імена атрибутів моделі або типи атрибутів моделі, які повинні прозоро зберігатися в сесії для наступних запитів на доступ.

Розглянемо наступний приклад:

Java
 @Controller
@SessionAttributes("pet") 
public class EditPetForm {
// ...
}
  1. Використання анотації @SessionAttributes.
Kotlin
@Controller
@SessionAttributes("pet") 
class EditPetForm {
// ...
} 
  1. Використання анотації @SessionAttributes.

При першому запиті, якщо модель додається до атрибуту моделі з ім'ям pet, він автоматично просувається в WebSession і зберігається в ній. Він залишається там, поки інший метод контролера не використовує аргумент методу SessionStatus для очищення сховища, як показано в наступному прикладі:

Java
@Controller
@SessionAttributes("pet") 
public class EditPetForm {
// ...
@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) { 
if (errors.hasErrors()) {
    // ...
}
    status.setComplete();
    // ...
}
}
}
  1. Використання анотації @SessionAttributes.
  2. Використання змінної SessionStatus.
Kotlin
@Controller
@SessionAttributes("pet") 
class EditPetForm {
// ...
@PostMapping("/pets/{id}")
fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String { 
if (errors.hasErrors()) {
    // ...
}
status.setComplete()
// ...
}
}
  1. Використання анотації @SessionAttributes.
  2. Використання змінної SessionStatus.

@SessionAttribute

Якщо потрібний доступ до заздалегідь існуючих атрибутів сесії, які управляються глобально (тобто поза контролером — наприклад, фільтром) і можуть бути присутніми або відсутніми, то можна використовувати анотацію @SessionAttribute для параметра методу, як показано в наступному прикладі:

Java
@GetMapping("/")
public String handle(@SessionAttribute User user) { 
// ...
}
  1. Використання анотації @SessionAttribute.
Kotlin
@GetMapping("/")
fun handle(@SessionAttribute user: User): String { 
// ...
}
  1. Використання анотації @SessionAttribute.

Для випадків використання, які вимагають додавання або видалення атрибутів сесії, зверни увагу на можливість впровадження WebSession у метод контролера.

Для тимчасового зберігання атрибутів моделі в сесії як частини робочого процесу контролера, розглянь можливість використання SessionAttributes.

@RequestAttribute

Аналогічно анотації @SessionAttribute, можна використовувати анотацію @RequestAttribute для здійснення доступу до вже існуючих атрибутів запиту, створеного раніше (наприклад, WebFilter), як показано в наступному прикладі:

Java
@GetMapping("/")
public String handle(@RequestAttribute Client client) { 
// ...
}
  1. Використання анотації @RequestAttribute.
Kotlin
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { 
// ...
}
  1. Використання анотації @RequestAttribute.

Багатокомпонентний вміст

ServerWebExchange забезпечує доступ до багатокомпонентного вмісту. Найкращий спосіб обробки форми завантаження файлу (наприклад, з браузера) у контролері — це прив'язка даних до об'єкта команди, як показано в наступному прикладі:

Java
class MyForm {
private String name;
private MultipartFile file;
// ...
}
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(MyForm form, BindingResult errors) {
// ...
}
}
Kotlin
class MyForm(
val name: String,
val file: MultipartFile)
@Controller
class FileUploadController {
@PostMapping("/form")
fun handleFormUpload(form: MyForm, errors: BindingResult): String {
// ...
}
}

Ти також можеш надсилати багатокомпонентні запити від небраузерних клієнтів у сценарії за допомогою RESTful служб. У цьому прикладі використовується файл разом з JSON:

POST /someUrl
Content-Type: multipart/mixed
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit
{
"name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...

Можна отримати доступ до окремих компонентів за допомогою анотації @RequestPart, як показано в наступному прикладі:

Java
@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, 
@RequestPart("file-data") FilePart file) { 
// ...
}
  1. Використання анотації @RequestPart для отримання метаданих.
  2. Використання анотації @RequestPart для отримання файлу.
Kotlin
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata,
@RequestPart("file-data") FilePart file): String { 
// ...
}
  1. Використання анотації @RequestPart для отримання метаданих.
  2. Використання анотації @RequestPart для отримання файлу.

Щоб десеріалізувати сирий вміст компонента (наприклад, в JSON — аналогічно інструкції @RequestBody), можна оголосити конкретний цільовий Object замість Part, як показано в наступному прикладі:

Java
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { 
// ...
}
  1. Використання анотації @RequestPart для отримання метаданих.
Kotlin
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { 
// ...
}
  1. Використання анотації @RequestPart для отримання метаданих.

Можно використати анотацію @RequestPart у поєднанні з анотацією javax.validation.Valid або анотацією @Validated зі Spring, що призведе до застосування стандартної Bean Validation. Помилки валідації призводять до виникнення WebExchangeBindException, внаслідок чого видається відповідь 400 (BAD_REQUEST). Виняток містить BindingResult з деталями помилки і також може бути оброблено в методі контролера шляхом оголошення аргументу з асинхронною функцією-оберткою та подальшого використання операторів, пов'язаних з помилкою:

Java

@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
// використовуємо один з операторів onError*...
}
Kotlin
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
// ...
}

Щоб отримати доступ до всіх багатокомпонентних даних у вигляді MultiValueMap, ви можеш використовувати анотацію @RequestBody, як це показано в наступному прикладі:

Java
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { 
// ...
}
  1. Використання анотації @RequestBody.
Kotlin
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { 
// ...
}
  1. Використання анотації @RequestBody.

Для послідовного потокового доступу до багатокомпонентних даних можна використовувати анотацію @RequestBody з Flux<Part> (або Flow<Part> у Kotlin), як показано в наступному прикладі:

Java
@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) { 
// ...
}
  1. Використання анотації @RequestBody.
Kotlin
@PostMapping("/")
fun handle(@RequestBody parts: Flow<Part>): String { 
// ...
}
  1. Використання анотації @RequestBody.

@RequestBody

Анотацію @RequestBody можна використовувати, щоб прочитати тіло запиту і десеріалізувати його в об'єкт Object HttpMessageReader. У цьому прикладі використовується аргумент, анотований @RequestBody:

Java
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
Kotlin
 @PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
// ...
}

На відміну від Spring MVC, у WebFlux аргумент методу, позначеного анотацією @RequestBody, підтримує реактивні типи і повністю неблокуюче читання та (клієнт-серверну) потокову передачу.

@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
// ...
}
Kotlin
@PostMapping("/accounts")
fun handle(@RequestBody accounts: Flow<Account>) {
// ...
}

Ти можеш використовувати опцію кодеків повідомлень, що передаються за протоколом HTTP, в конфігурації WebFlux для конфігурування або кастомного налаштування засобів читання повідомлень.

Анотацію @RequestBody можна застосовувати в поєднанні з анотацією javax.validation.Valid або анотацією @Validated із Spring, що призводить до застосування стандартної Bean Validation. Помилки валідації викликають WebExchangeBindException, що призводить до відповіді 400 (BAD_REQUEST). Виняток містить BindingResult з деталями помилки і також може бути оброблено в методі контролера шляхом оголошення аргументу з асинхронною функцією-обгорткою та подальшого використання операторів, пов'язаних з помилкою:

Java
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Mono<Account> account) {
// використовуємо один з операторів onError*...
}
Kotlin
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Mono<Account>) {
// ...
}

HttpEntity

HttpEntity більш-менш ідентичний анотації @RequestBody в плані використання, але ґрунтується на об'єкті-контейнер, який відкриває заголовки та тіло запиту. У цьому прикладі використовується HttpEntity:

Java
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
Kotlin
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
// ...
}

@ResponseBody

Анотацію @ResponseBody можна використовувати нам методом, щоб повернення серіалізувалося в тіло відповіді через HttpMessageWriter. У цьому прикладі показано, як це зробити:

Java
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
Kotlin
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
// ...
}

@ResponseBody також підтримується лише на рівні класу, у разі вона успадковується всіма методами контролера. У цьому полягає дія анотації @RestController, яка є нічим іншим, як мета-анотацією, позначеною анотаціями @Controller та @ResponseBody.

@ResponseBody підтримує реактивні типи, що означає, що ти можеш повертати типи Reactor або RxJava і отримувати відповідні асинхронні значення, які вони створюють.

Ти можеш комбінувати методи, позначені анотацією @ResponseBody, з представленнями серіалізації формату JSON.

Можна використовувати опцію кодеків повідомлень, що передаються за протоколом HTTP, у конфігурації WebFlux для конфігурування або кастомного налаштування пристроїв читання повідомлень.

ResponseEntity

Атрибут ResponseEntity подібний до анотації @ResponseBody, але зі статусом і заголовками. Наприклад:

Java
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).body(body);
}
Kotlin
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
val body: String = ...
val etag: String = ...
return ResponseEntity.ok().eTag(etag).build(body)
}

WebFlux підтримує використання реактивного типу з одним значенням для асинхронного створення ResponseEntity та/або реактивних типів з одним та кількома значеннями для тіла. Це дозволяє використовувати різні асинхронні відповіді з ResponseEntity таким чином:

  • ResponseEntity<Mono<T>> або ResponseEntity<Flux<T>> негайно повідомляють статус і заголовки відповіді, в той час як тіло відповіді надається асинхронно в пізніший момент. Використовуємо Mono, якщо тіло складається з 0..1 значень, або Flux, якщо воно може створювати декілька значень.

  • Mono<ResponseEntity<T>> передбачає всі три параметри — статус відповіді, заголовки і тіло — асинхронно в пізніший момент. Це дозволяє змінювати статус відповіді та заголовки залежно від результатів асинхронної обробки запиту.

  • Mono<ResponseEntity<Mono< code>Mono<ResponseEntity<Flux<T>>> є ще однією можливою, хоча і менш поширеною альтернативою. Спочатку вони асинхронно надають статус відповіді та заголовки, а потім, у другу чергу, тіло відповіді також асинхронно.

Jackson JSON

Spring передбачає підтримку бібліотеки Jackson JSON.

Подання JSON

Spring WebFlux передбачає вбудовані засоби підтримки подання серіалізації з бібліотеки Jackson, яка дозволяє візуалізувати виключно підмножину всіх полів Object. Щоб використовувати його з методами контролера, анотованими @ResponseBody, або класом ResponseEntity, можна використовувати анотацію @JsonView з Jackson для активації класу представлення серіалізації, як показано в наступному прикладі:

Java
@RestController
public class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView.class)
public User getUser() {
return new User("eric", "7!jd#h23");
}
}
public class User {
public interface WithoutPasswordView {};
public interface WithPasswordView extends WithoutPasswordView {};
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
@JsonView(WithoutPasswordView.class)
public String getUsername() {
return this.username;
}
@JsonView(WithPasswordView.class)
public String getPassword() {
return this.password;
}
}
Kotlin
@RestController
class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView::class)
fun getUser(): User {
return User("eric", "7!jd#h23")
}
}
class User(
@JsonView(WithoutPasswordView::class) val username: String,
@JsonView(WithPasswordView::class) val password: String
) {
interface WithoutPasswordView
interface WithPasswordView : WithoutPasswordView
}
Анотація @JsonView дозволяє використовувати масив класів подання, але можна задати лише одне за кожен метод контролера. Використовуй складовий інтерфейс, якщо тобі потрібно активувати не одне подання.

Model

Ти можеш використовувати анотацію @ModelAttribute:

  • Як аргумент методу в методах з анотацією @RequestMapping для створення або забезпечення доступу до об'єкта з моделі та прив'язки його до запиту через WebDataBinder.

  • Як анотацію на рівні методу в класах, позначених анотаціями @Controller або @ControllerAdvice, що допомагає ініціалізувати модель перед викликом методу з анотацією @RequestMapping.

  • На методі з анотацією @RequestMapping, щоб позначити його значення, що повертається як атрибут моделі.

У цьому розділі описано методи, анотовані @ModelAttribute, або другий пункт з попереднього списку. Контролер може мати будь-яку кількість методів, позначених анотацією @ModelAttribute. Усі такі методи викликаються перед методами, позначеними анотацією @RequestMapping у тому контролері. Метод із анотацією @ModelAttribute також можна розділити між контролерами через анотацію @ControllerAdvice.

Методи з анотацією @ModelAttribute мають гнучкі сигнатури методов. Вони підтримують багато з тих самих аргументів, як і методи, помічені анотацією @RequestMapping, крім самої анотації @ModelAttribute чи всього, що пов'язані з тілом запиту.

У наступному прикладі використовується метод, позначений анотацією @ModelAttribute:

Java

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}}
Kotlin
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
model.addAttribute(accountRepository.findAccount(number))
// add more ...
}

У наступному прикладі додано тільки один атрибут:

Java
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
Kotlin
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
return accountRepository.findAccount(number);
}
Якщо ім'я не вказано явним чином, то ім'я за замовчуванням вибирається на основі типу, як це описано в javadoc угод. Завжди можна присвоїти явне ім'я за допомогою перевантаженого методу addAttribute або через атрибут name в анотації @ModelAttribute (для значення, що повертається).

Spring WebFlux, на відміну від Spring MVC, явно підтримує реактивні типи в моделі (наприклад, Mono<Account> або io.reactivex.Single<Account>). Такі асинхронні атрибути моделі можна прозоро дозволити (і оновити модель) у їх фактичні значення під час виклику @RequestMapping, якщо аргумент з анотацією @ModelAttribute оголошено без функції-обгортки, як показано у наступному прикладі:

Java
@ModelAttribute
public void addAccount(@RequestParam String number) {
Mono<Account> accountMono = accountRepository.findAccount(number);
model.addAttribute("account", accountMono);
}
@PostMapping("/accounts")
public String handle(@ModelAttribute Account account, BindingResult errors) {
// ...
}
Kotlin
import org.springframework.ui.set
@ModelAttribute
fun addAccount(@RequestParam number: String) {
val accountMono: Mono<Account> = accountRepository.findAccount(number)
model["account"] = accountMono
}
@PostMapping("/accounts")
fun handle(@ModelAttribute account: Account, errors: BindingResult): String {
// ...
}

Крім того, будь-які атрибути моделі, що мають функцію-обгортку реактивного типу, дозволяються в їх фактичні значення (а модель оновлюється) безпосередньо перед візуалізацією подання.

Також можна використовувати анотацію @ModelAttribute як анотацію на рівні методу для методів, помічених анотацією @RequestMapping, і в цьому випадку значення методу, що повертається анотацією @RequestMapping інтерпретується як атрибут моделі. Зазвичай робити це не потрібно, оскільки така логіка роботи задана за замовчуванням у HTML-контролерах, якщо тільки значення, що повертається, не є String, яка в іншому випадку буде інтерпретована як ім'я уявлення. Анотація @ModelAttribute також може допомогти кастомно налаштувати ім'я атрибута моделі, як показано в наведеному нижче прикладі:

Java
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}
Kotlin
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
// ...
return account
}

DataBinder

Класи з анотацією @Controller або @ControllerAdvice можуть мати методи, анотовані @InitBinder, щоб ініціалізувати екземпляри WebDataBinder. Ті, у свою чергу, використовуються для:

  • Прив'язування параметрів запиту (тобто даних форми або запиту) до об'єкта моделі.

  • Перетворення значень запиту на основі String(таких як параметри запиту, змінні шляхи, заголовки, cookie та ін) в цільовий тип аргументів методу контролера.

  • Форматування значень об'єктів моделі як String значень при візуалізації HTML-форм.

Методи з анотацією @InitBinder можуть реєструвати специфічні для контролера компоненти java.beans.PropertyEditor або Converter та Formatter із Spring. До того ж, можна використовувати Java-конфігурацію WebFlux для реєстрації типів Converter і Formatter у спільно використовуваному на глобальному рівні FormattingConversionService.

Методи, позначені анотацією @InitBinder, підтримують багато з тих же аргументів, що й методи з анотацією @RequestMapping, за винятком аргументів, анотованих @ModelAttribute (Об'єкт команди). Як правило, вони оголошуються з аргументом WebDataBinder для реєстрації та значенням void, що повертається. У цьому прикладі використовується анотація @InitBinder:

Java
@Controller
public class FormController {
@InitBinder 
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
  1. Використання анотації @ InitBinder.
Kotlin
class FormController {
@InitBinder 
fun initBinder(binder: WebDataBinder) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
dateFormat.isLenient = false
binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
}
// ...
}

Як варіант, при використанні конфігурації на основі Formatter через загальний FormattingConversionService можна повторно використовувати той самий підхід і зареєструвати екземпляри Formatter, специфічні для контролера, як показано в наступному прикладі:

public class FormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); 
}
// ...
}
  1. Додати форматер користувача (в даному випадку DateFormatter).
Kotlin
@Controller
class FormController {
@InitBinder
fun initBinder(binder: WebDataBinder) {
binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) 
}
// ...
}
  1. Додати форматер користувача (в даному випадку DateFormatter).

Структура моделі

У контексті вебдодатків прив'язування даних передбачає прив'язку параметрів HTTP-запиту (тобто даних форми або параметрів запиту) до властивостей об'єкта моделі та його вкладених об'єктів.

Для прив'язки даних відкриті лише public властивості, що відповідають угоди про ім'я JavaBeans – наприклад, методи public String getFirstName() та public void setFirstName(String) для властивості firstName.

Об'єкт моделі та його вкладений граф об'єктів також іноді називають об'єктом команди, базовим об'єктом форми або POJO ("старий добрий Java-об'єкт").

За замовчуванням Spring дозволяє прив'язку до всіх публічних властивостей у графі об'єктів моделі. Це означає, що необхідно ретельно продумувати, які публічні властивості є у моделі, оскільки клієнт може зробити метою будь-яку публічну властивість, навіть ті, які не передбачаються для цього випадку використання.

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

Рекомендований підхід полягає у використанні спеціалізованого об'єкта моделі, який відкриває лише ті властивості, які стосуються відправки форми. Наприклад, у формі зміни адреси електронної пошти користувача об'єкт моделі повинен оголосити мінімальний набір властивостей, як у наступному ChangeEmailForm.

public class ChangeEmailForm {
private String oldEmailAddress;
private String newEmailAddress;
public void setOldEmailAddress(String oldEmailAddress) {
this.oldEmailAddress = oldEmailAddress;
}
public String getOldEmailAddress() {
return this.oldEmailAddress;
}
public void setNewEmailAddress(String newEmailAddress) {
this.newEmailAddress = newEmailAddress;
}
public String getNewEmailAddress() {
return this.newEmailAddress;
}
}

Якщо у тебе немає можливості або ти не бажаєш використовувати спеціалізований об'єкт моделі для кожного випадку використання прив'язки даних, то має обмежити властивості, які дозволені для прив'язки даних. В ідеалі цього можна досягти, зареєструвавши шаблони допустимих полів за допомогою методу setAllowedFields() у WebDataBinder.

Наприклад, щоб зареєструвати шаблони допустимих полів у вашому додатку, можна реалізувати метод, позначений анотацією @InitBinder, в компоненті з анотацією @Controller або @ControllerAdvice, як показано нижче:

@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}

На додаток до реєстрації шаблонів допустимих полів, можна також зареєструвати шаблони неприпустимих полів за допомогою методу setDisallowedFields() у DataBinder та його підкласах. Однак зверни увагу, що "список, що допускає", безпечніший, ніж "список, що забороняє". Отже, використання setAllowedFields() слід віддати перевагу замість використання setDisallowedFields().

Зверни увагу, що зіставлення з шаблонами допустимих полів чутливе до регістру; тоді як зіставлення з шаблонами неприпустимих полів — ні. До того ж, поле, що відповідає шаблону неприпустимих полів, не буде прийнято, навіть якщо воно також відповідає шаблону в списку допустимих.

Дуже важливо конфігурувати шаблони припустимих і неприпустимих полів при безпосередньому відкритті моделі предметної області для прив'язки даних. В іншому випадку це буде великий ризик для безпеки.

Також рекомендується не використовувати типи з твоєї моделі предметної області, такі як сутності JPA або Hibernate, як об'єкт моделі в сценаріях прив'язки даних.

Керування винятками

Класи з анотацією @Controller та @ControllerAdvice можуть мати методи, позначені анотацією @ExceptionHandler , для обробки винятків із методів контролера. Наступний приклад містить такий метод обробника:

Java
@Controller
public class SimpleController {
// ...
@ExceptionHandler 
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
  1. Оголошення анотації @ExceptionHandler.
Kotlin
class SimpleController {
// ...
@ExceptionHandler 
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
}
  1. Оголошення анотації @ExceptionHandler.

Виняток може збігатися з поширюваним високорівневим винятком (наприклад, прямо генерований IOException) або з вкладеною причиною всередині виключення-обгортки (наприклад, IOException, обернене всередині IllegalStateException).

Для типів винятків, що збігаються, найкраще оголошувати цільовий виняток як аргумент методу, як показано у попередньому прикладі. До того ж, оголошення анотації може звужувати типи винятків, які повинні збігатися. Зазвичай ми рекомендуємо бути якомога конкретнішими в сигнатурі аргументу і оголошувати головні відображення кореневих винятків на анотацію @ControllerAdvice у відповідному порядку.

Метод з анотацією @ExceptionHandler у WebFlux підтримує ті ж аргументи методу і значення, що повертаються, що і метод з анотацією @RequestMapping, за винятком аргументів методу, пов'язаних з тілом запиту і анотацією @ModelAttribute.

Підтримка методів, анотованих @ExceptionHandler, Spring WebFlux забезпечується за допомогою HandlerAdapter для методів з анотацією @RequestMapping.

Робота з винятками в REST API

Спільною вимогою для служб на основі REST є включення інформації про помилку в тіло відповіді. Spring Framework не здійснює це автоматично, оскільки вказівка інформації про помилку у тілі відповіді залежить від конкретної програми. Однак анотація @RestController може використовувати методи, позначені анотацією @ExceptionHandler, з значенням ResponseEntity, що повертається, для встановлення статусу і тіла відповіді. Такі методи також можуть бути оголошені в класах з анотацією @ControllerAdvice, щоб застосовувати їх на глобальному рівні.

Зверніть увагу, що Spring WebFlux не має еквівалента для ResponseEntityExceptionHandler з Spring MVC, оскільки WebFlux викликає лише ResponseStatusException (або їх підкласи), і їх не потрібно перетворювати на код стану HTTP.

Advice контролера

Зазвичай методи, позначені анотаціями @ExceptionHandler, @InitBinder та @ModelAttribute, застосовуються в межах класу @Controller (або ієрархії класів), в якому вони оголошені. Якщо тобі потрібно, щоб такі методи застосовувалися більш глобально (для всіх контролерів), можна оголосити їх у класі, анотованому @ControllerAdvice або @RestControllerAdvice.

Анотація @ControllerAdvice анотована за допомогою @Component, що означає, що такі класи можна реєструвати як бін Spring через сканування компонентів. Анотація @RestControllerAdvice — це складена анотація, яка позначена і анотацією @ControllerAdvice, і анотацією @ResponseBody, що означає, що методи з анотацією @ExceptionHandler візуалізуються в тілі відповіді через перетворення повідомлень (на відміну від дозволу уявлень або візуалізації шаблону).

При запуску класи інфраструктури для методів, анотованих @RequestMapping та @ExceptionHandler виявляють біни Spring, анотовані @ControllerAdvice, а потім застосовують їх методи під час виконання. Глобальні методи, анотовані за допомогою @ExceptionHandler (з анотації @ControllerAdvice) застосовуються після локальних (з анотації @Controller ). Навпаки, глобальні методи з анотаціями @ModelAttribute та @InitBinder застосовуються перед локальними.

За замовчуванням методи, позначені анотацією @ControllerAdvice застосовуються до кожного запиту (тобто всіх контролерів), але можна звузити коло контролерів, використовуючи атрибути анотації, як показано в наступному прикладі:

Java

// Вибираємо метою всі контролери, анотовані за допомогою @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Вибираємо метою всі контролери в певних пакетах
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Вибираємо метою всі контролери, які призначаються певним класам
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
Kotlin

// Вибираємо метою всі контролери, анотовані за допомогою @RestController
@ControllerAdvice(annotations = [RestController::class])
public class ExampleAdvice1 {}
// Вибираємо метою всі контролери в певних пакетах
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Вибираємо метою всі контролери, які призначаються певним класам
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
public class ExampleAdvice3 {}

Селектори в попередньому прикладі обчислюються під час виконання і можуть негативно вплинути на продуктивність при широкому використанні. Детальнішу інформацію дивіться в javadoc по анотації @ControllerAdvice.