Spring Boot使用Spring DeferredResult实现长轮询,纵享新丝滑让你体验丝滑般的感觉 - 第414篇
导读
Spring 3.2开始引入了DeferredResult,有助于将长时间运行的计算从http-worker线程卸载到单独的线程。
这一节我们将来一探Spring DeferredResult,看看它究竟是何方神圣。
长轮询系列:
(1)✅《什么是轮询、长轮询、长连接一篇文章让你不在懵懂》
(2)✅《Spring Boot使用Servlet居然也可以实现长轮询》
(3)✅《Spring Boot使用Spring DeferredResult实现长轮询,纵享新丝滑让你体验丝滑般的感觉》
(4)「待拟定」《Spring Boot使用Spring Callable实现长轮询》
(5)「待拟定」…
这一节我们先来看看《Spring Boot使用Spring DeferredResult实现长轮询,纵享新丝滑让你体验丝滑般的感觉》。
一、初识Spring DeferredResult
1.1 DeferredResult是什么?
从Spring 3.2开始引入了DeferredResult,有助于将长时间运行的计算从http-worker线程卸载到单独的线程。
尽管另一个线程将占用一些资源来进行计算,但同时不会阻止工作线程,并且可以处理传入的客户端请求。
DeferredResult:deferred(推迟、延缓)、result(结果),延迟结果,这是一个异步处理类,使用DeferredResult能够实现非阻塞的REST。
1.2 DeferredResult如何使用
使用起来很简单,只需要new一个DeferredResult对象即可,然后进行返回,如下:
客户端请求映射到控制器方法返回值为DeferredResult时,会立即释放Tomcat线程并将请求挂起,直到调用setResult()方法或者超时,才会响应客户端请求。
所以我们可以把复杂的代码另起一个线程进行处理,处理结果使用deferredResult.setResult()进行设置,至此请求相应给前端,请求结束。
二、深入Spring DeferredResult
2.1 例子说明
接下里我们会使用一个小栗子来演示使用DeferredResult的异步处理来实现长轮询。
对于这个例子先总体的说明下:
(1)有一个页面会使用ajax定时的请求后台,5秒一请求,看是否有新的信息发布。
(2)后端接收到请求之后会使用DeferredResult的异步处理请求,如果此时没有新的信息的话,那么等待超时。
(3)打开新的一个窗口,调用发布新的消息的请求发布新消息。
(4)此时ajax定时请求的页面,应该会及时的显示新的信息。
2.2 环境说明
(1)OS:Mac OS
(2)开发工具:IntelliJ Idea
(3)JDK:1.8
(4)Spring Boot:2.6.1
2.3 开发步骤
(1)构建一个基本的Spring Boot框架
(2)构建一个发布请求的Controller
(3)构建一个页面定时请求后台的Controller
(4)启动测试
2.4 开发实战
2.4.1构建一个基本的Spring Boot框架
使用开发工具构建一个基本的Spring Boot项目,这一步没啥好说的,
2.4.2构建一个发布请求的Controller
我们先看下Controller的代码,然后再解释核心部分的代码:
package com.kfit.springbootlongpollingdemo.test;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* Spring DeferredResult
*
* @author 悟纤「公众号SpringBoot」
* @date 2022-01-12
* @slogan 大道至简 悟在天成
*/
@RestController
public class SpringDeferredResultController {
@GetMapping("/handleReqDefResult")
public DeferredResult<String> handleReqDefResult(){
long timeoutValue = 4700;//超时时间.
DeferredResult<String> deferredResult = new DeferredResult<>(timeoutValue);
new Thread(){
@Override
public void run() {
//执行耗时的逻辑
try {
//休眠n秒钟进行模拟业务代码.
TimeUnit.SECONDS.sleep(new Random().nextInt(7));
} catch (InterruptedException e) {
e.printStackTrace();
}
//返回结果.
deferredResult.setResult("love ~ "+new Date());
}
}.start();
return deferredResult;
}
}
说明:
(1)使用DeferredResult定义了一个超时时间。
(2)这里的返回值是DeferredResult类型。
(3)开启了一个线程模拟业务的复杂流程,当处理完成之后使用deferredResult.setResult("love ~ "+new Date()); 返回数据,这里是模拟了一个数据,并不是用户进行设置的,先来个简单版本,待会再使用优化。
3.4.3构建一个页面定时请求后台的Servlet
看下index.html的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>长轮询</title>
</head>
<body>
<b>长轮询小栗子</b>
<div id="message"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script>
$(function () {
function getMessage() {
$.ajax({
// /longPollingServlet
url:"/handleReqDefResult"
,data:{}
,type:"get"
,timeout:5000 //定义超时时间为5秒
,success:function(rs){
if(rs !=''){
$("#message").append("<p>"+rs+"</p>");
}
}
,complete:function(rs){
console.log("重新发起");
getMessage();
}
});
}
getMessage();
});
</script>
</body>
</html>
说明:
(1)使用了jquery的ajax请求后台请求。
(2)对于长轮询前端做了什么呢?其一就是请求返回之后再次发起请求以此hold连接;其二就是定义了一个超时时间timeout,超时之后也会再次发起请求。这里不管是请求成功了还是超时了,jquery的ajax都会执行complete方法。
3.4.4启动测试
启动应用,然后访问地址:
http://127.0.0.1:8080/index
我们可以看到会定时的往控制台进行输出信息。
另外我们可以看到如果超时了,会看到超时的请求:
2.5 开发实战优化
我们发现前面的代码的消息是定时出来的,如果处理耗时的时间的业务逻辑,倒是可以实现,目前咱们这里的需求是需要有一个地方进行发送消息,然后才能进行返回。所以我们需要稍微调整下,这里就有的讲究了:
public static Map<HttpServletRequest,DeferredResult<String>> requestMap = new ConcurrentHashMap<>();
@GetMapping("/handleReqDefResult")
public DeferredResult<String> handleReqDefResult(HttpServletRequest req){
long timeoutValue = 5000;//超时时间.
DeferredResult<String> deferredResult = new DeferredResult<>(timeoutValue);
deferredResult.onTimeout(()->{
//超时移除元素.
requestMap.remove(req);
System.out.println("当前map元素个数:"+requestMap.size());
});
requestMap.put(req,deferredResult);
return deferredResult;
}
说明:
(1)定义一个Map存放DeferredResult,这些类我们可以在别的地方进行使用。
(2)利用DeferredResult的超时方法onTimeout处理超时的DeferredResult。
(3)在实际中,这里的Map里的key可以是用户的id,也可以是订单的id,比如要实现支付支付成功回调通知的业务需求就可以保存订单的id。
那么定义一个请求进行信息变动的发起:
@RequestMapping({"/publishMsg1"})
@ResponseBody
public String publishMsg1(String message){
if(SpringDeferredResultController.requestMap.size()>0){
for(Map.Entry<HttpServletRequest,DeferredResult<String>> entry:SpringDeferredResultController.requestMap.entrySet()){
entry.getValue().setResult(message);
}
}
return "OK";
}
这时候在页面在发起请求:
http://127.0.0.1:8080/index
界面上没有任何信息,需要我们发布一个信息,才能进行显示:
http://127.0.0.1:8080/publishMsg1?message=love1
在查看页面:
悟纤小结
(1)DeferredResult的使用特别简单,声明一个变量,然后返回DeferredResult:
(2)客户端请求映射到控制器方法返回值为DeferredResult时,会立即释放Tomcat线程并将请求挂起,直到调用setResult()方法或者超时,才会响应客户端请求。
购买完整视频,请前往:http://www.mark-to-win.com/TeacherV2.html?id=287