天天看点

传统的Spring Web MVC如何像WebFlux一样异步处理请求?

文章目录

  • ​​写在前面​​
  • ​​一、DeferredResult​​
  • ​​二、Callable​​
  • ​​三、执行过程​​
  • ​​1、异常处理​​
  • ​​2、拦截​​
  • ​​3、与WebFlux的比较​​
  • ​​四、HTTP流​​
  • ​​1、响应多个对象​​
  • ​​2、SSE​​
  • ​​3、原始数据​​
  • ​​五、Reactive类型​​
  • ​​六、断开连接​​
  • ​​七、配置​​
  • ​​1、servlet容器​​
  • ​​2、spring MVC​​
  • ​​参考资料​​

写在前面

Spring MVC与Servlet 3.0异步请求处理进行了广泛的集成:

  • 控制器方法中的DeferredResult和 Callable返回值为单个异步返回值提供了基本支持。
  • 控制器可以传输多个值,包括SSE和原始数据。
  • 控制器可以使用反应式客户端并返回反应式类型进行响应处理。

一、DeferredResult

一旦在Servlet容器中启用了异步请求处理功能,控制器方法就可以用DeferredResult包装任何受支持的控制器方法返回值,如下例所示:

@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<String>();
    // Save the deferredResult somewhere..
    return deferredResult;
}

// From some other thread...
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";
        }
    };
}      

然后,可以通过配置的TaskExecutor运行给定的任务来获得返回值。

三、执行过程

下面是Servlet异步请求处理的一个非常简明的概述:

  • 可以通过调用request.startAsync()将ServletRequest置于异步模式。这样做的主要效果是Servlet(以及任何过滤器)可以退出,但是响应保持打开,让处理稍后完成。
  • 对request.startAsync()的调用返回AsyncContext,您可以使用它来进一步控制异步处理。例如,它提供了dispatch方法,类似于Servlet API的forward,只是它让应用程序恢复Servlet容器线程上的请求处理。
  • ServletRequest提供对当前DispatcherType的访问,您可以使用它来区分处理初始请求、异步调度、转发和其他调度程序类型。

DeferredResult 处理的工作方式如下:

  • 控制器返回一个DeferredResult,并将其保存在某个可以访问的内存队列或列表中。
  • Spring MVC调用request.startAsync()。
  • 同时,DispatcherServlet和所有配置的过滤器退出请求处理线程,但是响应保持打开。
  • 应用程序从某个线程设置DeferredResult,Spring MVC将请求发送回Servlet容器。
  • DispatcherServlet再次被调用,处理继续进行,并返回异步产生的值。

Callable 处理的工作方式如下:

  • 控制器返回一个Callable。
  • Spring MVC调用request.startAsync()并将可调用内容提交给TaskExecutor,以便在单独的线程中进行处理。
  • 同时,DispatcherServlet和所有过滤器退出Servlet容器线程,但是响应保持打开。
  • 最后,Callable产生一个结果,Spring MVC将请求发送回Servlet容器以完成处理。
  • DispatcherServlet再次被调用,处理继续进行,并从Callable返回值。

1、异常处理

使用DeferredResult时,可以选择是调用setResult还是带有异常的setErrorResult。在这两种情况下,Spring MVC都将请求发送回Servlet容器以完成处理。然后,它要么被视为控制器方法返回了给定值,要么被视为产生了给定的异常。然后异常通过常规的异常处理机制(例如,调用@ExceptionHandler方法)。

当您使用Callable时,会发生类似的处理逻辑,主要区别在于结果是从Callable返回的,或者是由Callable引发的异常。

2、拦截

HandlerInterceptor实例可以是AsyncHandlerInterceptor类型,以便在启动异步处理的初始请求上接收afterConcurrentHandlingStarted回调(而不是postHandle和afterCompletion)。

HandlerInterceptor实现还可以注册CallableProcessingInterceptor或DeferredResultProcessingInterceptor,以便更深入地与异步请求的生命周期集成(例如,处理超时事件)。请多请了解AsyncHandlerInterceptor。

DeferredResult提供onTimeout(Runnable)和onCompletion(Runnable)回调。有关更多详细信息,请参见DeferredResult的javadoc。Callable可以替代WebAsyncTask,后者公开超时和完成回调的附加方法。

3、与WebFlux的比较

Servlet API最初是为通过Filter-Servlet链进行一次传递而构建的。Servlet 3.0中添加的异步请求处理允许应用程序退出Filter-Servlet链,但保持响应开放以供进一步处理。Spring MVC异步支持就是围绕这一机制构建的。当控制器返回DeferredResult时,Filter-Servlet链退出,Servlet容器线程被释放。稍后,当设置了DeferredResult时,将进行异步调度(到同一个URL),在此期间,控制器将再次映射,但不是调用它,而是使用DeferredResult值(就像控制器返回它一样)来恢复处理。

相比之下,Spring WebFlux既不是建立在Servlet API之上,也不需要这样的异步请求处理特性,因为它在设计上就是异步的。异步处理内置于所有框架契约中,并且在请求处理的所有阶段都得到本质上的支持。

从编程模型的角度来看,Spring MVC和Spring WebFlux都支持异步和反应类型作为控制器方法中的返回值。Spring MVC甚至支持流,包括反作用背压。然而,对响应的单独写入保持阻塞(并且在单独的线程上执行),这与WebFlux不同,web flux依赖于非阻塞I/O,并且每次写入都不需要额外的线程。

另一个基本区别是,Spring MVC不支持控制器方法参数中的异步或反应类型(例如,@RequestBody,@RequestPart等),也不明确支持异步和反应类型作为模型属性。Spring WebFlux确实支持所有这些。

四、HTTP流

对于单个异步返回值,可以使用DeferredResult和Callable。如果您想产生多个异步值并将它们写入响应,该怎么办?

1、响应多个对象

可以使用ResponseBodyEmitter返回值生成对象流,其中每个对象都用HttpMessageConverter序列化并写入响应,如下例所示:

@GetMapping("/events")
public ResponseBodyEmitter handle() {
    ResponseBodyEmitter emitter = new ResponseBodyEmitter();
    // Save the emitter somewhere..
    return emitter;
}

// In some other thread
emitter.send("Hello once");

// and again later on
emitter.send("Hello again");

// and done at some point
emitter.complete();      

还可以使用ResponseBodyEmitter作为ResponseEntity中的主体,从而允许您自定义响应的状态和标头。

当发射器抛出IOException时(例如,如果远程客户端离开了),应用程序不负责清理连接,也不应该调用emitter.complete或emitter.completeWithError。相反,servlet容器会自动启动AsyncListener错误通知,其中Spring MVC会调用completeWithError。这个调用反过来对应用程序执行最后一次异步调度,在此期间,Spring MVC调用配置好的异常解析器并完成请求。

2、SSE

SSE emitter(ResponseBodyEmitter的子类)提供了对服务器发送事件的支持,其中从服务器发送的事件根据W3C SSE规范进行格式化。若要从控制器生成SSE流,请返回SseEmitter,如下例所示:

@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {
    SseEmitter emitter = new SseEmitter();
    // Save the emitter somewhere..
    return emitter;
}

// In some other thread
emitter.send("Hello once");

// and again later on
emitter.send("Hello again");

// and done at some point
emitter.complete();      

虽然SSE是流入浏览器的主要选项,但请注意,Internet Explorer不支持服务器发送的事件。考虑将Spring的WebSocket消息传递与SockJS回退传输(包括SSE)结合使用,以广泛的浏览器为目标。

3、原始数据

有时,绕过消息转换并直接流式传输到响应输出流是有用的(例如,对于文件下载)。您可以使用StreamingResponseBody返回值类型来实现这一点,如下例所示:

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

可以使用StreamingResponseBody作为ResponseEntity中的正文来自定义响应的状态和标头。

五、Reactive类型

Spring MVC支持在控制器中使用反应式客户端库(也可以在WebFlux部分阅读反应式库)。这包括spring-webflux的WebClient和其他的,比如Spring Data reactive数据仓库。在这种情况下,能够从控制器方法返回反应类型是很方便的。

Reactive返回值的处理如下:

  • 单值promise 适用于,类似于使用DeferredResult。示例包括Mono(Reactor)或Single (RxJava)。
  • 与使用ResponseBodyEmitter或SseEmitter类似,具有流媒体类型(如application/x-ndjson或text/event-stream)适用于multi-value多值流。
  • 适应任何其他媒体类型(如application/json)的多值流,类似于使用DeferredResult<List <?>>.

Spring MVC通过来自spring-core的ReactiveAdapterRegistry支持Reactor和RxJava,这让它可以适应多个反应库。

对于到响应的流,支持反应性背压,但是对响应的写入仍然被阻塞,并且通过配置的TaskExecutor在单独的线程上运行,以避免阻塞上游源(例如从WebClient返回的流量)。默认情况下,SimpleAsyncTaskExecutor用于阻塞写操作,但这不适合负载情况。如果您计划使用反应型的流,那么您应该使用MVC配置来配置任务执行器。

六、断开连接

当远程客户端离开时,Servlet API不提供任何通知。因此,在通过SseEmitter或reactive类型流式传输响应时,定期发送数据非常重要,因为如果客户端断开连接,写入将会失败。发送可以采用空(仅注释)SSE事件或任何其他数据的形式,另一端必须将其解释为心跳并忽略。

或者,考虑使用具有内置心跳机制的web消息传递解决方案(如STOMP over WebSocket或带有SockJS的WebSocket)。

七、配置

异步请求处理特性必须在Servlet容器级别启用。MVC配置还为异步请求提供了几个选项。

1、servlet容器

Filter和Servlet声明有一个asyncSupported标志,需要设置为true才能启用异步请求处理。此外,应该声明过滤器映射来处理ASYNC javax.servlet.DispatchType。

在Java配置中,当您使用AbstractAnnotationConfigDispatcherServletInitializer初始化Servlet容器时,这是自动完成的。

在web.xml配置中,可以将<async-supported>true</async-supported>添加到DispatcherServlet和筛选器声明中,并将< dispatcher>ASYNC</dispatcher >添加到筛选器映射中。

2、spring MVC

MVC配置公开了以下与异步请求处理相关的选项:

  • Java配置:使用WebMvcConfigurer上的configureAsyncSupport回调。
  • XML名称空间:使用< mvc:annotation-driven >下的< async-support >元素。

可以配置以下内容:

  • 异步请求的默认超时值,如果没有设置,则取决于底层Servlet容器。
  • AsyncTaskExecutor用于在使用反应类型进行流式处理时阻止写入,以及用于执行从控制器方法返回的Callable 实例。如果您使用反应类型或具有返回Callable的控制器方法,我们强烈建议您配置此属性,因为默认情况下,它是SimpleAsyncTaskExecutor。
  • DeferredResultProcessingInterceptor实现和CallableProcessingInterceptor实现。

参考资料