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")
// и потом снова
emitter.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();
    // Сохраняем отправитель (эмиттер) где-нибудь...
    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.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, чтобы указать значение времени ожидания.