歡迎光臨
每天分享高質量文章

SpringBoot | 第二十章:非同步開發之非同步請求

(點選上方公眾號,可快速關註)


來源:oKong ,

blog.lqdev.cn/2018/08/16/springboot/chapter-twenty/

前言

關於web開發的相關知識點,後續有補充時再開續寫了。比如webService服務、發郵件等,這些一般上覺得不完全屬於web開發方面的,而且目前webService作為一個介面來提供服務的機會應該比較小了吧。所以本章節開始,開始講解關於非同步開發過程中會使用到的一些知識點。本章節就來講解下非同步請求相關知識點。

一點知識

何為非同步請求

在Servlet 3.0之前,Servlet採用Thread-Per-Request的方式處理請求,即每一次Http請求都由某一個執行緒從頭到尾負責處理。如果一個請求需要進行IO操作,比如訪問資料庫、呼叫第三方服務介面等,那麼其所對應的執行緒將同步地等待**IO操作完成, 而IO操作是非常慢的,所以此時的執行緒並不能及時地釋放回執行緒池以供後續使用,在併發量越來越大的情況下,這將帶來嚴重的效能問題。其請求流程大致為:

而在Servlet3.0釋出後,提供了一個新特性:非同步處理請求。可以先釋放容器分配給請求的執行緒與相關資源,減輕系統負擔,釋放了容器所分配執行緒的請求,其響應將被延後,可以在耗時處理完成(例如長時間的運算)時再對客戶端進行響應。其請求流程為:

在Servlet 3.0後,我們可以從HttpServletRequest物件中獲得一個AsyncContext物件,該物件構成了非同步處理的背景關係,Request和Response物件都可從中獲取。AsyncContext可以從當前執行緒傳給另外的執行緒,併在新的執行緒中完成對請求的處理並傳回結果給客戶端,初始執行緒便可以還回給容器執行緒池以處理更多的請求。如此,透過將請求從一個執行緒傳給另一個執行緒處理的過程便構成了Servlet 3.0中的非同步處理。

多說幾句:

隨著Spring5釋出,提供了一個響應式Web框架:Spring WebFlux。之後可能就不需要Servlet容器的支援了。以下是其先後對比圖:

左側是傳統的基於Servlet的Spring Web MVC框架,右側是5.0版本新引入的基於Reactive Streams的Spring WebFlux框架,從上到下依次是Router Functions,WebFlux,Reactive Streams三個新元件。

對於其發展前景還是拭目以待吧。有時間也該去瞭解下Spring5了。

原生非同步請求API說明

在編寫實際程式碼之前,我們來瞭解下一些關於非同步請求的api的呼叫說明。

獲取AsyncContext:根據HttpServletRequest物件獲取。

AsyncContext asyncContext = request.startAsync();

設定監聽器:可設定其開始、完成、異常、超時等事件的回呼處理

其監聽器的介面程式碼:

public interface AsyncListener extends EventListener {

    void onComplete(AsyncEvent event) throws IOException;

    void onTimeout(AsyncEvent event) throws IOException;

    void onError(AsyncEvent event) throws IOException;

    void onStartAsync(AsyncEvent event) throws IOException;

}

說明:

  1. onStartAsync:非同步執行緒開始時呼叫

  2. onError:非同步執行緒出錯時呼叫

  3. onTimeout:非同步執行緒執行超時呼叫

  4. onComplete:非同步執行完畢時呼叫

一般上,我們在超時或者異常時,會傳回給前端相應的提示,比如說超時了,請再次請求等等,根據各業務進行自定義傳回。同時,在非同步呼叫完成時,一般需要執行一些清理工作或者其他相關操作。

需要註意的是隻有在呼叫request.startAsync前將監聽器新增到AsyncContext,監聽器的onStartAsync方法才會起作用,而呼叫startAsync前AsyncContext還不存在,所以第一次呼叫startAsync是不會被監聽器中的onStartAsync方法捕獲的,只有在超時後又重新開始的情況下onStartAsync方法才會起作用。

設定超時:透過setTimeout方法設定,單位:毫秒。

一定要設定超時時間,不能無限等待下去,不然和正常的請求就一樣了。。

Servlet方式實現非同步請求

前面已經提到,可透過HttpServletRequest物件中獲得一個AsyncContext物件,該物件構成了非同步處理的背景關係。所以,我們來實際操作下。

0.編寫一個簡單控制層

/**

 * 使用servlet方式進行非同步請求

 * @author oKong

 *

 */

@Slf4j

@RestController

public class ServletController {

     

    @RequestMapping(“/servlet/orig”)

    public void todo(HttpServletRequest request,

            HttpServletResponse response) throws Exception {

        //這裡來個休眠

        Thread.sleep(100);

        response.getWriter().println(“這是【正常】的請求傳回”);

    }

     

    @RequestMapping(“/servlet/async”)

    public void todoAsync(HttpServletRequest request,

            HttpServletResponse response) {

        AsyncContext asyncContext = request.startAsync();

        asyncContext.addListener(new AsyncListener() {

             

            @Override

            public void onTimeout(AsyncEvent event) throws IOException {

                log.info(“超時了:”);

                //做一些超時後的相關操作

            }

             

            @Override

            public void onStartAsync(AsyncEvent event) throws IOException {

                // TODO Auto-generated method stub

                log.info(“執行緒開始”);

            }

             

            @Override

            public void onError(AsyncEvent event) throws IOException {

                log.info(“發生錯誤:”,event.getThrowable());

            }

             

            @Override

            public void onComplete(AsyncEvent event) throws IOException {

                log.info(“執行完成”);

                //這裡可以做一些清理資源的操作

                 

            }

        });

        //設定超時時間

        asyncContext.setTimeout(200);

        //也可以不使用start 進行非同步呼叫

//        new Thread(new Runnable() {

//            

//            @Override

//            public void run() {

//                編寫業務邏輯

//                

//            }

//        }).start();

         

        asyncContext.start(new Runnable() {            

            @Override

            public void run() {

                try {

                    Thread.sleep(100);

                    log.info(“內部執行緒:” + Thread.currentThread().getName());

                    asyncContext.getResponse().setCharacterEncoding(“utf-8”);

                    asyncContext.getResponse().setContentType(“text/html;charset=UTF-8”);

                    asyncContext.getResponse().getWriter().println(“這是【非同步】的請求傳回”);

                } catch (Exception e) {

                    log.error(“異常:”,e);

                }

                //非同步請求完成通知

                //此時整個請求才完成

                //其實可以利用此特性 進行多條訊息的推送 把連線掛起。。

                asyncContext.complete();

            }

        });

        //此時之類 request的執行緒連線已經釋放了

        log.info(“執行緒:” + Thread.currentThread().getName());

    } 

}

註意:非同步請求時,可以利用ThreadPoolExecutor自定義個執行緒池。

1.啟動下應用,檢視控制檯輸出就可以獲悉是否在同一個執行緒裡面了。同時,可設定下等待時間,之後就會呼叫超時回呼方法了。大家可自己試試。

2018-08-15 23:03:04.082  INFO 6732 — [nio-8080-exec-1] c.l.l.s.controller.ServletController     : 執行緒:http-nio-8080-exec-1

2018-08-15 23:03:04.183  INFO 6732 — [nio-8080-exec-2] c.l.l.s.controller.ServletController     : 內部執行緒:http-nio-8080-exec-2

2018-08-15 23:03:04.190  INFO 6732 — [nio-8080-exec-3] c.l.l.s.controller.ServletController     : 執行完成

使用過濾器時,需要加入asyncSupported為true配置,開啟非同步請求支援。

@WebServlet(urlPatterns = “/okong”, asyncSupported = true )   

public  class AsyncServlet extends HttpServlet …

題外話:其實我們可以利用在未執行asyncContext.complete()方法時請求未結束這特性,可以做個簡單的檔案上傳進度條之類的功能。但註意請求是會超時的,需要設定超時的時間下。

Spring方式實現非同步請求

在Spring中,有多種方式實現非同步請求,比如callable、DeferredResult或者WebAsyncTask。每個的用法略有不同,可根據不同的業務場景選擇不同的方式。以下主要介紹一些常用的用法

Callable

使用很簡單,直接傳回的引數包裹一層callable即可。

用法

@RequestMapping(“/callable”)

public Callable callable() {

    log.info(“外部執行緒:” + Thread.currentThread().getName());

    return new Callable() {

 

        @Override

        public String call() throws Exception {

            log.info(“內部執行緒:” + Thread.currentThread().getName());

            return “callable!”;

        }

    };

}

控制檯輸出:

2018-08-15 23:32:22.317  INFO 15740 — [nio-8080-exec-2] c.l.l.s.controller.SpringController      : 外部執行緒:http-nio-8080-exec-2

2018-08-15 23:32:22.323  INFO 15740 — [      MvcAsync1] c.l.l.s.controller.SpringController      : 內部執行緒:MvcAsync1

超時、自定義執行緒設定

從控制檯可以看見,非同步響應的執行緒使用的是名為:MvcAsync1的執行緒。第一次再訪問時,就是MvcAsync2了。若採用預設設定,會無限的建立新執行緒去處理非同步請求,所以正常都需要配置一個執行緒池及超時時間。

編寫一個配置類:CustomAsyncPool.java

@Configuration

public class CustomAsyncPool extends WebMvcConfigurerAdapter{

 

    /**

     * 配置執行緒池

     * @return

     */

    @Bean(name = “asyncPoolTaskExecutor”)

    public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() {

        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();

        taskExecutor.setCorePoolSize(20);

        taskExecutor.setMaxPoolSize(200);

        taskExecutor.setQueueCapacity(25);

        taskExecutor.setKeepAliveSeconds(200);

        taskExecutor.setThreadNamePrefix(“callable-“);

        // 執行緒池對拒絕任務(無執行緒可用)的處理策略,目前只支援AbortPolicy、CallerRunsPolicy;預設為後者

        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        taskExecutor.initialize();

        return taskExecutor;

    }

     

    @Override

    public void configureAsyncSupport(final AsyncSupportConfigurer configurer) {

        //處理 callable超時

        configurer.setDefaultTimeout(60*1000);

        configurer.registerCallableInterceptors(timeoutInterceptor());

        configurer.setTaskExecutor(getAsyncThreadPoolTaskExecutor());

    }

     

    @Bean

    public TimeoutCallableProcessor timeoutInterceptor() {

        return new TimeoutCallableProcessor();

    }     

}

自定義一個超時異常處理類:CustomAsyncRequestTimeoutException.java

/**

 * 自定義超時異常類

 * @author oKong

 *

 */

public class CustomAsyncRequestTimeoutException extends RuntimeException {

 

    /**

     * 

     */

    private static final long serialVersionUID = 8754629185999484614L;

 

    public CustomAsyncRequestTimeoutException(String uri){

        super(uri);

    }

}

同時,在統一異常處理加入對CustomAsyncRequestTimeoutException類的處理即可,這樣就有個統一的配置了。

之後,再執行就可以看見使用了自定義的執行緒池了,超時的可以自行模擬下:

2018-08-15 23:48:29.022  INFO 16060 — [nio-8080-exec-1] c.l.l.s.controller.SpringController      : 外部執行緒:http-nio-8080-exec-1

2018-08-15 23:48:29.032  INFO 16060 — [     oKong-1] c.l.l.s.controller.SpringController      : 內部執行緒:oKong-1


DeferredResult

相比於callable,DeferredResult可以處理一些相對複雜一些的業務邏輯,最主要還是可以在另一個執行緒裡面進行業務處理及傳回,即可在兩個完全不相干的執行緒間的通訊。

/**

     * 執行緒池

     */

    public static ExecutorService FIXED_THREAD_POOL = Executors.newFixedThreadPool(30);

     

@RequestMapping(“/deferredresult”)

    public DeferredResult deferredResult(){

        log.info(“外部執行緒:” + Thread.currentThread().getName());

        //設定超時時間

        DeferredResult result = new DeferredResult(60*1000L);

        //處理超時事件 採用委託機制

        result.onTimeout(new Runnable() {

             

            @Override

            public void run() {

                log.error(“DeferredResult超時”);

                result.setResult(“超時了!”);

            }

        });

        result.onCompletion(new Runnable() {

             

            @Override

            public void run() {

                //完成後

                log.info(“呼叫完成”);

            }

        });

        FIXED_THREAD_POOL.execute(new Runnable() {

             

            @Override

            public void run() {

                //處理業務邏輯

                log.info(“內部執行緒:” + Thread.currentThread().getName());

                //傳回結果

                result.setResult(“DeferredResult!!”);

            }

        });

        return result;

    }

控制檯輸出:

2018-08-15 23:52:27.841  INFO 12984 — [nio-8080-exec-2] c.l.l.s.controller.SpringController      : 外部執行緒:http-nio-8080-exec-2

2018-08-15 23:52:27.843  INFO 12984 — [pool-1-thread-1] c.l.l.s.controller.SpringController      : 內部執行緒:pool-1-thread-1

2018-08-15 23:52:27.872  INFO 12984 — [nio-8080-exec-2] c.l.l.s.controller.SpringController      : 呼叫完成

註意:傳回結果時記得呼叫下setResult方法。

題外話:利用DeferredResult可實現一些長連線的功能,比如當某個操作是非同步時,我們可以儲存這個DeferredResult物件,當非同步通知回來時,我們在找回這個DeferredResult物件,之後在setResult會結果即可。提高效能。

WebAsyncTask

使用方法都類似,只是WebAsyncTask是直接傳回了。覺得就是寫法不同而已,更多細節希望大神解答!

@RequestMapping(“/webAsyncTask”)

   public WebAsyncTask webAsyncTask() {

       log.info(“外部執行緒:” + Thread.currentThread().getName());

       WebAsyncTask result = new WebAsyncTask(60*1000L, new Callable() {

 

           @Override

           public String call() throws Exception {

               log.info(“內部執行緒:” + Thread.currentThread().getName());

               return “WebAsyncTask!!!”;

           }

       });

       result.onTimeout(new Callable() {

            

           @Override

           public String call() throws Exception {

               // TODO Auto-generated method stub

               return “WebAsyncTask超時!!!”;

           }

       });

       result.onCompletion(new Runnable() {

            

           @Override

           public void run() {

               //超時後 也會執行此方法

               log.info(“WebAsyncTask執行結束”);

           }

       });

       return result;

   }

控制檯輸出:

2018-08-15 23:55:02.568  INFO 2864 — [nio-8080-exec-1] c.l.l.s.controller.SpringController      : 外部執行緒:http-nio-8080-exec-1

2018-08-15 23:55:02.587  INFO 2864 — [          oKong-1] c.l.l.s.controller.SpringController      : 內部執行緒:oKong-1

2018-08-15 23:55:02.615  INFO 2864 — [nio-8080-exec-2] c.l.l.s.controller.SpringController      : WebAsyncTask執行結束

參考資料

  1. https://blog.csdn.net/paincupid/article/details/52266905

  2. https://docs.spring.io/spring/docs/4.3.18.RELEASE/spring-framework-reference/htmlsingle/#mvc-ann-async

總結

本章節主要是講解了非同步請求的使用及相關配置,如超時,異常等處理。設定非同步請求時,記得不要忘記設定超時時間。非同步請求只是提高了服務的吞吐量,提高單位時間內處理的請求數,並不會加快處理效率的,這點需要註意。。下一章節,講講使用@Async進行非同步呼叫相關知識。

最後

目前網際網路上很多大佬都有SpringBoot系列教程,如有雷同,請多多包涵了。本文是作者在電腦前一字一句敲的,每一步都是自己實踐的。若文中有所錯誤之處,還望提出,謝謝。

系列


【關於投稿】


如果大家有原創好文投稿,請直接給公號傳送留言。


① 留言格式:
【投稿】+《 文章標題》+ 文章連結

② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/

③ 最後請附上您的個人簡介哈~



看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂