使用Callable或DeferredResult實現springmvc的異步請求
使用Callable實現springmvc的異步請求
如果一個請求中的某些操作耗時很長,會一直占用線程。這樣的請求多了,可能造成線程池被占滿,新請求無法執行的情況。這時,可以考慮使用異步請求,即主線程只返回Callable類型,然後去處理新請求,耗時長的業務邏輯由其他線程執行。
下面是一個示例demo,用線程睡眠來模擬耗時操作,springmvc配置以及視圖解析器、攔截器等組件的註冊略,詳見https://www.cnblogs.com/dubhlinn/p/10808879.html博文,本文只展示controller組件,歡迎頁面welcome.jsp略。
package cn.monolog.annabelle.springmvc.controller;import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.Mapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.util.concurrent.Callable; /** * 處理器,用於測試springmvc異步請求 * created on 2019-05-12*/ @Controller @RequestMapping(value = "/asyn") public class AsynController { /** * 使用Callable發送異步請求進入歡迎頁面 * @return */ @RequestMapping(value = "/welcome") public Callable<String> welcome() { //打印主線程 System.out.println("主線程" + Thread.currentThread().getName() + "開始:" + System.currentTimeMillis());//使用使用Callable獲取頁面路徑 Callable<String> path = new Callable() { @Override public String call() throws Exception { //打印副線程 System.out.println("副線程" + Thread.currentThread().getName() + "開始:" + System.currentTimeMillis()); //線程休眠5秒鐘 Thread.sleep(5000); //打印副線程 System.out.println("副線程" + Thread.currentThread().getName() + "結束:" + System.currentTimeMillis()); //返回歡迎頁面的url return "welcome"; } }; //打印主線程 System.out.println("主線程" + Thread.currentThread().getName() + "結束:" + System.currentTimeMillis()); //返回頁面 return path; } }
在瀏覽器訪問/asyn/welcome,會延時5秒鐘才進入歡迎頁面,然後來看服務器日誌,發現是這樣的:
1. 主線程開始、結束幾乎是同一時刻;
2. 副線程結束比開始晚了5秒多,是在執行線程休眠,即模擬"耗時多的業務邏輯";
3. 攔截器的preHandle方法執行了兩次。
1和2都符合我們之前的描述和預期,但是為什麽攔截器的preHandle方法會執行兩次?來看一下springmvc異步請求的原理:
1. 當處理器的方法返回Callable(以及其他異步形式的返回值,例如DeferredResult)時,springmvc會將
Callable的實現方法交給TaskExecutor在一個隔離的線程中執行;
2. 同時,DispatcherServlet(前端控制器)和所有的攔截器退出web容器的線程,但是response仍然保持打開狀態;
3. 當Callable的實現方法產生返回值時,springmvc會將請求重新派發給容器;
4. DispatcherServlet(前端控制器)重新接收請求,並根據Callable的返回值進行視圖渲染或者返回數據。
因此,第一次請求之前執行了preHandle方法,當前端控制器接收到請求之後,發現返回值是Callable,
攔截器就退出主線程了,也就沒有了後面的postHandle和afterCompletion方法。
使用DeferredResult實現springmvc的異步請求
實際項目中的異步請求,可能在不同的接口中執行,甚至可能在不同的應用中執行,這時Callable就無法實現。
springmvc提供了另一種異步返回類型:DeferredResult,其主要使用步驟是:
1. 在主線程中新建DeferredResult實例,然後直接返回這個實例,主線程結束;
2. 副線程在主線程新建的DeferredResult實例中設置值(調用setResult方法);
3. 這時,主線程獲取到副線程在DeferredResult實例中設置的值,重新接收請求、視圖渲染或返回數據。
下面是一個示例demo,我們用一個自定義隊列來模擬DeferredResult的存取。
自定義隊列
package cn.monolog.annabelle.springmvc.queue; import org.springframework.web.context.request.async.DeferredResult; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; /** * 自定義隊列,用於存儲DeferredResult * created on 2019-05-12 */ public class DeferredResultQueue { //隊列 private static Queue<DeferredResult> deferredResultQueue = new ConcurrentLinkedQueue<>(); /** * 添加DeferredResult進隊列 * @param deferredResult */ public static void save(DeferredResult deferredResult) { deferredResultQueue.add(deferredResult); } /** * 從隊列中取出第一個元素並刪除 * @return */ public static DeferredResult get() { return deferredResultQueue.poll(); } }
controller組件
package cn.monolog.annabelle.springmvc.controller; import cn.monolog.annabelle.springmvc.queue.DeferredResultQueue; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.Mapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.async.DeferredResult; import java.util.UUID; /** * 處理器,用於測試springmvc異步請求 * created on 2019-05-12 */ @Controller @RequestMapping(value = "/asyn") public class AsynController { /** * 異步創建訂單 * 只返回DeferredResult,並不真正創建訂單 * @return */ @RequestMapping("/createOrder") @ResponseBody public DeferredResult<String> createOrder() { //新建DeferredResult實例,並保存到隊列中,參數的意義是最長等待5秒,否則返回值自動設置為timeout DeferredResult<String> deferredResult = new DeferredResult<>((long)5000, "timeout"); DeferredResultQueue.save(deferredResult); //返回 return deferredResult; } /** * 真正的創建訂單 * @return */ @RequestMapping("/create") @ResponseBody public String createOrderActioin() { //隨機生成訂單號 String orderNum = UUID.randomUUID().toString(); //從隊列中取出DeferredResult DeferredResult deferredResult = DeferredResultQueue.get(); //向DeferredResult中存值 deferredResult.setResult(orderNum); //返回 return orderNum; } }
直接在瀏覽器訪問/asyn/createOrder,因為並沒有線程為DeferredResult存值,因此等待5秒之後,返回timeout。
如果在5秒之內,在瀏覽器的另一個標簽訪問/asyn/create,會發現第一個標簽返回了第二個接口創建的訂單編號。
使用Callable或DeferredResult實現springmvc的異步請求