當前位置:
首頁 > 知識 > SpringBoot | 第二十章:非同步開發之非同步請求

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<String> callable() {


    log.info("外部線程:" + Thread.currentThread().getName());


    return new Callable<String>() {


 


        @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<String> deferredResult(){


        log.info("外部線程:" + Thread.currentThread().getName());


        //設置超時時間


        DeferredResult<String> result = new DeferredResult<String>(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<String> webAsyncTask() {


       log.info("外部線程:" + Thread.currentThread().getName());


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


 


           @Override


           public String call() throws Exception {


               log.info("內部線程:" + Thread.currentThread().getName());


               return "WebAsyncTask!!!";


           }


       });


       result.onTimeout(new Callable<String>() {


            


           @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系列教程,如有雷同,請多多包涵了。本文是作者在電腦前一字一句敲的,每一步都是自己實踐的。若文中有所錯誤之處,還望提出,謝謝。




系列






  • SpringBoot | 第一章:第一個 SpringBoot 應用



  • SpringBoot | 第二章:lombok 介紹及簡單使用



  • SpringBoot | 第三章:springboot 配置詳解



  • SpringBoot | 第四章:日誌管理



  • SpringBoot | 第五章:多環境配置



  • SpringBoot | 第六章:常用註解介紹及簡單使用



  • SpringBoot | 第七章:過濾器、監聽器、攔截器



  • SpringBoot | 第八章:統一異常、數據校驗處理



  • SpringBoot | 第九章:Mybatis-plus 的集成和使用



  • SpringBoot | 第十章:Swagger 2 的集成和使用



  • SpringBoot | 第十一章:Redis 的集成和簡單使用



  • SpringBoot | 第十二章:RabbitMQ 的集成和使用



  • SpringBoot | 第十三章:測試相關 ( 單元測試、性能測試 )



  • SpringBoot | 第十四章:基於 Docker 的簡單部署



  • SpringBoot | 第十五章:基於 Postman 的 RESTful 介面測試



  • SpringBoot | 第十六章:web 應用開發



  • SpringBoot | 第十七章:web 應用開發之文件上傳



  • SpringBoot | 第十八章:web 應用開發之WebJars 使用



  • SpringBoot | 第十九章:web 應用開發之 WebSocket




【關於投稿】




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




① 留言格式:


【投稿】+《 文章標題》+ 文章鏈接

② 示例:


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

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






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


關注「ImportNew」,提升Java技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 ImportNew 的精彩文章:

SpringBoot | 第十章:Swagger 2 的集成和使用
SpringBoot | 第五章 : 多環境配置

TAG:ImportNew |