1. 程式人生 > >使用Callable或DeferredResult實現springmvc的異步請求

使用Callable或DeferredResult實現springmvc的異步請求

mvc 進入 img 結束 log web容器 刪除 dom and

使用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的異步請求