Spring MVC має всебічну інтеграцію асинхронної обробки запитів на Servlet 3.0:
Значення, що повертаються —
DeferredResult
таCallable
— у методах контролера забезпечують базову підтримку одного асинхронного значення, що повертається.Контролери можуть виконувати потокову передачу кількох значень, включно з SSE і сирими даними.
Контролери можуть використовувати реактивні клієнти та повертати реактивні типи для обробки відповідей.
DeferredResult
Щойно функція асинхронної обробки запитів буде активована в контейнері сервлетів, методи контролера зможуть обертати
будь-яке підтримуване значення методу контролера за
допомогою DeferredResult
, як показано в наступному прикладі:
@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<String>();
// З якогось іншого потоку...
return deferredResult;
}
// Зберігаємо deferredResult десь...
deferredResult.setResult(result);
@GetMapping("/quotes")
@ResponseBody
fun quotes(): DeferredResult<String> {
val deferredResult = DeferredResult<String>()
// З якогось іншого потоку...
return deferredResult
}
// Зберігаємо deferredResult десь...
deferredResult.setResult(result)
Контролер може отримати значення, що повертається, асинхронно, з іншого потоку — наприклад, у відповідь на зовнішню подію (повідомлення JMS), призначену задачу або іншу подію.
Callable
Контролер може обертати будь-яке підтримуване значення, що повертається, за допомогою java.util.concurrent.Callable
,
як показано в наступному прикладі:
@PostMapping
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public String call() throws Exception {
// ...
return "someView";
}
};
}
@PostMapping
fun processUpload(file: MultipartFile) = Callable<String> {
// ...
"someView"
}
Значення, що повертається, можна отримати, якщо виконати це завдання через конфігурований TaskExecutor
.
Обробка
Далі коротко описана асинхронна обробка запитів сервлетів:
ServletRequest
можна перевести в асинхронний режим, викликавшиrequest.startAsync()
. Основним результатом є те, що сервлет (і будь-які фільтри) може завершити виконання, але відповідь залишиться відкритою, щоб обробка завершилася пізніше.Виклик
request.startAsync()
повертаєAsyncContext
, який можна використовувати для подальшого контролю над асинхронною обробкою. Наприклад, він передбачає методdispatch
, який схожий на пересилання із Servlet API, за винятком того, що він дозволяє додатку відновити обробку запиту в потоці контейнера сервлетів.ServletRequest
надає доступ до поточногоDispatcherType
, який можна використовувати для розрізнення між обробкою початкового запиту, асинхронною диспетчеризацією, пересиланням та іншими типами диспетчерів.
Обробка DeferredResult
працює таким чином:
Контролер повертає
DeferredResult
і зберігає його в певній черзі або списку в пам'яті, де до нього можна отримати доступ.Spring MVC викликає
request.startAsync()
.Тим часом
DispatcherServlet
і всі налаштовані фільтри виходять з потоку обробки запиту, але відповідь залишається відкритою.Додаток встановлює
DeferredResult
з певного потоку, а Spring MVC надсилає запит назад до контейнера сервлетів.DispatcherServlet
викликається знову, а обробка відновлюється з використанням асинхронно отриманого значення, що повертається.
Обробка Callable
відбувається таким чином:
Контролер повертає
Callable
.Spring MVC викликає
request.startAsync()
і передаєCallable
уTaskExecutor
для обробки в окремому потоці.Тим часом
DispatcherServlet
і всі фільтри виходять з потоку контейнера сервлетів, але відповідь залишається відкритою.Зрештою
Callable
видає результат, і Spring MVC відправляє запит назад до контейнера сервлетів для завершення обробки.DispatcherServlet
викликається знову, а обробка поновлюється з використанням асинхронно отриманого значення, що повертається зCallable
.
Для отримання додаткової інформації та контексту можна також ознайомитися з постами в блозі, в яких була представлена підтримка асинхронної обробки запитів для Spring MVC 3.2.
Обробка винятків
Якщо ти використовуєш DeferredResult
, можеш вибрати, чи
викликати setResult
чи setErrorResult
за винятком. В обох випадках Spring MVC надсилає
запит назад до контейнера сервлетів для завершення обробки. Потім він враховується або якщо б метод контролера
повернув задане значення, або якщо б він видав зазначений виняток. Потім виняток проходить через звичайний механізм
обробки винятків (наприклад, виклик методів, анотованих @ExceptionHandler
).
Коли
використовується Callable
, відбувається аналогічна логіка обробки, але основна відмінність полягає в
тому, що результат повертається з Callable
або в ньому виникає виняток.
Перехоплення
Екземпляри HandlerInterceptor
можуть мати тип AsyncHandlerInterceptor
, щоб отримувати
зворотний виклик afterConcurrentHandlingStarted на початковий запит, який запускає асинхронну обробку (замість
postHandle та afterCompletion).
Реалізації HandlerInterceptor
, CallableProcessingInterceptor
або DeferredResultProcessingInterceptor
для більш глибокої
інтеграції з життєвим циклом асинхронного запиту (наприклад, для обробки події часу очікування). Детальнішу
інформацію дивіться у розділі, присвяченому AsyncHandlerInterceptor
.
DeferredResult
передбачає
зворотні виклики onTimeout(Runnable)
та onCompletion(Runnable)
. Докладніше про javadoc за DeferredResult
. Callable
можна замінити на WebAsyncTask
,
який відкриває додаткові методи для часу очікування та зворотного виклику після завершення.
У порівнянні з WebFlux
Servlet API спочатку створювався до виконання одного проходу за ланцюжком "фільтр-сервлет".
Асинхронна обробка запитів, додана в Servlet 3.0, дозволяє програмам виходити з ланцюжка "фільтр-сервлет", але
залишити відповідь відкритою для подальшої обробки. Підтримка асинхронності у Spring MVC побудована навколо цього
механізму. Коли контролер повертає DeferredResult
, виконання ланцюжка "фільтр-сервлет" завершується, а
потік контейнера сервлетів звільняється. Пізніше, коли DeferredResult
буде встановлено, виконується
ASYNC
відправка (на ту ж URL-адресу), під час чого контролер знову зіставляє, але замість виклику
використовується значення DeferredResult
(ніби контролер повернув його) для відновлення обробки.
Натомість Spring WebFlux не побудований на Servlet API, і йому не потрібна така функція асинхронної обробки запитів, оскільки він передбачає асинхронність за замовчуванням. Асинхронна обробка вбудована у всі контракти фреймворку і невід'ємно підтримується на всіх етапах обробки запиту.
З точки зору моделі програмування, і Spring MVC, і Spring WebFlux підтримують асинхронні та реактивні типи як значення, що повертаються в методах контролера. Spring MVC навіть підтримує потокову передачу, включно з реактивною зворотною реакцією. Однак окремі записи у відповідь залишаються блокуючими (і виконуються в окремому потоці), на відміну від WebFlux, який покладається на неблокуюче введення-виведення і не потребує додаткового потоку для кожного запису.
Ще одна фундаментальна відмінність
полягає в тому, що Spring MVC не підтримує асинхронні або реактивні типи в аргументах методів контролера (наприклад,
@RequestBody
, @RequestPart
та інші), а також не має явної підтримки асинхронних та
реактивних типів як атрибутів моделі. Spring WebFlux підтримує все зазначене.
Потокова передача за протоколом HTTP
Можна використовувати DeferredResult
та Callable
для одного асинхронного
значення, що повертається. Що, якщо тобі потрібно отримати кілька асинхронних значень і записати їх у відповідь? У
цьому розділі описано, як це зробити.
Об'єкти
Ти можеш використовувати значення ResponseBodyEmitter
,
що повертається,
для створення потоку об'єктів, де кожен об'єкт серіалізується за допомогою HttpMessageConverter
і
записується у відповідь, як показано в наступному прикладі:
@GetMapping("/events")
public ResponseBodyEmitter handle() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
// Зберігаємо відправник (емітер) десь...
return emitter;
}
// В іншому потоці
emitter.send("Hello once");
// і потім знову
emitter.send("Hello again");
// і в якийсь момент буде готово
emitter.complete();
@GetMapping( "/events")
fun handle() = ResponseBodyEmitter().apply {
// Зберігаємо відправник (емітер) десь...
}
// В іншому потоці
emitter.send("Hello once")
// і потім знову
.send("Hello again")
// і в якийсь момент буде готово
emitter.complete()
Також можна використовувати ResponseBodyEmitter
як тіло в ResponseEntity
, що
дозволить налаштувати статус і заголовки відповіді.
Коли emitter
генерує
IOException
(наприклад, якщо віддалений клієнт зник), програми не відповідають за очищення з'єднання і
не повинні викликати emitter.complete
або emitter.completeWithError
. Натомість контейнер
сервлетів автоматично ініціює повідомлення про помилку AsyncListener
, у якому Spring MVC здійснює
виклик completeWithError
. Цей виклик, у свою чергу, виконує останню ASYNC
відправку до
програми, під час якої Spring MVC викликає налаштовані розпізнавачі винятків і завершує запит.
SSE
SseEmitter
(підклас ResponseBodyEmitter
) забезпечує підтримку Server-Sent Events , якщо події, надіслані
сервером, відформатовані відповідно до специфікації W3C SSE. Щоб створити потік SSE з контролера, поверни SseEmitter
,
як показано в наведеному нижче прикладі:
@GetMapping(path="/events ", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {
SseEmitter emitter = new SseEmitter();
// Зберігаємо відправник (emitter) десь...
return emitter;
}
// В іншому потоці
emitter.send("Hello once");
// і потім знову
emitter.send("Hello again");
// і в якийсь момент буде готово
emitter.complete();
@GetMapping( "/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun handle() = SseEmitter().apply {
// Зберігаємо відправник (emitter) десь...
}
// В іншому потоці
emitter.send("Hello once ")
// і потім знову
emitter.send("Hello again")
// і в якийсь момент буде готово
emitter.complete()
Хоча SSE є основним варіантом для потокової передачі в браузери, зверни увагу, що Internet Explorer не підтримує події, що посилаються сервером (Server-Sent Events). Розглянь можливість використання обміну повідомленнями через протокол WebSocket з Spring із запасними варіантами механізмів передачі з протоколу SockJS (включно з SSE), які призначені для широкого спектру браузерів.
Сирі дані
Іноді корисно обійти перетворення повідомлень і здійснити потокову передачу
безпосередньо у відповідь OutputStream
(наприклад, при завантаженні файлу). Для цього можна
використовувати тип значення, що повертається StreamingResponseBody
, як показано в наступному прикладі:
@GetMapping("/download")
public StreamingResponseBody handle() {
return new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
// записуємо...
}
};
}
@GetMapping("/download")
fun handle() = StreamingResponseBody {
// записуємо ...
}
Можна використовувати StreamingResponseBody
як тіло в ResponseEntity
, щоб
налаштувати статус і заголовки відповіді.
Реактивні типи
Spring MVC підтримує використання
реактивних клієнтських бібліотек у контролері. Серед таких бібліотек WebClient
з
spring-webflux
та інші, такі як реактивні репозиторії даних проекту Spring Data. У таких сценаріях
зручно мати можливість повертати реактивні типи з методу контролера.
Реактивні значення, що повертаються, обробляються таким чином:
Проміс з одним значенням підлаштовується, як при використанні
DeferredResult
. Як приклад можна навестиMono
(Reactor) абоSingle
(RxJava).Багатозначний потік з потоковим типом середовища передачі даних (наприклад,
application/x-ndjson
абоtext/event-stream
) підлаштовується, як при використанніResponseBodyEmitter
абоSseEmitter
. Прикладами можуть бутиFlux
(Reactor) абоObservable
(RxJava). Програми також можуть повертатиFlux<ServerSentEvent>
абоObservable<ServerSentEvent>
.Багатозначний потік з будь-яким іншим типом середовища передачі даних (наприклад,
application/json
) підлаштовується, як при використанніDeferredResult<List<?>>
.
ReactiveAdapterRegistry
з
spring-core
, що дозволяє йому підлаштовуватися при переході між кількома реактивними бібліотеками.
Для потокової передачі даних у відповідь підтримується реактивна зворотна реакція, але запис у відповідь, як і
раніше, блокується і виконується в окремому потоці через сконфігурований TaskExecutor
, щоб уникнути
блокування джерела потоку, який стоїть вище (наприклад, Flux
, що повертається з WebClient
).
За замовчуванням для блокуючого запису використовується SimpleAsyncTaskExecutor
, але при завантаженні
він не підходить. Якщо планується використовувати потік з реактивним типом, слід використовувати конфігурацію MVC
для налаштування виконавця.
Втрата з'єднання
Servlet API не передає жодних повідомлень, коли віддалений клієнт пропаде. Тому при потоковій передачі відповіді, будь то через SseEmitter або реактивні типи, важливо періодично надсилати дані, оскільки запис не відбудеться, якщо клієнт від'єднався. Надсилання може приймати форму порожньої (тільки для коментарів) SSE-події або будь-яких інших даних, які інша сторона повинна буде інтерпретувати як heartbeat-повідомлення та ігнорувати.
До того ж, можна використовувати рішення для обміну вебповідомленнями (наприклад, STOMP поверх WebSocket або WebSocket з SockJS), які мають вбудований механізм передачі heartbeat-повідомлень.
Конфігурація
Функція асинхронної обробки запитів має бути активована на рівні контейнера сервлетів. Конфігурація MVC також відкриває кілька опцій для асинхронних запитів.
Контейнер сервлетів
Оголошення фільтрів та сервлетів мають прапор asyncSupported
, який має бути
встановлений у true
, щоб активувати асинхронну обробку запитів. До того ж, відображення фільтра
повинні бути оголошені для обробки ASYNC javax.servlet.DispatchType
.
У конфігурації Java, коли
ти використовуєш AbstractAnnotationConfigDispatcherServletInitializer
для ініціалізації контейнера
сервлетів, це відбувається автоматично.
У конфігурації web.xml
можна додати <async-supported>true</async-supported>
до DispatcherServlet
та до оголошень Filter
, а також додати <dispatcher>ASYNC</dispatcher>
до відображень фільтра.
Spring MVC
Конфігурація MVC передбачає наступні опції, пов'язані з асинхронною обробкою запитів:
Конфігурація Java: Використовуй зворотний виклик
configureAsyncSupport
дляWebMvcConfigurer
.Простір імен XML: Використовуй елемент
<async-support>
у розділі<mvc:annotation-driven> ;
.
Можна здійснити конфігурацію наступних параметрів:
Значення часу очікування за замовчуванням для асинхронних запитів, який, якщо воно не встановлено, залежить від базового контейнера сервлетів.
AsyncTaskExecutor
для використання з метою блокування запису при потоковій передачі даних із використанням реактивних типів і для виконання екземплярівCallable
, які повертаються з методів контролера. Рекомендуємо налаштувати цю властивість, якщо ти працюєш із реактивними типами або маєш методи контролера, що повертаютьCallable
, оскільки за замовчуванням цеSimpleAsyncTaskExecutor
.Реалізації DeferredResultProcessingInterceptor
та реалізаціїCallableProcessingInterceptor
.
Зверни увагу, що також можна встановити значення часу очікування за замовчуванням для
DeferredResult
, ResponseBodyEmitter
та SseEmitter
. Для
Callable
можна використовувати WebAsyncTask
, щоб вказати значення часу очікування.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ