Spring MVC має всебічну інтеграцію асинхронної обробки запитів на Servlet 3.0:

  • Значення, що повертаються — DeferredResult та Callable — у методах контролера забезпечують базову підтримку одного асинхронного значення, що повертається.

  • Контролери можуть виконувати потокову передачу кількох значень, включно з SSE і сирими даними.

  • Контролери можуть використовувати реактивні клієнти та повертати реактивні типи для обробки відповідей.

DeferredResult

Щойно функція асинхронної обробки запитів буде активована в контейнері сервлетів, методи контролера зможуть обертати будь-яке підтримуване значення методу контролера за допомогою DeferredResult, як показано в наступному прикладі:

Java

@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<String>();
    // З якогось іншого потоку...
    return deferredResult;
}
// Зберігаємо deferredResult десь...
deferredResult.setResult(result);
Kotlin

@GetMapping("/quotes")
@ResponseBody
fun quotes(): DeferredResult<String> {
    val deferredResult = DeferredResult<String>()
    // З якогось іншого потоку...
    return deferredResult
}
// Зберігаємо deferredResult десь...
deferredResult.setResult(result)

Контролер може отримати значення, що повертається, асинхронно, з іншого потоку — наприклад, у відповідь на зовнішню подію (повідомлення JMS), призначену задачу або іншу подію.

Callable

Контролер може обертати будь-яке підтримуване значення, що повертається, за допомогою java.util.concurrent.Callable, як показано в наступному прикладі:

Java

@PostMapping
public Callable<String> processUpload(final MultipartFile file) {
    return new Callable<String>() {
        public String call() throws Exception {
            // ...
            return "someView";
        }
    };
}
Kotlin
@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 і записується у відповідь, як показано в наступному прикладі:

Java

@GetMapping("/events")
public ResponseBodyEmitter handle() {
    ResponseBodyEmitter emitter = new ResponseBodyEmitter();
    // Зберігаємо відправник (емітер) десь...
    return emitter;
}
// В іншому потоці
emitter.send("Hello once");
// і потім знову
emitter.send("Hello again");
// і в якийсь момент буде готово
emitter.complete();
Kotlin

@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, як показано в наведеному нижче прикладі:

Java

@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();
Kotlin

@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, як показано в наступному прикладі:

Java

@GetMapping("/download")
public StreamingResponseBody handle() {
    return new StreamingResponseBody() {
        @Override
        public void writeTo(OutputStream outputStream) throws IOException {
            // записуємо...
        }
    };
}
Kotlin
@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<?>>.

Spring MVC підтримує Reactor та RxJava через 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, щоб вказати значення часу очікування.