Spring MVC的异步模式(ResponseBodyEmitter、SseEmitter、StreamingResponseBody) 高级使用篇
DeferredResult
高级使用
上篇博文介绍的它的基本使用,那么本文主要结合一些特殊的使用场景,来介绍下它的高级使用,让能更深刻的理解DeferredResult
的强大之处。
它的优点也是非常明显的,能够实现两个完全不相干的线程间的通信。
处理的时候请注意图中标记的线程安全问题~~~

实现长轮询服务端推送消息(long polling)
简单科普双向通信的方式
在WebSocket
协议之前(它是2011年发布的),有三种实现双向通信的方式:轮询(polling)、长轮询(long-polling)和iframe流(streaming)。
- 轮询(polling):这个不解释了。优点是实现简单粗暴,后台处理简单。缺点也是大大的,耗流量、耗CPU。。。
- 长轮询(long-polling):长轮询是对轮询的改进版。客户端发送HTTP给服务器之后,看有没有新消息,如果没有新消息,就一直等待(而不是一直去请求了)。当有新消息的时候,才会返回给客户端。 优点是对轮询做了优化,时效性也较好。缺点是:保持连接会消耗资源; 服务器没有返回有效数据,程序超时~~~
- iframe流(streaming):是在页面中插入一个
隐藏的iframe
,利用其src属性在服务器和客户端之间创建一条长连接,服务器向iframe传输数据(通常是HTML,内有负责插入信息的javascript),来实时更新页面。(个人觉得还不如长轮询呢。。。) - WebSocket:WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。它将TCP的Socket(套接字)应用在了webpage上。 它的有点一大把:支持双向通信,实时性更强;可发送二进制文件;非常节省流量。 但也是有缺点的:
浏览器支持程度不一致
,不支持断开重连 (其实是最推荐的~~~)
之前看apollo配置中心
的实现原理,apollo的发布配置推送变更消息就是用DeferredResult
实现的。它的大概实现步骤如下:
- apollo客户端会像服务端发送
长轮询http请求
,超时时间60秒 - 当超时后返回客户端一个304 httpstatus,表明配置没有变更,客户端
继续这个步骤重复发起请求
- 当有发布配置的时候,服务端会调用
DeferredResult.setResult
返回200状态码。客户端收到响应结果后,会发起请求获取变更后的配置信息(注意这里是另外一个请求哦~)。
为了演示,简单的按照此方式,写一个Demo:
@Configuration
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer { @Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
// 超时时间设置为60s
configurer.setDefaultTimeout(TimeUnit.SECONDS.toMillis(60));
}
}
服务端简单代码模拟如下:
@Slf4j
@RestController
public class ApolloController { // 值为List,因为监视同一个名称空间的长轮询可能有N个(毕竟可能有多个客户端用同一份配置嘛)
private Map<String, List<DeferredResult<String>>> watchRequests = new ConcurrentHashMap<>(); @GetMapping(value = "/all/watchrequests")
public Object getWatchRequests() {
return watchRequests;
} // 模拟长轮询:apollo客户端来监听配置文件的变更~ 可以指定namespace 监视指定的NameSpace
@GetMapping(value = "/watch/{namespace}")
public DeferredResult<String> watch(@PathVariable("namespace") String namespace) {
log.info("Request received,namespace is" + namespace + ",当前时间:" + System.currentTimeMillis()); DeferredResult<String> deferredResult = new DeferredResult<>(); //当deferredResult完成时(不论是超时还是异常还是正常完成),都应该移除watchRequests中相应的watch key
deferredResult.onCompletion(() -> {
log.info("onCompletion,移除对namespace:" + namespace + "的监视~");
List<DeferredResult<String>> list = watchRequests.get(namespace);
list.remove(deferredResult);
if (list.isEmpty()) {
watchRequests.remove(namespace);
}
}); List<DeferredResult<String>> list = watchRequests.computeIfAbsent(namespace, (k) -> new ArrayList<>());
list.add(deferredResult);
return deferredResult; } //模拟发布namespace配置:修改配置
@GetMapping(value = "/publish/{namespace}")
public void publishConfig(@PathVariable("namespace") String namespace) {
//do Something for update config if (watchRequests.containsKey(namespace)) {
List<DeferredResult<String>> deferredResults = watchRequests.get(namespace); //通知所有watch这个namespace变更的长轮训配置变更结果
for (DeferredResult<String> deferredResult : deferredResults) {
deferredResult.setResult(namespace + " changed,时间为" + System.currentTimeMillis());
}
} }
}
apollo处理超时时候会抛出一个异常AsyncRequestTimeoutException
,因此我们全局处理一下就成:
@Slf4j
@ControllerAdvice
class GlobalControllerExceptionHandler { @ResponseStatus(HttpStatus.NOT_MODIFIED)//返回304状态码 效果同HttpServletResponse#sendError(int) 但这样更优雅
@ResponseBody
@ExceptionHandler(AsyncRequestTimeoutException.class) //捕获特定异常
public void handleAsyncRequestTimeoutException(AsyncRequestTimeoutException e) {
System.out.println("handleAsyncRequestTimeoutException");
}
}
用Ajax
模拟Client端的伪代码如下:
//长轮询:一直去监听指定namespace的配置文件
function watchConfig(){
$.ajax({
url:"http://localhost:8080/demo_war/watch/classroomconfig",
method:"get",
success:function(response,status){
if(status == 304){
watchConfig(); //超时,没有更改,那就继续去监听
}else if(status == 200){
getNewConfig(); //监听到更改后,立马去获取最新的配置文件内容回来做事
... watchConfig(); // 昨晚事后又去监听着
}
} });
} // 调用去监听获取配置文件的函数
watchConfig();
这样子我们就基本模拟了一个长轮询
的案例~
长轮询的应用场景也是很多的,比如我们现在要实现这样一个功能:浏览器要实时展示服务端计算出来的数据。(这个用普通轮询就会有延迟且浪费资源,但是用这种类似长连接
的方案就很合适)
ResponseBodyEmitter和SseEmitter
Callback
和DeferredResult
用于设置单个结果,如果有多个结果需要set返回给客户端时,可以使用SseEmitter以及ResponseBodyEmitter
,each object is written with a compatible HttpMessageConverter
。返回值可以直接写他们本身,也可以放在ResponseEntity
里面
它俩都是Spring4.2之后提供的类。由
ResponseBodyEmitterReturnValueHandler
负责处理。 这个和Spring5提供的webFlux技术已经很像了,后续讲到的时候还会提到他们~~~~ Emitter:发射器
它们的使用方式几乎同:DeferredResult
,这里我只把官方的例子拿出来你就懂了

SseEmitter
是ResponseBodyEmitter
的子类,它提供Server-Sent Events(Sse)
.服务器事件发送是”HTTP Streaming”的另一个变种技术.只是从服务器发送的事件按照W3C Server-Sent Events
规范来的(推荐使用) 它的使用方式上,完全同上
Server-Sent Events
这个规范能够来用于它们的预期使用目的:就是从server发送events到clients(服务器推).在Spring MVC中可以很容易的实现.仅仅需要返回一个SseEmitter
类型的值.
向这种场景在在线游戏、在线协作、金融领域等等都有很好的应用。当然,如果你对稳定性什么的要求都非常高,官方也推荐最好是使用
WebSocket
来实现~
ResponseBodyEmitter
允许通过HttpMessageConverter
把发送的events写到对象到response中.这可能是最常见的情况。例如写JSON数据 可是有时候它被用来绕开message转换直接写入到response的OutputStream。例如文件下载.这样可以通过返回StreamingResponseBody
类型的值做到.
StreamingResponseBody (很方便的文件下载)
它用于直接将结果写出到Response的OutputStream
中; 如文件下载等

接口源码非常简单:
@FunctionalInterface
public interface StreamingResponseBody {
void writeTo(OutputStream outputStream) throws IOException;
}
异步优化
Spring内部默认不使用线程池处理的(通过源码分析后面我们是能看到的),为了提高处理的效率,我们可以自己优化,建议自己在配置里注入一个线程池供给使用,参考如下:
// 提供一个mvc里专用的线程池。。。 这是全局的方式~~~~
@Bean
public ThreadPoolTaskExecutor mvcTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setQueueCapacity(100);
executor.setMaxPoolSize(25);
return executor;
} // 最优解决方案不是像上面一样配置通用的,而是配置一个单独的专用的,如下~~~~
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter { // 配置异步支持~~~~
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
// 设置一个用于异步执行的执行器~~~AsyncTaskExecutor
configurer.setTaskExecutor(mvcTaskExecutor());
configurer.setDefaultTimeout(60000L);
}
}
总结
总的来说,Spring MVC提供的便捷的异步支持,能够大大的提高Tomcat容器等的性能。同时也给我们的应用提供了更多的便利。这也为Spring5以后的Reactive编程模型提供了有利的支持和保障。 Spring是一个易学难精的技术,想要把各种技术融汇贯通,还有后续更扎实的深挖~
Spring MVC的异步模式(ResponseBodyEmitter、SseEmitter、StreamingResponseBody) 高级使用篇的更多相关文章
- Spring MVC的异步模式
高性能的关键:Spring MVC的异步模式 我承认有些标题党了,不过话说这样其实也没错,关于“异步”处理的文章已经不少,代码例子也能找到很多,但我还是打算发表这篇我写了好长一段时间,却一直没发表 ...
- 高性能的关键:Spring MVC的异步模式
我承认有些标题党了,不过话说这样其实也没错,关于“异步”处理的文章已经不少,代码例子也能找到很多,但我还是打算发表这篇我写了好长一段时间,却一直没发表的文章,以一个更简单的视角,把异步模式讲清楚. 什 ...
- Spring MVC的异步模式DefferedResult
原文:http://www.importnew.com/21051.html 什么是异步模式 要知道什么是异步模式,就先要知道什么是同步模式,先看最典型的同步模式: (图1) 浏览器发起请求,Web服 ...
- MVC的异步模式
[小家Spring]高性能关键技术之---体验Spring MVC的异步模式(Callable.WebAsyncTask.DeferredResult) 基础使用篇 https://blog.csdn ...
- spring mvc ajax异步文件的上传和普通文件上传
表单提交方式文件上传和ajax异步文件上传 一:首先是我在spring mvc下的表单提交方式上传 ssm的包配置我就不一一详细列出来了,但是上传的包我还是列出来 这一段我也不知道怎么给大家讲解就是直 ...
- spring mvc对异步请求的处理
在spring mvc3.2及以上版本增加了对请求的异步处理,是在servlet3的基础上进行封装的. 1.修改web.xml <?xml version="1.0" enc ...
- hibernate+spring+mvc+Easyui框架模式下使用grid++report的总结
最近刚开始接触hibernate+spring+mvc+Easyui框架,也是刚开通了博客,希望能记录一下自己实践出来的东西,让其他人少走弯路. 转让正题,以个人浅薄的认识hibernate对于开发人 ...
- Spring MVC 异步测试
从spring3.2开始,支持servlet3的异步请求,这对于处理耗时的请求如缓慢的数据库查询是非常有好处的,不至于很快的耗光servlet的线程池,影响可扩展性. 让我们先来了解一下servlet ...
- Spring MVC 异步处理请求,提高程序性能
原文:http://blog.csdn.net/he90227/article/details/52262163 什么是异步模式 如何在Spring MVC中使用异步提高性能? 一个普通 Servle ...
随机推荐
- BZOJ 4522: [Cqoi2016]密钥破解 exgcd+Pollard-Rho
挺简单的,正好能再复习一遍 $exgcd$~ 按照题意一遍一遍模拟即可,注意一下 $pollard-rho$ 中的细节. #include <ctime> #include <cma ...
- MessagePack Java Jackson Dataformat 不使用 str8 数据类型的序列化
老的 msgpack-java(例如 0.6.7)并不支持 MessagePack str8 数据类型. 当你的希望的你的应用程序需要支持老的版本的话,你需要禁用这个数据类型,例如使用下面的语句: M ...
- codevs 3022 西天收费站 x
题目描述 Description 唐僧师徒四人终于发现西天就在眼前,但猴子突然发现前面有n个收费站(如来佛太可恶),在每个收费站用不同的方式要交的钱不同,输入 ...
- Ubuntu安装jdk10
一:去官网下载jdk,和jre 因为jdk10之后jdk和jre是分开的 jdk下载 jre下载 二:解压缩,并放到指定目录 # 创建目录 sudo mkdir /usr/lib/java ...
- 25.Python逻辑运算符及其用法
逻辑运算符是对真和假两种布尔值进行运算(操作 bool 类型的变量.常量或表达式),逻辑运算的返回值也是 bool 类型值. Python 中的逻辑运算符主要包括 and(逻辑与).or(逻辑或)以及 ...
- JETSON TK1 ~ 安装Cuda和OpenCV3
一:安装Cuda6.5 1:下载安装包 Cuda6.5 2.在TK1上安装软件包: cd ~/Downloads sudo dpkg -i cuda-repo-l4t-r21.3-6-5-prod_6 ...
- JAVA常见工具配置
1.MyEclipse中配备struts.xml的自动提示 https://jingyan.baidu.com/article/9158e0004054baa2541228e2.html 2.MySQ ...
- 学号 20175329 《Java程序设计》第10周学习总结
20175329 <Java程序设计>第十周学习总结 教材学习内容总结 线程与进程 进程时程序的一次动态执行过程.线程是比进程更小的执行单位,一个进程在其执行过程中,可以产生多个线程. J ...
- excel怎么只打印某页?excel怎么只打印某几页
有时候我们需要打印的excel文件,内容较多有好几页,而我们只需要打印里面的部分内容,为了减少纸张.碳粉的浪费,我们怎样精准打印某页或某几页呢? 工具/原料 Excel / WPS软件 方法/ ...
- 理解JVM
1.JVM运行时数据区 2.方法区 方法区垃圾回收的条件:该类的所有实例(堆内存中)被回收:加载该类字节码的类加载器被回收:所有的类对象(如Student.class)的引用被回收 一般采用可达性分析 ...