使用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,會發現第一個標簽返回了第二個接口創建的訂單編號。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM