DeferredResult——非同步請求處理
本文地址: https://www.jianshu.com/p/062c2c6e21da
傳送門
大家都知道,Callable和DeferredResult可以用來進行非同步請求處理。利用它們,我們可以非同步生成返回值,在具體處理的過程中,我們直接在controller中返回相應的Callable或者DeferredResult,在這之後,servlet執行緒將被釋放,可用於其他連線;DeferredResult另外會有執行緒來進行結果處理,並setResult。
基礎準備
在正式開始之前,我們先做一點準備工作,在專案中新建了一個base模組。其中包含一些提供基礎支援的java類,在其他模組中可能會用到。
ResponseMsg
我們定義了一個ResponseMsg的實體類來作為我們的返回值型別:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseMsg<T> {
private int code;
private String msg;
private T data;
}
非常簡單,裡面包含了code、msg和data三個欄位,其中data為泛型型別。另外類的註解Data、NoArgsConstructor和AllArgsConstructor都是lombok提供的簡化我們開發的,主要功能分別是,為我們的類生成set和get方法,生成無參構造器和生成全參構造器。使用idea進行開發的童鞋可以裝一下lombok的支援外掛。另外,lombok的依賴參見:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-maven</artifactId>
<version>1.16.16.0</version>
<type>pom</type>
</dependency>
TaskService
我們建立了一個TaskService,用來為阻塞呼叫和Callable呼叫提供實際結果處理的。程式碼如下:
@Service
public class TaskService {
private static final Logger log = LoggerFactory.getLogger(TaskService.class);
public ResponseMsg<String> getResult(){
log.info("任務開始執行,持續等待中...");
try {
Thread.sleep(30000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("任務處理完成");
return new ResponseMsg<String>(0,"操作成功","success");
}
}
可以看到,裡面實際提供服務的是getResult方法,這邊直接返回一個new ResponseMsg<String>(0,"操作成功","success")。但是其中又特意讓它sleep了30秒,模擬一個耗時較長的請求。
阻塞呼叫
平時我們用的最普遍的還是阻塞呼叫,通常請求的處理時間較短,在併發量較小的情況下,使用阻塞呼叫問題也不是很大。
阻塞呼叫實現非常簡單,我們首先新建一個模組blockingtype,裡面只包含一個controller類,用來接收請求並利用TaskService來獲取結果。
@RestController
public class BlockController {
private static final Logger log = LoggerFactory.getLogger(BlockController.class);
@Autowired
private TaskService taskService;
@RequestMapping(value = "/get", method = RequestMethod.GET)
public ResponseMsg<String> getResult(){
log.info("接收請求,開始處理...");
ResponseMsg<String> result = taskService.getResult();
log.info("接收任務執行緒完成並退出");
return result;
}
}
我們請求的是getResult方法,其中呼叫了taskService,這個taskService我們是注入得到的。關於怎麼跨模組注入的,其實也非常簡單,在本模組,加入對其他模組的依賴就可以了。比如這裡我們在blockingtype的pom.xml檔案中加入對base模組的依賴:
<dependency>
<groupId>com.sunny</groupId>
<artifactId>base</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
然後我們看一下實際呼叫效果,這裡我們設定埠號為8080,啟動日誌如下:
2018-06-24 19:02:48.514 INFO 11207 --- [ main] com.sunny.BlockApplication : Starting BlockApplication on xdeMacBook-Pro.local with PID 11207 (/Users/zsunny/IdeaProjects/asynchronoustask/blockingtype/target/classes started by zsunny in /Users/zsunny/IdeaProjects/asynchronoustask)
2018-06-24 19:02:48.519 INFO 11207 --- [ main] com.sunny.BlockApplication : No active profile set, falling back to default profiles: default
2018-06-24 19:02:48.762 INFO 11207 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4445629: startup date [Sun Jun 24 19:02:48 CST 2018]; root of context hierarchy
2018-06-24 19:02:50.756 INFO 11207 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2018-06-24 19:02:50.778 INFO 11207 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2018-06-24 19:02:50.780 INFO 11207 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.23
2018-06-24 19:02:50.922 INFO 11207 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2018-06-24 19:02:50.922 INFO 11207 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2200 ms
2018-06-24 19:02:51.156 INFO 11207 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2018-06-24 19:02:51.162 INFO 11207 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-24 19:02:51.163 INFO 11207 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-06-24 19:02:51.163 INFO 11207 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-06-24 19:02:51.163 INFO 11207 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2018-06-24 19:02:51.620 INFO 11207 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4445629: startup date [Sun Jun 24 19:02:48 CST 2018]; root of context hierarchy
2018-06-24 19:02:51.724 INFO 11207 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/get],methods=[GET]}" onto public com.sunny.entity.ResponseMsg<java.lang.String> com.sunny.controller.BlockController.getResult()
2018-06-24 19:02:51.730 INFO 11207 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-06-24 19:02:51.731 INFO 11207 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-06-24 19:02:51.780 INFO 11207 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 19:02:51.780 INFO 11207 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 19:02:51.838 INFO 11207 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 19:02:52.126 INFO 11207 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-06-24 19:02:52.205 INFO 11207 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2018-06-24 19:02:52.211 INFO 11207 --- [ main] com.sunny.BlockApplication : Started BlockApplication in 5.049 seconds (JVM running for 6.118)
可以看到順利啟動了,那麼我們就來訪問一下:
http://localhost:8080/get
等待了大概30秒左右,得到json資料:
{"code":0,"msg":"操作成功","data":"success"}
圖片.png
然後我們來看看控制檯的日誌:
2018-06-24 19:04:07.315 INFO 11207 --- [nio-8080-exec-1] com.sunny.controller.BlockController : 接收請求,開始處理...
2018-06-24 19:04:07.316 INFO 11207 --- [nio-8080-exec-1] com.sunny.service.TaskService : 任務開始執行,持續等待中...
2018-06-24 19:04:37.322 INFO 11207 --- [nio-8080-exec-1] com.sunny.service.TaskService : 任務處理完成
2018-06-24 19:04:37.322 INFO 11207 --- [nio-8080-exec-1] com.sunny.controller.BlockController : 接收任務執行緒完成並退出
可以看到在“ResponseMsg<String> result = taskService.getResult();”的時候是阻塞了大約30秒鐘,隨後才執行它後面的列印語句“log.info("接收任務執行緒完成並退出");”。
Callable非同步呼叫
涉及到較長時間的請求處理的話,比較好的方式是用非同步呼叫,比如利用Callable返回結果。非同步主要表現在,接收請求的servlet可以不用持續等待結果產生,而可以被釋放去處理其他事情。當然,在呼叫者來看的話,其實還是表現在持續等待30秒。這有利於服務端提供更大的併發處理量。
這裡我們新建一個callabledemo模組,在這個模組中,我們一樣只包含一個TaskController,另外也是需要加入base模組的依賴。只不過這裡我們的返回值不是ResponseMsg型別了,而是一個Callable型別。
@RestController
public class TaskController {
private static final Logger log = LoggerFactory.getLogger(TaskController.class);
@Autowired
private TaskService taskService;
@RequestMapping(value = "/get",method = RequestMethod.GET)
public Callable<ResponseMsg<String>> getResult(){
log.info("接收請求,開始處理...");
Callable<ResponseMsg<String>> result = (()->{
return taskService.getResult();
});
log.info("接收任務執行緒完成並退出");
return result;
}
}
在裡面,我們建立了一個Callable型別的變數result,並實現了它的call方法,在call方法中,我們也是呼叫taskService的getResult方法得到返回值並返回。
下一步我們就執行一下這個模組,這裡我們在模組的application.yml中設定埠號為8081:
server:
port: 8081
啟動,可以看到控制檯的訊息:
2018-06-24 19:38:14.658 INFO 11226 --- [ main] com.sunny.CallableApplication : Starting CallableApplication on xdeMacBook-Pro.local with PID 11226 (/Users/zsunny/IdeaProjects/asynchronoustask/callabledemo/target/classes started by zsunny in /Users/zsunny/IdeaProjects/asynchronoustask)
2018-06-24 19:38:14.672 INFO 11226 --- [ main] com.sunny.CallableApplication : No active profile set, falling back to default profiles: default
2018-06-24 19:38:14.798 INFO 11226 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4445629: startup date [Sun Jun 24 19:38:14 CST 2018]; root of context hierarchy
2018-06-24 19:38:16.741 INFO 11226 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8081 (http)
2018-06-24 19:38:16.762 INFO 11226 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2018-06-24 19:38:16.764 INFO 11226 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.23
2018-06-24 19:38:16.918 INFO 11226 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2018-06-24 19:38:16.919 INFO 11226 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2126 ms
2018-06-24 19:38:17.144 INFO 11226 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2018-06-24 19:38:17.149 INFO 11226 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-24 19:38:17.150 INFO 11226 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-06-24 19:38:17.150 INFO 11226 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-06-24 19:38:17.150 INFO 11226 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2018-06-24 19:38:17.632 INFO 11226 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4445629: startup date [Sun Jun 24 19:38:14 CST 2018]; root of context hierarchy
2018-06-24 19:38:17.726 INFO 11226 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/get],methods=[GET]}" onto public java.util.concurrent.Callable<com.sunny.entity.ResponseMsg<java.lang.String>> com.sunny.controller.TaskController.getResult()
2018-06-24 19:38:17.731 INFO 11226 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-06-24 19:38:17.733 INFO 11226 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-06-24 19:38:17.777 INFO 11226 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 19:38:17.777 INFO 11226 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 19:38:17.825 INFO 11226 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 19:38:18.084 INFO 11226 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-06-24 19:38:18.176 INFO 11226 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8081 (http)
2018-06-24 19:38:18.183 INFO 11226 --- [ main] com.sunny.CallableApplication : Started CallableApplication in 4.538 seconds (JVM running for 5.327)
完美啟動了,然後我們還是一樣,訪問一下:
http://localhost:8081/get
在大約等待了30秒左右,我們在瀏覽器上得到json資料:
{"code":0,"msg":"操作成功","data":"success"}
圖片.png
和阻塞呼叫的結果一樣——當然一樣啦,都是同taskService中得到的結果。
然後我們看看控制檯的訊息:
2018-06-24 19:39:07.738 INFO 11226 --- [nio-8081-exec-1] com.sunny.controller.TaskController : 接收請求,開始處理...
2018-06-24 19:39:07.740 INFO 11226 --- [nio-8081-exec-1] com.sunny.controller.TaskController : 接收任務執行緒完成並退出
2018-06-24 19:39:07.753 INFO 11226 --- [ MvcAsync1] com.sunny.service.TaskService : 任務開始執行,持續等待中...
2018-06-24 19:39:37.756 INFO 11226 --- [ MvcAsync1] com.sunny.service.TaskService : 任務處理完成
很顯然,這裡的訊息出現的順序和阻塞模式有所不同了,這裡在“接收請求,開始處理...”之後直接列印了“接收任務執行緒完成並退出”。而不是先出現“任務處理完成”後再出現“接收任務執行緒完成並退出”。這就說明,這裡沒有阻塞在從taskService中獲得資料的地方,controller中直接執行後面的部分(這裡可以做其他很多事,不僅僅是列印日誌)。
DeferredResult非同步呼叫
前面鋪墊了那麼多,還是主要來說DeferredResult的;和Callable一樣,DeferredResult也是為了支援非同步呼叫。兩者的主要差異,Sunny覺得主要在DeferredResult需要自己用執行緒來處理結果setResult,而Callable的話不需要我們來維護一個結果處理執行緒。總體來說,Callable的話更為簡單,同樣的也是因為簡單,靈活性不夠;相對地,DeferredResult更為複雜一些,但是又極大的靈活性。在可以用Callable的時候,直接用Callable;而遇到Callable沒法解決的場景的時候,可以嘗試使用DeferredResult。
這裡Sunny將會設計兩個DeferredResult使用場景。
場景一:
- 建立一個持續在隨機間隔時間後從任務佇列中獲取任務的執行緒
- 訪問controller中的方法,建立一個DeferredResult,設定超時時間和超時返回物件
- 設定DeferredResult的超時回撥方法和完成回撥方法
- 將DeferredResult放入任務中,並將任務放入任務佇列
- 步驟1中的執行緒獲取到任務佇列中的任務,併產生一個隨機結果返回
場景其實非常簡單,接下來我們來看看具體的實現。首先,我們還是來看任務實體類是怎麼樣的。
/**
* 任務實體類
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Task {
private int taskId;
private DeferredResult<ResponseMsg<String>> taskResult;
@Override
public String toString() {
return "Task{" +
"taskId=" + taskId +
", taskResult" + "{responseMsg=" + taskResult.getResult() + "}" +
'}';
}
}
看起來非常簡單,成員變數又taskId和taskResult,前者是int型別,後者為我們的DeferredResult型別,它的泛型型別為ResponseMsg<String>,注意這裡用到ResponseMsg,所以也需要匯入base模組的依賴。另外註解之前已經說明了,不過這裡再提一句,@Data註解也包含了toString的重寫,但是這裡為了知道具體的ResponseMsg的內容,Sunny特意手動重寫。
看完Task型別,我們再來看看任務佇列。
@Component
public class TaskQueue {
private static final Logger log = LoggerFactory.getLogger(TaskQueue.class);
private static final int QUEUE_LENGTH = 10;
private BlockingQueue<Task> queue = new LinkedBlockingDeque<>(QUEUE_LENGTH);
private int taskId = 0;
/**
* 加入任務
* @param deferredResult
*/
public void put(DeferredResult<ResponseMsg<String>> deferredResult){
taskId++;
log.info("任務加入佇列,id為:{}",taskId);
queue.offer(new Task(taskId,deferredResult));
}
/**
* 獲取任務
* @return
* @throws InterruptedException
*/
public Task take() throws InterruptedException {
Task task = queue.poll();
log.info("獲得任務:{}",task);
return task;
}
}
這裡我們將它作為一個bean,之後會在其他bean中注入,這裡實際的佇列為成員變數queue,它是LinkedBlockingDeque型別的。還有一個成員變數為taskId,是用於自動生成任務id的,並且在加入任務的方法中實現自增,以確保每個任務的id唯一性。方法的話又put和take方法,分別用於向佇列中新增任務和取出任務;其中,對queue的操作,分別用了offer和poll,這樣是實現一個非阻塞的操作,並且在佇列為空和佇列已滿的情況下不會丟擲異常。另外,大家實現的時候,可以考慮使用ConcurrentLinkedQueue來高效處理併發,Sunny這裡選擇阻塞佇列,在使用的時候需要加鎖。
然後我們來看步驟1中的,啟動一個持續從任務佇列中獲取任務的執行緒的具體實現。
@Component
public class TaskExecute {
private static final Logger log = LoggerFactory.getLogger(TaskExecute.class);
private static final Random random = new Random();
//預設隨機結果的長度
private static final int DEFAULT_STR_LEN = 10;
//用於生成隨機結果
private static final String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
@Autowired
private TaskQueue taskQueue;
/**
* 初始化啟動
*/
@PostConstruct
public void init(){
log.info("開始持續處理任務");
new Thread(this::execute).start();
}
/**
* 持續處理
* 返回執行結果
*/
private void execute(){
while (true){
try {
//取出任務
Task task;
synchronized (taskQueue) {
task = taskQueue.take();
}
if(task != null) {
//設定返回結果
String randomStr = getRandomStr(DEFAULT_STR_LEN);
ResponseMsg<String> responseMsg = new ResponseMsg<String>(0, "success", randomStr);
log.info("返回結果:{}", responseMsg);
task.getTaskResult().setResult(responseMsg);
}
int time = random.nextInt(10);
log.info("處理間隔:{}秒",time);
Thread.sleep(time*1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 獲取長度為len的隨機串
* @param len
* @return
*/
private String getRandomStr(int len){
int maxInd = str.length();
StringBuilder sb = new StringBuilder();
int ind;
for(int i=0;i<len;i++){
ind = random.nextInt(maxInd);
sb.append(str.charAt(ind));
}
return String.valueOf(sb);
}
}
這裡,我們注入了TaskQueue,成員變數比較簡單並且有註釋,不再說明,主要來看方法。先看一下最後一個方法getRandomStr,很顯然,這是一個獲得長度為len的隨機串的方法,訪問限定為private,為類中其他方法服務的。然後我們看init方法,它執行的其實就是開啟了一個執行緒並且執行execute方法,注意一下它上面的@PostContruct註解,這個註解就是在這個bean初始化的時候就執行這個方法。所以我們需要關注的實際邏輯在execute方法中。可以看到,在execute方法中,用了一個while(true)來保證執行緒持續執行。因為是併發環境下,考慮對taskQueue加鎖,從中取出任務;如果任務不為空,獲取用getRandomStr生成一個隨機結果並用setResult方法進行返回。最後可以看到,利用random生成來一個[0,10)的隨機數,並讓執行緒sleep相應的秒數。這裡注意一下,需要設定一個時間間隔,否則,先執行緒持續跑會出現CPU負載過高的情況。
接下來我們就看看controller是如何處理的。
@RestController
public class TaskController {
private static final Logger log = LoggerFactory.getLogger(TaskController.class);
//超時結果
private static final ResponseMsg<String> OUT_OF_TIME_RESULT = new ResponseMsg<>(-1,"超時","out of time");
//超時時間
private static final long OUT_OF_TIME = 3000L;
@Autowired
private TaskQueue taskQueue;
@RequestMapping(value = "/get",method = RequestMethod.GET)
public DeferredResult<ResponseMsg<String>> getResult() {
log.info("接收請求,開始處理...");
//建立DeferredResult物件,設定超時時間,以及超時返回超時結果
DeferredResult<ResponseMsg<String>> result = new DeferredResult<>(OUT_OF_TIME, OUT_OF_TIME_RESULT);
result.onTimeout(() -> {
log.info("呼叫超時");
});
result.onCompletion(() -> {
log.info("呼叫完成");
});
//併發,加鎖
synchronized (taskQueue) {
taskQueue.put(result);
}
log.info("接收任務執行緒完成並退出");
return result;
}
}
這裡我們同樣注入了taskQueue。請求方法就只有一個getResult,返回值為DeferredResult<ResponseMsg<String>>。這裡我們首先建立了DeferredResult物件result並且設定超時時間和超時返回結果;隨後設定result的onTimeout和onCompletion方法,其實就是傳入兩個Runnable物件來實現回撥的效果;之後就是加鎖並且將result加入任務佇列中。
總體來說,場景不算非常複雜,看到這裡大家應該都能基本瞭解了。然後我們來跑一下測試一下。我們在application.yml中設定埠為8082:
server:
port: 8082
啟動模組,控制檯資訊如下:
2018-06-24 21:49:28.815 INFO 11322 --- [ main] com.sunny.DeferredResultApplication : Starting DeferredResultApplication on xdeMacBook-Pro.local with PID 11322 (/Users/zsunny/IdeaProjects/asynchronoustask/deferredresultdemo/target/classes started by zsunny in /Users/zsunny/IdeaProjects/asynchronoustask)
2018-06-24 21:49:28.821 INFO 11322 --- [ main] com.sunny.DeferredResultApplication : No active profile set, falling back to default profiles: default
2018-06-24 21:49:29.010 INFO 11322 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5ccddd20: startup date [Sun Jun 24 21:49:28 CST 2018]; root of context hierarchy
2018-06-24 21:49:30.971 INFO 11322 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8082 (http)
2018-06-24 21:49:30.980 INFO 11322 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2018-06-24 21:49:30.981 INFO 11322 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.23
2018-06-24 21:49:31.062 INFO 11322 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2018-06-24 21:49:31.063 INFO 11322 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2066 ms
2018-06-24 21:49:31.207 INFO 11322 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2018-06-24 21:49:31.212 INFO 11322 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-24 21:49:31.213 INFO 11322 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-06-24 21:49:31.213 INFO 11322 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-06-24 21:49:31.213 INFO 11322 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2018-06-24 21:49:31.247 INFO 11322 --- [ main] com.sunny.bean.TaskExecute : 開始持續處理任務
2018-06-24 21:49:31.249 INFO 11322 --- [ Thread-8] com.sunny.bean.TaskQueue : 獲得任務:null
2018-06-24 21:49:31.250 INFO 11322 --- [ Thread-8] com.sunny.bean.TaskExecute : 處理間隔:6秒
2018-06-24 21:49:31.498 INFO 11322 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5ccddd20: startup date [Sun Jun 24 21:49:28 CST 2018]; root of context hierarchy
2018-06-24 21:49:31.572 INFO 11322 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/get],methods=[GET]}" onto public org.springframework.web.context.request.async.DeferredResult<com.sunny.entity.ResponseMsg<java.lang.String>> com.sunny.controller.TaskController.getResult()
2018-06-24 21:49:31.576 INFO 11322 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-06-24 21:49:31.577 INFO 11322 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-06-24 21:49:31.602 INFO 11322 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 21:49:31.602 INFO 11322 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 21:49:31.628 INFO 11322 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 21:49:31.811 INFO 11322 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-06-24 21:49:31.892 INFO 11322 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8082 (http)
2018-06-24 21:49:31.897 INFO 11322 --- [ main] com.sunny.DeferredResultApplication : Started DeferredResultApplication in 3.683 seconds (JVM running for 4.873)
2018-06-24 21:49:37.254 INFO 11322 --- [ Thread-8] com.sunny.bean.TaskQueue : 獲得任務:null
2018-06-24 21:49:37.254 INFO 11322 --- [ Thread-8] com.sunny.bean.TaskExecute : 處理間隔:6秒
首先程式完美啟動,這沒有問題,然後我們注意這幾條資訊:
2018-06-24 21:49:31.247 INFO 11322 --- [ main] com.sunny.bean.TaskExecute : 開始持續處理任務
2018-06-24 21:49:31.249 INFO 11322 --- [ Thread-8] com.sunny.bean.TaskQueue : 獲得任務:null
2018-06-24 21:49:31.250 INFO 11322 --- [ Thread-8] com.sunny.bean.TaskExecute : 處理間隔:6秒
這說明我們TaskExecute中已經成功啟動了持續獲取任務的執行緒。
接著,我們還是訪問一下:
http://localhost:8082/get
這一回等待了若干秒就出現了結果:
{"code":0,"msg":"success","data":"CEUO2lmMJr"}
圖片.png
可以看到我們的隨機串是CEUO2lmMJr。再一次請求又會出現不同的隨機串。再看一下我們控制檯的相關資訊:
2018-06-24 21:51:04.303 INFO 11322 --- [nio-8082-exec-1] com.sunny.controller.TaskController : 接收請求,開始處理...
2018-06-24 21:51:04.304 INFO 11322 --- [nio-8082-exec-1] com.sunny.bean.TaskQueue : 任務加入佇列,id為:1
2018-06-24 21:51:04.304 INFO 11322 --- [nio-8082-exec-1] com.sunny.controller.TaskController : 接收任務執行緒完成並退出
2018-06-24 21:51:04.323 INFO 11322 --- [ Thread-8] com.sunny.bean.TaskQueue : 獲得任務:Task{taskId=1, taskResult{responseMsg=null}}
2018-06-24 21:51:04.323 INFO 11322 --- [ Thread-8] com.sunny.bean.TaskExecute : 返回結果:ResponseMsg(code=0, msg=success, data=CEUO2lmMJr)
也是符合我們的預期,請求進來進入佇列中,由TaskExecute獲取請求並進行處理結果返回。
場景二
- 使用者傳送請求到TaskController的getResult方法,該方法接收到請求,建立一個DeferredResult,設定超時時間和超時返回物件
- 設定DeferredResult的超時回撥方法和完成回撥方法,超時和完成都會將本次請求產生的DeferredResult從集合中remove
- 將DeferredResult放入集合中
- 另有一個TaskExecuteController,訪問其中一個方法,可取出集合中的等待返回的DeferredResult物件,並將傳入的引數設定為結果
首先我們來看看DeferredResult的集合類:
@Component
@Data
public class TaskSet {
private Set<DeferredResult<ResponseMsg<String>>> set = new HashSet<>();
}
非常簡單,只包含了一個HashSet的成員變數。這裡可以考慮用ConcurrentHashMap來實現高效併發,Sunny這裡簡單實用HashSet,配合加鎖實現併發處理。
然後我們看看發起呼叫的Controller程式碼:
@RestController
public class TaskController {
private Logger log = LoggerFactory.getLogger(TaskController.class);
//超時結果
private static final ResponseMsg<String> OUT_OF_TIME_RESULT = new ResponseMsg<>(-1,"超時","out of time");
//超時時間
private static final long OUT_OF_TIME = 60000L;
@Autowired
private TaskSet taskSet;
@RequestMapping(value = "/get",method = RequestMethod.GET)
public DeferredResult<ResponseMsg<String>> getResult(){
log.info("接收請求,開始處理...");
//建立DeferredResult物件,設定超時時間,以及超時返回超時結果
DeferredResult<ResponseMsg<String>> result = new DeferredResult<>(OUT_OF_TIME, OUT_OF_TIME_RESULT);
result.onTimeout(() -> {
log.info("呼叫超時,移除任務,此時佇列長度為{}",taskSet.getSet().size());
synchronized (taskSet.getSet()) {
taskSet.getSet().remove(result);
}
});
result.onCompletion(() -> {
log.info("呼叫完成,移除任務,此時佇列長度為{}",taskSet.getSet().size());
synchronized (taskSet.getSet()) {
taskSet.getSet().remove(result);
}
});
//併發,加鎖
synchronized (taskSet.getSet()) {
taskSet.getSet().add(result);
}
log.info("加入任務集合,集合大小為:{}",taskSet.getSet().size());
log.info("接收任務執行緒完成並退出");
return result;
}
}
和場景一中有些類似,但是注意這裡在onTimeout和onCompletion中都多了一個移除元素的操作,這也就是每次呼叫結束,需要將集合中的DeferredResult物件移除,即集合中儲存的都是等待請求結果的DeferredResult物件。
然後我們看處理請求結果的Controller:
@RestController
public class TaskExecuteController {
private static final Logger log = LoggerFactory.getLogger(TaskExecuteController.class);
@Autowired
private TaskSet taskSet;
@RequestMapping(value = "/set/{result}",method = RequestMethod.GET)
public String setResult(@PathVariable("result") String result){
ResponseMsg<String> res = new ResponseMsg<>(0,"success",result);
log.info("結果處理開始,得到輸入結果為:{}",res);
Set<DeferredResult<ResponseMsg<String>>> set = taskSet.getSet();
synchronized (set){
set.forEach((deferredResult)->{deferredResult.setResult(res);});
}
return "Successfully set result: " + result;
}
}
看起來非常簡單,只是做了兩個操作,接收得到的引數並利用引數生成一個ResponseMsg<String>物件,隨後將集合中的所有DeferredResult都設定結果為根據引數生成的ResponseMsg<String>物件。最後返回一個提示:成功設定結果...
好了,話不多說,我們來啟動測試驗證一下。我們說一下驗證的過程,我們同時開啟兩個請求,然後再設定一個結果,最後兩個請求都會得到這個結果。當然同時多個或者一個請求也是一樣。這裡有一個地方需要注意一下:
瀏覽器可能會對相同的url請求有快取策略,也就是同時兩個標籤向同一個url傳送請求,瀏覽器只會先傳送一個請求,等一個請求結束才會再傳送另外一個請求。
這樣,我們考慮從兩個瀏覽器中傳送請求:
localhost:8083/get
然後隨便找其中一個,傳送請求來設定結果:
http://localhost:8083/set/aaa
首先我們先啟動模組,可以從控制檯中看到完美啟動管理了:
2018-06-25 00:18:44.379 INFO 12688 --- [ main] com.sunny.DeferredResultApplication : Starting DeferredResultApplication on xdeMacBook-Pro.local with PID 12688 (/Users/zsunny/IdeaProjects/asynchronoustask/deferredresultdemo2/target/classes started by zsunny in /Users/zsunny/IdeaProjects/asynchronoustask)
2018-06-25 00:18:44.382 INFO 12688 --- [ main] com.sunny.DeferredResultApplication : No active profile set, falling back to default profiles: default
2018-06-25 00:18:44.489 INFO 12688 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@96def03: startup date [Mon Jun 25 00:18:44 CST 2018]; root of context hierarchy
2018-06-25 00:18:45.650 INFO 12688 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8083 (http)
2018-06-25 00:18:45.658 INFO 12688 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2018-06-25 00:18:45.659 INFO 12688 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.23
2018-06-25 00:18:45.722 INFO 12688 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2018-06-25 00:18:45.723 INFO 12688 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1241 ms
2018-06-25 00:18:45.817 INFO 12688 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2018-06-25 00:18:45.821 INFO 12688 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-25 00:18:45.821 INFO 12688 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-06-25 00:18:45.821 INFO 12688 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-06-25 00:18:45.821 INFO 12688 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2018-06-25 00:18:46.150 INFO 12688 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@96def03: startup date [Mon Jun 25 00:18:44 CST 2018]; root of context hierarchy
2018-06-25 00:18:46.197 INFO 12688 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/get],methods=[GET]}" onto public org.springframework.web.context.request.async.DeferredResult<com.sunny.entity.ResponseMsg<java.lang.String>> com.sunny.controller.TaskController.getResult()
2018-06-25 00:18:46.199 INFO 12688 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/set/{result}],methods=[GET]}" onto public java.lang.String com.sunny.controller.TaskExecuteController.setResult(java.lang.String)
2018-06-25 00:18:46.202 INFO 12688 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-06-25 00:18:46.202 INFO 12688 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-06-25 00:18:46.237 INFO 12688 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-25 00:18:46.238 INFO 12688 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-25 00:18:46.262 INFO 12688 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-25 00:18:46.362 INFO 12688 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-06-25 00:18:46.467 INFO 12688 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8083 (http)
2018-06-25 00:18:46.472 INFO 12688 --- [ main] com.sunny.DeferredResultApplication : Started DeferredResultApplication in 2.675 seconds (JVM running for 3.623)
完美啟動,接下來Sunny在火狐中發起一個請求
圖片.png
可以看到正在等待請求結果。隨後我們在谷歌瀏覽器中發起請求
圖片.png
兩個請求同時處於等待狀態,這時候我們看一下控制檯資訊:
2018-06-25 00:22:34.642 INFO 12688 --- [nio-8083-exec-6] com.sunny.controller.TaskController : 接收請求,開始處理...
2018-06-25 00:22:34.642 INFO 12688 --- [nio-8083-exec-6] com.sunny.controller.TaskController : 加入任務集合,集合大小為:1
2018-06-25 00:22:34.642 INFO 12688 --- [nio-8083-exec-6] com.sunny.controller.TaskController : 接收任務執行緒完成並退出
2018-06-25 00:22:37.332 INFO 12688 --- [nio-8083-exec-7] com.sunny.controller.TaskController : 接收請求,開始處理...
2018-06-25 00:22:37.332 INFO 12688 --- [nio-8083-exec-7] com.sunny.controller.TaskController : 加入任務集合,集合大小為:2
2018-06-25 00:22:37.332 INFO 12688 --- [nio-8083-exec-7] com.sunny.controller.TaskController : 接收任務執行緒完成並退出
可以看到兩個請求都已經接收到了,並且加入了佇列。這時候,我們再傳送一個設定結果的請求。
圖片.png
隨後我們檢視兩個呼叫請求的頁面,發現頁面已經不在等待狀態中了,都已經得到了結果。
圖片.png
圖片.png
另外,再給大家展示一下超時的結果,即我們發起呼叫請求,但是不發起設定結果的請求,等待時間結束。
圖片.png
檢視控制檯資訊:
2018-06-25 00:26:15.898 INFO 12688 --- [nio-8083-exec-4] com.sunny.controller.TaskController : 接收請求,開始處理...
2018-06-25 00:26:15.898 INFO 12688 --- [nio-8083-exec-4] com.sunny.controller.TaskController : 加入任務集合,集合大小為:1
2018-06-25 00:26:15.898 INFO 12688 --- [nio-8083-exec-4] com.sunny.controller.TaskController : 接收任務執行緒完成並退出
2018-06-25 00:27:16.014 INFO 12688 --- [nio-8083-exec-5] com.sunny.controller.TaskController : 呼叫超時,移除任務,此時佇列長度為1
2018-06-25 00:27:16.018 INFO 12688 --- [nio-8083-exec-5] com.sunny.controller.TaskController : 呼叫完成,移除任務,此時佇列長度為0
後記
想要完整程式碼的童鞋,可以點此檢視fork實踐。
相關文章
- SpringBoot 教程之處理非同步請求Spring Boot非同步
- 直播帶貨原始碼,非同步處理中會處理兩次請求原始碼非同步
- 請求資料處理
- springmvc處理ajax請求SpringMVC
- nginx 如何處理一個請求Nginx
- Go Web如何處理Web請求?GoWeb
- yai 請求預處理指令碼AI指令碼
- Netty(二):如何處理io請求?Netty
- Laravel請求處理管道理解Laravel
- 處理 HTTP 請求的註解HTTP
- springmvc原始碼 ---DispatcherServlet 處理請求SpringMVC原始碼Servlet
- 非同步的發展,順手學會怎麼處理多請求非同步
- Java後端中的請求最佳化:從請求合併到非同步處理的實現策略Java後端非同步
- 處理請求(AFURLRequestSerialization)和響應(AFURLResponseSerialization)
- 使用cors完成跨域請求處理CORS跨域
- linux如何處理多連線請求?Linux
- 4、Ktor學習-處理HTTP請求;HTTP
- java webservice 帶請求頭方式處理JavaWeb
- Spring MVC的請求處理邏輯SpringMVC
- SpringBoot可以同時處理多少請求?Spring Boot
- 搜尋 伺服器處理請求伺服器
- options 請求跨域問題處理跨域
- Nacos - 服務端處理心跳請求服務端
- 關於在request請求時,處理請求引數的問題
- Nginx請求處理流程你瞭解嗎?Nginx
- axios中POST請求變成OPTIONS處理iOS
- Node.js如何處理多個請求?Node.js
- 封裝springmvc處理ajax請求結果封裝SpringMVC
- 前後端處理流檔案請求後端
- Apache Tomcat如何高併發處理請求ApacheTomcat
- Laravel 底層是如何處理HTTP請求LaravelHTTP
- Nacos - 服務端處理註冊請求服務端
- SpringBoot使用Axios傳送請求,引數處理Spring BootiOS
- Spring MVC框架處理Web請求的基本流程SpringMVC框架Web
- 用Golang處理每分鐘100萬份請求Golang
- ajax呼叫WebMethed返回處理請求時出錯Web
- iOS for 迴圈內網路請求的處理iOS內網
- spring security:ajax請求的session超時處理SpringSession