SpringMVC异步请求
背景
Tomcat等应用服务器的连接线程池实际上是有限制的;每一个连接请求都会耗掉线程池的一个连接数;如果某些耗时很长的操作,如对大量数据的查询操作、调用外部系统提供的服务以及一些 IO 密集型操作等,会占用连接很长时间,这个时候这个连接就无法被释放而被其它请求重用。如果连接占用过多,服务器就很可能无法及时响应每个请求;极端情况下如果将线程池中的所有连接耗尽,服务器将长时间无法向外提供服务
在常规场景中,客户端需要等待服务器处理完毕后返回才能继续进行其它操作,这个场景下每一步都是同步调用,如客户端调用 Servlet 后需要等待其处理返回,Servlet 调用具体的 Controller 后也需要等待其返回。这种情况是在服务器端开发中最常见的场景,适合于服务器端处理时间不是很长的情况;默认情况下 Spring 的 Controller 提供的就是这样的服务
当某项服务处理时间过长时,如邮件发送,需要调用到外部接口,处理时间不受调用方的控制,因此如果耗时过长会有两个比较严重的后果:一是如上文所说的会长时间的占用请求连接数,严重时有可能导致服务器失去响应; 二是客户端等待时间过长,导致前端应用的用户友好性下降,而且客户很有可能因为长时间得不到服务器响应而重复操作,从而加重服务器的负担,应用崩溃的机率变大!
为应对这种场景,一般会启用一个后台的线程池,处理请求的 Controller 会先提交一个耗时长操作如邮件发送到线程池中,然后立即返回到前台。因此处理响应的主线程耗时变短,客户感受到的就是在点击某个发送按钮后很快就得到服务器反馈结果,然后就放心的继续处理其它工作。实际上邮件发送这种事情延迟几秒对于客户来说根本感受不到。当然应用需要保证提交到线程池中的任务执行成功,或者是执行失败后在前端某个地方能够看到失败的具体情况
这种场景在 Spring 中可使用 TaskExecutor 或者是 Async 来处理,关于它们的用法请参考:Spring 基础学习-任务执行(TaskExecutor及Async)
通过以上两种场景,很容易就会想到,如果某个操作既耗时很长,客户端又必须要等待其返回才能进一步处理时,应该通过什么方式来处理?Servlet3.0 中引入异步请求处理来处理这种场景,相应的,Spring在3.2 版本中就引入相关机制来使用Servlet的该特性
SpringMVC 异步处理概述
为满足耗时任务占用应用服务器连接数,而客户端又必须等待这些耗时长任务返回才能处理下一步工作的场景,Spring 引入了以下机制来处理:
使用 Callable 或者 DeferredResult 作为 Controller 的返回值,能够处理异步返回单个结果的场景;使用 ResponseBodyEmitter/SseEmitter 或者StreamingResponseBody 来流式处理多个返回值;在 Controller 中使用响应式客户端调用服务并返回响应式的数据对象
Callable
Callable 直接使用在 Controller 中被 RequestMapping 所注解的方法上,做为其返回对象
使用示例
@RequestMapping("/testCallable")
public Callable < String > testCallable() {logger.info("Controller开始执行!");Callable < String > callable = () - > {Thread.sleep(5000);logger.info("实际工作执行完成!");return "succeed!";};logger.info("Controller执行结束!");return callable;
}
可以看到以下结果:
浏览器等待了大约5秒后返回结果
打印日志中,Controller 在 6ms 就执行结束
打印日志中,实际的任务执行在一个名称为 MvcAsync1 的线程中执行,并且在 Controller 执行完5s后才执行结束
因此可以得到结论:
返回 Callable 对象时,实际工作线程会在后台处理,Controller 无需等待工作线程处理完成,但 Spring 会在工作线程处理完毕后才返回客户端
它的执行流程是这样的
客户端请求服务
SpringMVC 调用 Controller,Controller 返回一个 Callback 对象
SpringMVC 调用 request.startAsync 并且将Callback提交到 TaskExecutor 中去执行
DispatcherServlet 以及 Filters 等从应用服务器线程中结束,但 Response 仍旧是打开状态,也就是说暂时还不返回给客户端
TaskExecutor 调用 Callback 返回一个结果,SpringMVC 将请求发送给应用服务器继续处理
DispatcherServlet 再次被调用并且继续处理 Callback 返回的对象,最终将其返回给客户端
DeferredResult
DeferredResult 使用方式与 Callable 类似,但在返回结果上不一样,它返回的时候实际结果可能没有生成,实际的结果可能会在另外的线程里面设置到 DeferredResult 中去
该类包含以下日常使用相关的特性:
超时配置:通过构造函数可以传入超时时间,单位为毫秒;因为需要等待设置结果后才能继续处理并返回客户端,如果一直等待会导致客户端一直无响应,因此必须有相应的超时机制来避免这个问题;就算不设置这个超时时间,应用服务器或者 Spring 也会有一些默认的超时机制来处理这个问题
结果设置:它的结果存储在一个名称为result的属性中;可以通过调用 setResult 的方法来设置属性;由于这个 DeferredResult 天生就是使用在多线程环境中的,因此对这个result属性的读写是有加锁的
接下来将对DeferredResult的处理流程进行说明,并实现一个较为简单的示例
DeferredResult 处理流程
DeferredResult 的处理过程与Callback类似,不一样的地方在于它的结果不是 DeferredResult 直接返回的,而是由其它线程通过同步的方式设置到该对象中,它的执行过程如下所示:
客户端请求服务
SpringMVC 调用 Controller,Controller 返回一个 DeferredResult 对象
SpringMVC 调用 request.startAsync
DispatcherServlet 以及 Filters 等从应用服务器线程中结束,但 Response 仍旧是打开状态,也就是说暂时还不返回给客户端
某些其它线程将结果设置到 DeferredResult 中,SpringMVC 将请求发送给应用服务器继续处理
DispatcherServlet 再次被调用并且继续处理 DeferredResult 中的结果,最终将其返回给客户端
DeferredResult 使用示例
本示例将在一个 Controller 中添加两个 RequestMapping 注解的方法,其中一个返回的是 DeferredResult 的对象,另外一个设置这个对象的值
注意:每个请求都要返回一个 新的 DeferredResult 对象,不能设置成单例,不能多个方法公用一个 DeferredResult
@RestController
@RequestMapping("/test")
public class TestController {private DeferredResult<String> deferredResult;/*** 返回DeferredResult对象*/@RequestMapping("/getDeferredResult")public DeferredResult <String> testDeferredResult() {deferredResult = new DeferredResult < > (5000 L, "请求失败(超时)"); // 设置 5 秒超时return deferredResult;}/*** 对DeferredResult的结果进行设置*/@RequestMapping("/setDeferredResult")public String setDeferredResult() {deferredResult.setResult("success");return "succeed";}
}
第一步先访问:http://localhost/test/getDeferredResult 此时客户端将会一直等待,5秒后会返回 “请求失败(超时)” 提醒
第二步新开页面访问:http://localhost/test/setDeferredResult 此时第一个页面会返回结果(第一个页面还没有返回超时错误的情况下)
上面只是演示了如何使用 DeferredResult 实现异步方法,我们发现,对于 /getDeferredResult 这个请求,每次都返回了新的 DeferredResult 对象,那么如何确保多个请求的时候,/setDeferredResult 能设值到对应的 DeferredResult?在实际开发中,需要对 DeferredResult 进行封装,一个简单的实现方式是把每次创建的新的 DeferredResult 放到 Queue(队列) 中,队列具有从尾部添加新元素,从头部获取新元素的特性,正好可以确保DeferredResult 的使用顺序,即先创建的先使用,后创建的后使用;参考示例代码如下
DeferredResult 封工具类
public class DeferredResultQueue {// 创建一个线程安全的链式队列private static Queue < DeferredResult > queue = new ConcurrentLinkedDeque < > ();// 添加 DeferredResult 方法, 从尾部添加public static void set(DeferredResult < Object > deferredResult) {queue.add(deferredResult);}// 获取并删除队列第一个 DeferredResult 对象public static DeferredResult < Object > get() {return queue.poll();}
}
所以上面的示例代码可修改为
@RestController
@RequestMapping("/test")
public class TestController {/*** 返回DeferredResult对象*/@RequestMapping("/getDeferredResult")public DeferredResult < String > testDeferredResult() {DeferredResult < String > deferredResult = new DeferredResult < > (5000 L, "请求失败(超时)"); // 设置 5 秒超时DefferredResultQueue.set(deferredResult); // 把 new 出来的 DeferredResult 对象放到工具类队列容器中return deferredResult;}/*** 对DeferredResult的结果进行设置*/@RequestMapping("/setDeferredResult")public String setDeferredResult() {DeferredResult < String > deferredResult = DefferredResultQueue.get(); // 获取到对应顺序的 DeferredResult 对象deferredResult.setResult("success");return "succeed";}
}
SseEmitter
Callback 和 DeferredResult 用于设置单个结果,如果有多个结果需要返回给客户端时,可以使用 SseEmitter 以及 ResponseBodyEmitter 等;
下面直接看示例,与 DeferredResult 的示例类似:
@RestController
@RequestMapping("/test")
public class TestController {private static final Logger logger = LoggerFactory.getLogger(TestController.class);private SseEmitter sseEmitter;/*** 返回SseEmitter对象*/@RequestMapping("/testSseEmitter")public SseEmitter testSseEmitter() {sseEmitter = new SseEmitter(10000 L); // 设置 10 秒超时return sseEmitter;}/*** 向SseEmitter对象发送数据*/@RequestMapping("/setSseEmitter")public String setSseEmitter() {try {sseEmitter.send(System.currentTimeMillis());} catch (IOException e) {logger.error("IOException!", e);return "error";}return "Succeed!";}/*** 将SseEmitter对象设置成完成*/@RequestMapping("/completeSseEmitter")public String completeSseEmitter() {sseEmitter.complete();return "Succeed!";}
}
第一步访问:http://localhost/test/testSseEmitter
第二步连续访问:http://localhost/test/setSseEmitter,第一步会不停返回第二步设置的值
第三步访问:http://localhost/test/completeSseEmitter
可以看到结果,只有当第三步执行后,第一步的访问才算结束
注意
(1)如果第一步请求时间已经到了,第二步没有设值,那么第一步所在请求会抛出异常
(2)如果第一步请求时间已经到了,第二步还在继续设值,那么第二步所在请求会抛出 ResponseBodyEmitter is already set complete 异常
(3)如果第三步执行,表示 sseEmitter 已被关闭,第二步执行就会抛出 ResponseBodyEmitter is already set complete 异常
StreamingResponseBody
用于直接将结果写出到 Response 的 OutputStream 中; 如文件下载等(可尝试文件下载进度实现),优点是不用考虑什么时候关闭流(实际上也根本无法确认什么时候操作完成)示例
@GetMapping("/download")
public StreamingResponseBody handle() {return new StreamingResponseBody() {@Overridepublic void writeTo(OutputStream outputStream) throws IOException {// write...}}
}
异步处理拦截器
在进行异步处理时,可以使用 CallableProcessingInterceptor 来对 Callback返回参数的情况进行拦截,也可以使用 DeferredResultProcessingInterceptor 来对 DeferredResult 的情况进行拦截。 也可以直接使用 AsyncHandlerInterceptor
拦截器的使用与普通拦截器一样,因此此处不再展开;具体可以参考:Spring Boot 拦截器示例及源码原理分析
注意:在使用异步机制前,要在 Web 配置,向 Servlet 添加对异步方法的支持
// 相当于配置 web.xml
public class WebInitializer implements WebApplicationInitializer {public void onStartup(ServletContext servletContext) throws ServletException {AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();ctx.register(SpringConfig.class);ctx.setServletContext(servletContext);Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(ctx));servlet.addMapping("/");servlet.setLoadOnStartup(1);//开启异步方法支持servlet.setAsyncSupported(true);}
}
相关文章:
SpringMVC异步请求
背景 Tomcat等应用服务器的连接线程池实际上是有限制的;每一个连接请求都会耗掉线程池的一个连接数;如果某些耗时很长的操作,如对大量数据的查询操作、调用外部系统提供的服务以及一些 IO 密集型操作等,会占用连接很长时间&#…...

这七个100%提高Python代码性能的技巧,一定要知道
B站|公众号:啥都会一点的研究生 相关阅读 整理了几个100%会踩的Python细节坑,提前防止脑血栓 整理了十个100%提高效率的Python编程技巧,更上一层楼 Python-列表,从基础到进阶用法大总结,进来查漏补缺 Python-元组&…...

计算机网络笔记、面试八股(五)—— 浏览器输入URL
本章目录5. 从输入URL到浏览器显示页面过程中都发生了什么5.1 URL输入5.2 DNS解析5.2.1 域名的等级5.2.2 DNS解析的流程5.2.3 DNS查询方式5.3 建立TCP连接5.4 发送HTTP/HTTPS请求5.5 服务器处理请求并返回HTTP响应5.6 浏览器解析渲染页面5.7 HTTP请求结束,断开TCP连…...
【速记】快速调通算法项目的环境
1.创建新的conda环境,避免把原有的环境给搞坏。 在CMD中执行,而不是在anaconda的命令行中执行: conda create -n 环境名 --offline python3.8 2.在pycharm中配置conda环境: setting->Project Interpreter->齿轮->add-&g…...
开放开源开先河(上)
目录 1.唯一性定义品牌 2.打造爆款塑造品牌 3.构筑生态体系传播品牌 2022年7月28日,以“软件定义世界 开源共筑未来”为主题的全球数字经济大会开放原子开源峰会在北京开幕,承办主峰会和为捐赠人进行授牌仪式的开放原子开源基金会再次进入公众视野。基金…...

TencentOS 3.1安装MySQL 8.0.32
到官网下载安装包:https://dev.mysql.com/downloads/mysql/ 使用如下命令解包。 tar xf mysql-8.0.32-1.el8.x86_64.rpm-bundle.tar 使用rpm -qa |grep mysql 和rpm -qa |grep mariadb检查是否安装过mysql 如果有,使用下命令移除: rpm -e …...

Javascript的API基本内容(五)
一、js组成 JavaScript的组成 ECMAScript: 规定了js基础语法核心知识。 比如:变量、分支语句、循环语句、对象等等 Web APIs : DOM 文档对象模型, 定义了一套操作HTML文档的API BOM 浏览器对象模型,定义了一套操作浏览器窗口的API 二、loc…...

分层测试(2)单元测试【必备】
1. 什么是单元测试? 对代码中的逻辑隔离的最小代码片段进行测试,验证其逻辑是否符合预期,单元可以是函数,方法,类,功能模块。 2. 单元测试的优点 掌握代码:单元测试允许开发人员了解单元提供…...

代码随想录算法训练营day45 |动态规划之背包问题 70. 爬楼梯 (进阶) 322. 零钱兑换 279.完全平方数
day4570. 爬楼梯 (进阶)1. 确定dp数组以及下标的含义2.确定递推公式3.dp数组如何初始化4.确定遍历顺序5.举例来推导dp数组322. 零钱兑换1. 确定dp数组以及下标的含义2.确定递推公式3.dp数组如何初始化4.确定遍历顺序5.举例推导dp数组279.完全平方数1. 确…...

秒懂算法 | 基于图神经网络的推荐算法
图神经网络(Graph Neural Networks,GNN)是近几年兴起的学科,用来作推荐算法自然效果也相当好,但是要学会基于图神经网络的推荐算法之前,需要对图神经网络自身有个了解。 图卷积网络(Graph Convolutional Networks,GCN)提出于2017年。GCN 的出现标志着图神经网络的出现。深度学习…...

CANoe TC8测试脚本的结构介绍
CANoe TC8脚本是通过vTESTstudio平台编写。每个协议(ARP\ICMPv4\IPv4\UDP\TCP\SOMEIP\DHCP)都有自己的vtt文件。每个vtt文件的测试树结构为: Test Fixture Fixture Preparation Test Case Test Case … Test Case Test Case Fixture Completion 当Test Fixture里的Test Case…...

DP(4)--区间DP
将n(1≤n≤200)堆石子绕圆形操场摆放,现要将石子有次序地合并成一堆。 规定每次只能选相邻的两堆石子合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。 (1)选择一种合并石子的方案,使得做n-1次合并,得分的总…...

【C语言】“qsort函数详解”与“使用冒泡思想模拟使用qsort”
✨✨✨✨如果文章对你有帮助记得点赞收藏关注哦!!✨✨✨✨ 文章目录✨✨✨✨如果文章对你有帮助记得点赞收藏关注哦!!✨✨✨✨qsort的介绍:一、qsort函数的使用✨比较int类型数据比较字符型数据比较结构体数据冒泡思想…...

接口自动化框架---升级版(Pytest+request+Allure)
目录:导读 一、简单介绍 二、目录介绍 三、代码分析 写在最后 接口自动化是指模拟程序接口层面的自动化,由于接口不易变更,维护成本更小,所以深受各大公司的喜爱。 第一版入口:接口自动化框架(PytestrequestAllure…...

C语言循环语句简述
C 循环 有的时候,我们可能需要多次执行同一块代码。一般情况下,语句是按顺序执行的:函数中的第一个语句先执行,接着是第二个语句,依此类推。 编程语言提供了更为复杂执行路径的多种控制结构。 循环语句允许我们多次…...

STM32开发(16)----CubeMX配置DMA
CubeMX配置DMA前言一、什么是DMA?二、实验过程1.CubeMX配置2.代码实现3.实验结果总结前言 本章介绍使用STM32CubeMX对DMA进行配置的方法,DMA的原理、概念和特点,配置各个步骤的功能,并通过串口DMA传输实验方式验证。 一、什么是…...

让物流园区可视可控,顺丰供应链与亚马逊云科技的供应链新解法
导读:物流园区如何破解供应链断点?在物流园区附近,我们经常看到周边道路停满了集装箱卡车。这是物流园区的一个典型痛点,由于园区内部业务情况的不可见性,司机们往往到了园区才被告知业务繁忙,需要长时间排…...

2023年3月北京/西安/广州/深圳DAMA-CDGA/CDGP数据治理认证报名
DAMA认证为数据管理专业人士提供职业目标晋升规划,彰显了职业发展里程碑及发展阶梯定义,帮助数据管理从业人士获得企业数字化转型战略下的必备职业能力,促进开展工作实践应用及实际问题解决,形成企业所需的新数字经济下的核心职业…...

「TCG 规范解读」TCG 主规范-设计原则
可信计算组织(Ttrusted Computing Group,TCG)是一个非盈利的工业标准组织,它的宗旨是加强在相异计算机平台上的计算环境的安全性。TCG于2003年春成立,并采纳了由可信计算平台联盟(the Trusted Computing Platform Alliance,TCPA)所开发的规范。现在的规范都不是最终稿,都…...

【Spring源码】Spring AOP的核心概念
废话版什么是AOP关于什么是AOP,这里还是要简单介绍下AOP,Aspect Oriented Programming,面向切面编程,通过预编译和运行期间提供动态代理的方式实现程序功能的统一维护,使用AOP可以降低各个部分的耦合度,提高…...

《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)
CSI-2 协议详细解析 (一) 1. CSI-2层定义(CSI-2 Layer Definitions) 分层结构 :CSI-2协议分为6层: 物理层(PHY Layer) : 定义电气特性、时钟机制和传输介质(导线&#…...
可靠性+灵活性:电力载波技术在楼宇自控中的核心价值
可靠性灵活性:电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中,电力载波技术(PLC)凭借其独特的优势,正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据,无需额外布…...

[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...

微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
06 Deep learning神经网络编程基础 激活函数 --吴恩达
深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...

SAP学习笔记 - 开发26 - 前端Fiori开发 OData V2 和 V4 的差异 (Deepseek整理)
上一章用到了V2 的概念,其实 Fiori当中还有 V4,咱们这一章来总结一下 V2 和 V4。 SAP学习笔记 - 开发25 - 前端Fiori开发 Remote OData Service(使用远端Odata服务),代理中间件(ui5-middleware-simpleproxy)-CSDN博客…...
08. C#入门系列【类的基本概念】:开启编程世界的奇妙冒险
C#入门系列【类的基本概念】:开启编程世界的奇妙冒险 嘿,各位编程小白探险家!欢迎来到 C# 的奇幻大陆!今天咱们要深入探索这片大陆上至关重要的 “建筑”—— 类!别害怕,跟着我,保准让你轻松搞…...
Go语言多线程问题
打印零与奇偶数(leetcode 1116) 方法1:使用互斥锁和条件变量 package mainimport ("fmt""sync" )type ZeroEvenOdd struct {n intzeroMutex sync.MutexevenMutex sync.MutexoddMutex sync.Mutexcurrent int…...
《Offer来了:Java面试核心知识点精讲》大纲
文章目录 一、《Offer来了:Java面试核心知识点精讲》的典型大纲框架Java基础并发编程JVM原理数据库与缓存分布式架构系统设计二、《Offer来了:Java面试核心知识点精讲(原理篇)》技术文章大纲核心主题:Java基础原理与面试高频考点Java虚拟机(JVM)原理Java并发编程原理Jav…...

初探用uniapp写微信小程序遇到的问题及解决(vue3+ts)
零、关于开发思路 (一)拿到工作任务,先理清楚需求 1.逻辑部分 不放过原型里说的每一句话,有疑惑的部分该问产品/测试/之前的开发就问 2.页面部分(含国际化) 整体看过需要开发页面的原型后,分类一下哪些组件/样式可以复用,直接提取出来使用 (时间充分的前提下,不…...