Primavera MVC tiene una amplia integración del procesamiento de solicitudes asincrónicas en Servlet 3.0:

  • DeferredResult y Callable devuelven valores en los métodos del controlador proporciona soporte básico para un único valor de retorno asincrónico.

  • Los controladores pueden transmitir múltiples valores, incluidos SSE y datos sin procesar.

  • Los controladores pueden usar clientes reactivos y devolver tipos reactivos para procesar respuestas.

DeferredResult

Una vez que se activa la función de procesamiento de solicitudes asincrónicas habilitado en el contenedor de servlets, los métodos del controlador podrán encapsular cualquier valor de retorno admitido del método del controlador usando DeferredResult, como se muestra en el siguiente ejemplo:

Java

@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<String>();
    // De algún otro hilo...
    return deferredResult;
}
// Guardar resultado diferido en algún lugar...
deferredResult.setResult(result)
Kotlin

@GetMapping("/quotes")
@ResponseBody
fun quotes(): DeferredResult<String> {
    val deferredResult = DeferredResult<String>()
    // De algún otro hilo...
    return deferredResult
}
// Guardar deferredResult en algún lugar...
deferredResult.setResult(result)

El controlador puede recibir el valor de retorno de forma asincrónica, desde otro hilo, por ejemplo, en respuesta a un evento externo (mensaje JMS), una tarea programada u otro evento.

Callable

El controlador puede ajustar cualquier valor de retorno admitido usando java.util.concurrent.Callable, como se muestra en el siguiente ejemplo:

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"
}

El valor de retorno se puede obtener ejecutando esta tarea a través del TaskExecutor configurado .

Procesamiento

El procesamiento asincrónico de solicitudes de servlet se describe brevemente a continuación:

  • ServletRequest se puede configurar al modo asíncrono llamando a request.startAsync(). El efecto principal es que el servlet (así como cualquier filtro) puede completar la ejecución, pero la respuesta permanecerá abierta para que el procesamiento pueda finalizar más tarde.

  • Llame a request.startAsync() devuelve un AsyncContext que se puede utilizar para controlar aún más el procesamiento asincrónico. Por ejemplo, proporciona un método dispatch, que es similar al envío desde la API de servlet, excepto que permite a la aplicación reanudar el procesamiento de la solicitud en el subproceso del contenedor de servlet.

  • ServletRequest proporciona acceso al DispatcherType actual, que se puede utilizar para diferenciar entre procesamiento de solicitudes iniciales, envío asincrónico, reenvío y otros tipos de despachadores.

El procesamiento de DeferredResult funciona de la siguiente manera:

  • El controlador devuelve DeferredResult y lo almacena en alguna cola o lista en la memoria donde se puede acceder.

  • Spring MVC llama a request.startAsync().

  • Mientras tanto, DispatcherServlet y todos los filtros configurados salen del hilo de procesamiento de solicitudes, pero la respuesta permanece abierta.

  • La aplicación establece DeferredResult desde algún subproceso y Spring MVC envía la solicitud de regreso al contenedor de servlets.

  • DispatcherServlet se llama nuevamente y el procesamiento se reanuda utilizando el valor de retorno recibido asincrónicamente.

El procesamiento de Callable ocurre de la siguiente manera:

  • El controlador devuelve Callable.

  • Spring MVC llama a request.startAsync() y pasa Callable a TaskExecutor para ser procesado en un hilo separado.

  • Mientras tanto, DispatcherServlet y todos los filtros salen del subproceso del contenedor de servlets, pero la respuesta permanece abierta.

  • Eventualmente, Callable produce un resultado y Spring MVC envía la solicitud de regreso a el contenedor de servlets para completar el procesamiento.

  • DispatcherServlet se llama nuevamente y el procesamiento se reanuda utilizando el valor de retorno recibido asincrónicamente de Callable .

Para recibir Para obtener más información y contexto, consulte también publicaciones de blog que introdujeron soporte para el manejo de solicitudes asincrónicas para Spring MVC 3.2.

Excepción Manejo

Si está utilizando DeferredResult, puede elegir si llamar a setResult o setErrorResult con una excepción. En ambos casos, Spring MVC envía la solicitud de regreso al contenedor de servlets para completar el procesamiento. Luego se trata como si el método del controlador hubiera devuelto el valor especificado o como si hubiera generado la excepción especificada. Luego, la excepción pasa por el mecanismo normal de manejo de excepciones (por ejemplo, llamar a métodos anotados con @ExceptionHandler).

Cuando se utiliza Callable, se realiza un procesamiento similar ocurre la lógica, pero la principal diferencia es si el resultado se devuelve desde el Callable o si arroja una excepción.

Interceptación

Las instancias HandlerInterceptor pueden tener el tipo AsyncHandlerInterceptor para recibir una devolución de llamada afterConcurrentHandlingStarted en la solicitud inicial que desencadena el procesamiento asincrónico (en lugar de postHandle y afterCompletion).

HandlerInterceptor las implementaciones también pueden registrar CallableProcessingInterceptor o DeferredResultProcessingInterceptor para una integración más profunda con el ciclo de vida de la solicitud asincrónica (por ejemplo, para manejar un evento de tiempo de espera). Para obtener más información, consulte la sección sobre el destino AsyncHandlerInterceptor.

DeferredResult proporciona onTimeout(Runnable) y callbacks onCompletion(Ejecutable). Ver detalles en javadoc por DeferredResult. Callable se puede reemplazar con WebAsyncTask, que expone métodos adicionales para el tiempo de espera y la devolución de llamada de finalización.

En comparación con WebFlux

El servlet La API se diseñó originalmente para realizar un solo paso a lo largo de la cadena de servlet de filtro. El procesamiento de solicitudes asincrónico, agregado en Servlet 3.0, permite que las aplicaciones salgan de la cadena de filtro-servlet pero dejen la respuesta abierta para su posterior procesamiento. El soporte asíncrono de Spring MVC se basa en este mecanismo. Cuando el controlador devuelve DeferredResult, la cadena de filtro-servlet completa la ejecución y se libera el subproceso del contenedor de servlet. Más tarde, cuando se establece DeferredResult, se realiza un envío ASYNC (a la misma URL), durante el cual el controlador vuelve a coincidir, pero en lugar de llamar al valor de DeferredResult (como si el controlador lo hubiera devuelto) para reanudar el procesamiento.

Por el contrario, Spring WebFlux no está construido sobre la API de Servlet y no necesita esta función de procesamiento de solicitudes asincrónicas, ya que es asincrónico por definición. El procesamiento asincrónico está integrado en todos los contratos marco y es inherentemente compatible en todas las etapas del procesamiento de solicitudes.

Desde la perspectiva del modelo de programación, tanto Spring MVC como Spring WebFlux admiten tipos asincrónicos y reactivos como valores de retorno en el controlador. métodos. Spring MVC incluso admite subprocesos, incluida la reacción reactiva. Sin embargo, las escrituras individuales en respuesta siguen bloqueando (y se ejecutan en un hilo separado), a diferencia de WebFlux, que se basa en E/S sin bloqueo y no necesita un hilo adicional para cada escritura.

Otro fundamental La diferencia es que Spring MVC no admite tipos asíncronos o reactivos en los argumentos del método del controlador (por ejemplo, @RequestBody, @RequestPart y otros), y tampoco tiene soporte explícito para tipos asíncronos y reactivos como atributos del modelo. Spring WebFlux admite todo lo anterior.

Transmisión HTTP

Puede usar DeferredResult y Callable para un único valor de retorno asincrónico. ¿Qué sucede si necesita recibir varios valores asincrónicos y volver a escribirlos? Esta sección describe cómo hacer esto.

Objetos

Puede usar el valor de retorno de ResponseBodyEmitter para crear una secuencia de objetos, donde cada objeto está serializado usando HttpMessageConverter y se escribe en la respuesta, como se muestra en el siguiente ejemplo:

Java

@GetMapping("/events")
public ResponseBodyEmitter handle() {
    ResponseBodyEmitter emitter = new ResponseBodyEmitter();
    // Guarda el remitente (emisor) en algún lugar...
    return emitter;
}
// En otro hilo
emitter.send("Hello once");
// y luego otra vez
emitter.send("Hello again");
// y en algún momento estará listo
emitter.complete();
Kotlin

@GetMapping( "/events")
fun handle() = ResponseBodyEmitter().apply {
    // Save the sender (emitter) somewhere...
}
// En otro hilo
emitter.send("Hello once")
// y luego emitter nuevamente
.send("Hello again")
// y en algún momento estará listo
emitter.complete()

También puedes usar ResponseBodyEmitter como cuerpo en ResponseEntity, lo que le permitirá personalizar el estado de la respuesta y los encabezados.

Cuando el emisor arroja IOException (por ejemplo, si el cliente remoto ha desaparecido), las aplicaciones no son responsables de limpiar la conexión y no deben llamar a emitter.complete o emitter.completeWithError. En cambio, el contenedor de servlets activa automáticamente una notificación de error AsyncListener en la que Spring MVC realiza una llamada a completeWithError. Esta llamada, a su vez, realiza el envío ASYNC final a la aplicación, durante el cual Spring MVC llama a los controladores de excepciones configurados y completa la solicitud.

SSE

SseEmitter (una subclase de ResponseBodyEmitter) proporciona soporte para Server -Eventos enviados si los eventos enviados por el servidor están formateados según la especificación W3C SSE. Para crear una transmisión SSE desde un controlador, devuelva un SseEmitter como se muestra en el siguiente ejemplo:

Java

@GetMapping(path="/events ", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {
    SseEmitter emitter = new SseEmitter();
    // Guarda el remitente (emisor) en algún lugar...
    return emitter;
}
// En otro hilo
emitter.send("Hello once");
// y luego otra vez
emitter.send("Hello again");
// y en algún momento
emitter.complete();
Kotlin

@GetMapping( "/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun handle() = SseEmitter().apply {
    // Guarda el remitente (emisor) en algún lugar...
}
// En otro hilo
emitter.send("Hello once ")
// y luego otra vez
emitter.send("Hello again")
// y en algún estará listo
emitter.complete()

Mientras SSE es la opción principal para transmitir a navegadores; tenga en cuenta que Internet Explorer no admite eventos enviados por el servidor. Considere la posibilidad de utilizar la mensajería del protocolo WebSocket de Spring con mecanismos alternativos del protocolo SockJS (incluido SSE), que están diseñados para una amplia gama de navegadores.

Datos sin procesar

A veces es útil evitar el mensajes de conversión y transmitirlos directamente a la respuesta OutputStream (por ejemplo, al descargar un archivo). Para hacer esto, puede usar el tipo de retorno StreamingResponseBody, como se muestra en el siguiente ejemplo:

Java

@GetMapping("/download")
public StreamingResponseBody handle() {
    return new StreamingResponseBody() {
        @Override
        public void writeTo(OutputStream outputStream) throws IOException {
            // escribir...
        }
    };
}
Kotlin

@GetMapping("/download")
fun handle() = StreamingResponseBody {
    // escribir ...
}

Puede utilizar StreamingResponseBody como cuerpo en ResponseEntity para configurar el estado y encabezados de respuesta.

Tipos reactivos

Spring MVC admite el uso de bibliotecas de cliente reactivas en el controlador. Estas bibliotecas incluyen WebClient de spring-webflux y otras, como los repositorios de datos reactivos del proyecto Spring Data. En tales escenarios, es conveniente poder devolver tipos reactivos desde un método de controlador.

Los valores de retorno reactivos se manejan de la siguiente manera:

  • A La promesa de valor único se ajusta como si se usara DeferredResult. Los ejemplos incluyen Mono (Reactor) o Single (RxJava).

  • Transmisión de valores múltiples con datos de tipo de medios de transmisión ( por ejemplo, application/x-ndjson o text/event-stream) se ajusta, como cuando se usa ResponseBodyEmitter o SseEmitter. Algunos ejemplos son Flux (Reactor) o Observable (RxJava). Las aplicaciones también pueden devolver Flux<ServerSentEvent> o Observable<ServerSentEvent>.

  • Transmisión de valores múltiples con cualquier otro medio Los datos de tipo (por ejemplo, application/json) se ajustan, como cuando se usa DeferredResult<List<?>>.

Spring MVC admite Reactor y RxJava a través de ReactiveAdapterRegistry de spring-core, lo que le permite adaptarse al moverse entre múltiples bibliotecas reactivas.

Se admite la reacción reactiva para transmitir datos a la respuesta, pero la escritura en la respuesta aún se bloquea y se ejecuta en un hilo separado a través de un TaskExecutor configurado para evitar bloquear la fuente del hilo ascendente (por ejemplo, Flux devuelto desde WebClient). El valor predeterminado para bloquear la entrada es SimpleAsyncTaskExecutor, pero no es adecuado para cargar. Si planea usar una transmisión con un tipo reactivo, entonces debe usar la configuración MVC para configurar el ejecutor.

Conexión perdida

La API de Servlet no envía ninguna notificación cuando el control remoto El cliente desaparece. Por lo tanto, al transmitir una respuesta, ya sea a través de SseEmitter o de tipos reactivos, es importante enviar datos periódicamente, ya que no se escribirá si el cliente se ha desconectado. El envío podría tomar la forma de un evento SSE vacío (solo comentarios) o cualquier otro dato que la otra parte tendría que interpretar como un mensaje de latido e ignorar.

También puede utilizar soluciones de mensajería web (por ejemplo, (por ejemplo, STOMP sobre WebSocket o WebSocket con SockJS), que tienen un mecanismo incorporado para transmitir mensajes de latido.

Configuración

La función de procesamiento de solicitudes asincrónicas debe estar habilitada en el contenedor de servlets. nivel. La configuración de MVC también abre varias opciones para solicitudes asincrónicas.

Contenedor de servlet

Las declaraciones de filtro y servlet tienen un indicador asyncSupported que debe establecerse en true para habilitar el procesamiento de solicitudes asincrónicas. Además, se deben declarar asignaciones de filtro para manejar ASYNC javax.servlet.DispatchType.

En la configuración de Java, cuando usa AbstractAnnotationConfigDispatcherServletInitializer para inicializar el contenedor de servlets , esto sucede automáticamente.

En la configuración de web.xml, puede agregar <async-supported>true</async-supported> a DispatcherServlet y declaraciones Filter, y agregue <dispatcher>ASYNC</dispatcher> para filtrar asignaciones.

Spring MVC

La configuración de MVC proporciona las siguientes opciones relacionadas con el procesamiento de solicitudes asincrónicas:

  • Configuración de Java: use la devolución de llamada configureAsyncSupport para WebMvcConfigurer.

  • Espacio de nombres XML: use el elemento <async-support> en <mvc:annotation-driven> sección.

Se pueden configurar los siguientes parámetros:

  • El valor de tiempo de espera predeterminado para solicitudes asincrónicas, que, si no se configura, depende del contenedor de servlet subyacente.

  • AsyncTaskExecutor se utilizará con fines de bloqueo de escritura cuando se transmitan datos mediante tipos reactivos y para ejecutar instancias Callable devueltas por los métodos del controlador. Recomendamos encarecidamente configurar esta propiedad si está trabajando con tipos reactivos o tiene métodos de controlador que devuelven Callable, ya que el valor predeterminado es SimpleAsyncTaskExecutor.

  • Implementaciones de DeferredResultProcessingInterceptor e implementaciones deCallableProcessingInterceptor.

Tenga en cuenta que también es posible configurar el valor de tiempo de espera predeterminado para DeferredResult, ResponseBodyEmitter y SseEmitter. Para Callable puede usar WebAsyncTask para especificar el valor del tiempo de espera.