前言
關於web開發的相關知識點,後續有補充時再開續寫了。比如webService服務、發郵件等,這些一般上覺得不完全屬於web開發方面的,而且目前webService作為一個介面來提供服務的機會應該比較小了吧。所以本章節開始,開始講解關於非同步開發過程中會使用到的一些知識點。本章節就來講解下非同步請求相關知識點。
- 一點知識
- Servlet方式實現非同步請求
- Spring方式實現非同步請求
- Callable
- DeferredResult
- WebAsyncTask
- 參考資料
- 總結
- 最後
- 老生常談
一點知識何為非同步請求
在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;}
說明:
- onStartAsync:非同步線程開始時調用
- onError:非同步線程出錯時調用
- onTimeout:非同步線程執行逾時調用
- onComplete:非同步執行完畢時調用
一般上,我們在逾時或者異常時,會返回給前端相應的提示,比如說逾時了,請再次請求等等,根據各業務進行自訂返回。同時,在非同步呼叫完成時,一般需要執行一些清理工作或者其他相關操作。
需要注意的是只有在調用request.startAsync前將監聽器添加到AsyncContext,監聽器的onStartAsync方法才會起作用,而調用startAsync前AsyncContext還不存在,所以第一次調用startAsync是不會被監聽器中的onStartAsync方法捕獲的,只有在逾時後又重新開始的情況下onStartAsync方法才會起作用。
- 設定逾時:通過
setTimeout方法設定,單位:毫秒。
一定要設定逾時時間,不能無限等待下去,不然和正常的請求就一樣了。。
Servlet方式實現非同步請求
前面已經提到,可通過HttpServletRequest對象中獲得一個AsyncContext對象,該對象構成了非同步處理的上下文。所以,我們來實際操作下。
0.編寫一個簡單控制層
/** * 使用servlet方式進行非同步請求 * @author oKong * */@Slf4j@RestControllerpublic 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-12018-08-15 23:03:04.183 INFO 6732 --- [nio-8080-exec-2] c.l.l.s.controller.ServletController : 內部線程:http-nio-8080-exec-22018-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-22018-08-15 23:32:22.323 INFO 15740 --- [ MvcAsync1] c.l.l.s.controller.SpringController : 內部線程:MvcAsync1
逾時、自訂線程設定
從控制台可以看見,非同步響應的線程使用的是名為:MvcAsync1的線程。第一次再訪問時,就是MvcAsync2了。若採用預設設定,會無限的建立新線程去處理非同步請求,所以正常都需要配置一個線程池及逾時時間。
編寫一個配置類:CustomAsyncPool.java
@Configurationpublic 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-12018-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-22018-08-15 23:52:27.843 INFO 12984 --- [pool-1-thread-1] c.l.l.s.controller.SpringController : 內部線程:pool-1-thread-12018-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-12018-08-15 23:55:02.587 INFO 2864 --- [ oKong-1] c.l.l.s.controller.SpringController : 內部線程:oKong-12018-08-15 23:55:02.615 INFO 2864 --- [nio-8080-exec-2] c.l.l.s.controller.SpringController : WebAsyncTask執行結束
參考資料
- 52266905
- docs.spring.io/spring/docs/4.3.18.RELEASE/spring-framework-reference/htmlsingle/#mvc-ann-async
總結
本章節主要是講解了非同步請求的使用及相關配置,如逾時,異常等處理。設定非同步請求時,記得不要忘記設定逾時時間。非同步請求只是提高了服務的輸送量,提高單位時間內處理的請求數,並不會加快處理效率的,這點需要注意。。下一章節,講講使用@Async進行非同步呼叫相關知識。
最後
目前互連網上很多大佬都有SpringBoot系列教程,如有雷同,請多多包涵了。本文是作者在電腦前一字一句敲的,每一步都是自己實踐的。若文中有所錯誤之處,還望提出,謝謝。
老生常談
- 個人QQ:
499452441
- 公眾號:
lqdevOps
個人部落格:http://blog.lqdev.cn
完整樣本:github.com/xie19900123/spring-boot-learning/tree/master/chapter-20
原文地址:http://