SpringMVC 教程 - 异步请求

Spring MVC 集成了Servlet 3.0的异步请求处理:

  • controller 的方法返回DeferredResultCallable
  • controller 流式处理多个值,包括SSE和原生数据。
  • controller 使用reactive客户端,返回reactive 类型。
DeferredResult

在Servlet容器中启动异步支持之后,controller的方法可以通过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(data);

controller可以通过其他线程异步返回结果,例如:响应时间(例如JMS消息),一个调度任务或者其他类型。

Callable

java.util.Callable也可以包装任何需要异步支持的返回值。例如:

@PostMapping
public Callable<String> processUpload(final MultipartFile file) {

    return new Callable<String>() {
        public String call() throws Exception {
            // ...
            return "someView";
        }
    };

}

返回值由配置的TaskExecutor执行相应任务并返回结果。

处理异步请求
Servlet异步请求的处理过程如下:
  • 通过调用request.startAsync()开始异步处理。调用后Servlet,Filter等可以退出,但是响应开发,直到处理完成返回。
  • 调用request.startAsync()后返回AsyncContext,可以在后续的处理中使用AsyncContext获取各种信息。
  • ServletRequest提供接口访问当前的DispatcherType以便区分初始请求,异步分配,重定向或者其他DispatcherType
DeferredResult处理过程:
  • controller返回一个DeferredResult并且将其保存到内存中的队列或者列表中。
  • Spring MVC调用 request.startAsync()
  • 同时DispatcherServletFileter,退出请求处理线程,响应保持开放。
  • 应用从线程获取值设置DeferredResult,Spring MVC将请求发送回Servlet 容器。
  • 再次调用DispatcherServlet,获取异步返回值,恢复请求处理。
Callable 处理过程:
  • controller 返回一个Callable
  • Spring MVC 调用request.startAsync(),将Callable提交到TaskExecutor处理
  • 同时DispatcherServletFileter,退出请求处理线程,响应保持开放。
  • Callable产生结果,Spring MVC将请求发送回Servlet 容器。
  • 再次调用DispatcherServlet,通过从Callable获取的返回值恢复请求处理。
异常处理

使用DeferredResult可以调用setResult或者setErrorResult来返回结果,调用这两个函数后Spring MVC都会将请求发送回Servlet 容器以完成处理。接着会检查时正常返回还是返回了异常,如果有异常返回就走一般的异常处理流程,例如:调用@ExceptionHandler方法。 使用Callable的处理流程大体相同,主要的区别是又Callable返回结果或者抛出异常。

拦截器

AsyncHandlerInterceptor可以在异步请求处理开始后接收afterConcurrentHandlingStarted的回调代替postHandleafterCompltetionCallableProcessingInterceptor或者DeferredResultProcessingInterceptor深度继承异步处理请求的生命周期,例如超时处理等。 DeferredResult提供了onTimeout(Runnable)onCompletion(Runnable)的回调。详情可以查看JavaDoc。Callable可以取代WebAsyncTask,它提供了超时和完成的回调。

与WebFlux对比

Servlet API之前是为Filter-Servlet请求处理链构建的。在Servlet 3.0 添加了异步处理后,允许应用退出Filter-Servlet请求处理链,只保留响应开放以便日后处理。Spring MVC支持的异步处理就是建立在这项技术之上的。当controller返回一个DeferredResult后,Filter-Servlet处理链退出,Servlet容器的请求线程释放。稍后DeferredResult返回结果,开始一个异步调用,重新映射到controller但是并不在调用controller,使用DeferredResult的返回值继续处理结果。 作为对比Spring WebFlux既没有使用Servlet API也不需要这样的一个异步处理模型,因为它完全是异步设计的。异步处理内置在所有的WebFlux框架中,并且支持异步处理的每一个步骤。 从编程模型来看,Spring MVC和Spring WebFlux都支持异步处理和返回Reactive类型。Spring MVC甚至支持流处理。然而并不想WebFlxu一样使用非阻塞IO,每次写入响应无需单独的线程,SpringMVC单独写入响应仍然是阻塞的。 另一项区别就是Spring MVC不支持异步或者reactive类型作为函数参数。Spring WebFlux支持。

HTTP 流

DeferredResultCallback每次只能异步返回一个值。如果要返回到个值则可以用HTTP 流。

Objects

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,这样就可以定制响应的header和状态了。 emitter抛出IOException异常的时候(例如,远程client关闭),应用并不负责回收连接,也不会调用emitter.complete()或者emitter.completeWithError。相反,Servlet容器会自动初始化一个AsyncListener错误通知,Spring MVC将会调用completeWithError,反过来执行异步分配,应用继续执行正常的异常处理流程。

SSE

SseEmitterResponseBodyEmitter的子类,提供了Server-Send Events的支持。例如:

@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 是将流发送到浏览器的主要方式,但是IE浏览器不支持。如果想要支持更多浏览器,可以使用Spring的SockJS。

原始数据

有时绕过消息转换,直接将流写入到响应的OutputStream更加实用,例如:下载。可以使用StreamingResponseBody作为返回值处理:

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

StreamingResponseBody也可以放入ResponseEntity中来定制响应header和状态。

Reactive 类型

Spring MVC支持在controller使用reactive client 库。包括spring-webflux中的WebClient和Spring Data 中的reactive 数据资源库。在一些场景中,从controller返回reactive类型非常的方便。 Reactive返回处理方式如下:

  • 类似DeferredResult单一值的promise,例如:Reactor的Mono,RxJava的Single。
  • 类似ResponseBodyEmitter或者SseEmitter的多值流(multi-value stream),流的媒体类型是application/stream+json或者text/event-stream。例如,Reactor的Flux,RxJava的Observable。应用可以返回Flux 或者Observable。
  • DeferredResult<List<?>>类似的多值流(multi-value stream)其媒体类型可以是其他类型例如:application/json
断开链接

当远程客户端断开连接时Servlet并不会发送通知。因此当向响应写入流的,不论是通过SseEmitter还是其他的reactive类型,定期发送数据很重要,因为如果客户端断开连接,写入就会失败。可以发送空的SSE事件或者其他类型的数据,客户端可以将其当作心跳处理,或者直接忽略。

配置

异步处理请求必须在Servlet容器级别开启。Spring MVC也提供了一些处理异步请求的参数。

Servlet容器配置

Filter和Servlet有一个asyncSupported的声明,如果要开启异步请求需要设置为true。另外,Filter的映射需要声明处理ASYNC javax.servlet.DispatchType。 使用Java配置:使用AbstractAnnotationConfigDispatcherServletInitializer初始化时自动配置 使用xml配置:对DispatcherServletFilter添加<async-supported>true</async-supported>的声明,同样需要对filter的映射添加<dispatcher>ASYNC</dispatcher>

Spring MVC配置

SpringMVC的配置如下:

- Java配置 - 在`WebMvcConfiger`配置`configureAsyncSupport`回调
- XML配置 - 在`<mvc:annotation-driver>`后配置`<async-support>`

可以配置如下参数:

- 默认的超时事件,默认不设置,依赖于底层Servlet容器(tomcat 10s)
- `AsyncTaskExecutor` 执行reactive 类型和`Callable`返回值处理的线程。默认`SimpleAsyncTaskExecutor`
- `DeferredResultProcessingInterceptor`和`CallableProcessingInterceptor`

注意,默认的超时时间也可以通过DeferredResult,ResponseBodyEmitter,SseEmitter设置,对于Callable可以使用WebAsyncTask设置超时时间。

原文发布于微信公众号 - 代码拾遗(gh_8f61e8bcb1b1)

原文发表时间:2018-04-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java编程技术

你真的了解Netty中@Sharable?

Netty 是一个可以快速开发网络应用程序的基于事件驱动的异步 网络通讯 框架,它大大简化了 TCP 或者 UDP 服务器的网络编程。Netty 的应用还是比较...

17630
来自专栏耕耘实录

Linux(Centos7.4和RHEL7.4)环境下基于chrony的NTP服务器的构建

版权声明:本文为耕耘实录原创文章,各大自媒体平台同步更新。欢迎转载,转载请注明出处,谢谢

14310
来自专栏battcn

一起来学SpringBoot | 第三篇:SpringBoot日志配置

SpringBoot 内部采用的是 CommonsLogging进行日志记录,但在底层为 JavaUtilLogging、 Log4J2、 Logback 等日...

14830
来自专栏软件工程师成长笔记

SSM框架——干净详细的整合学习教程(Spring+SpringMVC+MyBatis)

熟悉MVC的同学都知道,MVC即model(模型)、view(视图)、controller(控制),用一种业务逻辑,数据,界面显示分离的方式使得开发更加的便捷高...

1.6K20
来自专栏吴生的专栏

使用Spring AOP实现MySQL数据库读写分离案例分析

分布式环境下数据库的读写分离策略是解决数据库读写性能瓶颈的一个关键解决方案,更是最大限度了提高了应用中读取 (Read)数据的速度和并发量。

16820
来自专栏Gaussic

使用IntelliJ IDEA开发SpringMVC网站(五)博客文章管理 顶

访问GitHub下载最新源码:https://github.com/gaussic/SpringMVCDemo

29120
来自专栏青青天空树

springboot配置读写分离

  近日工作任务较轻,有空学习学习技术,遂来研究如果实现读写分离。这里用博客记录下过程,一方面可备日后查看,同时也能分享给大家(网上的资料真的大都是抄来抄去,,...

30730
来自专栏Kirito的技术分享

Re:从零开始的Spring Session(二)

上一篇文章介绍了一些Session和Cookie的基础知识,这篇文章开始正式介绍Spring Session是如何对传统的Session进行改造的。官网这么介绍...

39270
来自专栏乐沙弥的世界

Linux 6 下编译安装 PHP 5.6

25420
来自专栏青玉伏案

JavaEE开发之记事本完整案例(SpringBoot + iOS端)

上篇博客我们聊了《JavaEE开发之SpringBoot整合MyBatis以及Thymeleaf模板引擎》,并且在之前我们也聊了《Swift3.0服务端开发(五...

24050

扫码关注云+社区

领取腾讯云代金券