關於
web開發
的相關知識點,後續有補充時再開續寫了。好比webService
服務、發郵件
等,這些通常上以爲不徹底屬於web開發
方面的,並且目前webService
做爲一個接口來提供服務的機會應該比較小了吧。因此本章節開始,開始講解關於異步開發過程當中會使用到的一些知識點。本章節就來說解下異步請求相關知識點。html
在Servlet 3.0
以前,Servlet
採用Thread-Per-Request
的方式處理請求,即每一次Http
請求都由某一個線程從頭至尾負責處理。若是一個請求須要進行IO操做,好比訪問數據庫、調用第三方服務接口等,那麼其所對應的線程將同步地等待IO操做完成, 而IO操做是很是慢的,因此此時的線程並不能及時地釋放回線程池以供後續使用,在併發量愈來愈大的狀況下,這將帶來嚴重的性能問題。其請求流程大體爲:前端
而在Servlet3.0
發佈後,提供了一個新特性:異步處理請求。能夠先釋放容器分配給請求的線程與相關資源,減輕系統負擔,釋放了容器所分配線程的請求,其響應將被延後,能夠在耗時處理完成(例如長時間的運算)時再對客戶端進行響應。其請求流程爲:java
在Servlet 3.0
後,咱們能夠從HttpServletRequest
對象中得到一個**AsyncContext
**對象,該對象構成了異步處理的上下文,Request
和Response
對象均可從中獲取。AsyncContext
能夠從當前線程傳給另外的線程,並在新的線程中完成對請求的處理並返回結果給客戶端,初始線程即可以還回給容器線程池以處理更多的請求。如此,經過將請求從一個線程傳給另外一個線程處理的過程便構成了Servlet 3.0
中的異步處理。git
多說幾句:github
隨着Spring5
發佈,提供了一個響應式Web框架:Spring WebFlux
。以後可能就不須要Servlet
容器的支持了。如下是其前後對比圖:web
左側是傳統的基於Servlet
的Spring Web MVC
框架,右側是5.0版本新引入的基於Reactive Streams
的Spring WebFlux
框架,從上到下依次是Router Functions,WebFlux,Reactive Streams三個新組件。spring
對於其發展前景仍是拭目以待吧。有時間也該去了解下Spring5
了。數據庫
在編寫實際代碼以前,咱們來了解下一些關於異步請求的api的調用說明。api
HttpServletRequest
對象獲取。AsyncContext asyncContext = request.startAsync();
其監聽器的接口代碼:springboot
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; }
說明:
通常上,咱們在超時或者異常時,會返回給前端相應的提示,好比說超時了,請再次請求等等,根據各業務進行自定義返回。同時,在異步調用完成時,通常須要執行一些清理工做或者其餘相關操做。
須要注意的是隻有在調用request.startAsync
前將監聽器添加到AsyncContext
,監聽器的onStartAsync
方法纔會起做用,而調用startAsync
前AsyncContext
還不存在,因此第一次調用startAsync
是不會被監聽器中的onStartAsync
方法捕獲的,只有在超時後又從新開始的狀況下onStartAsync
方法纔會起做用。
setTimeout
方法設置,單位:毫秒。必定要設置超時時間,不能無限等待下去,否則和正常的請求就同樣了。。
前面已經提到,可經過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
中,有多種方式實現異步請求,好比callable
、DeferredResult
或者WebAsyncTask
。每一個的用法略有不一樣,可根據不一樣的業務場景選擇不一樣的方式。如下主要介紹一些經常使用的用法
使用很簡單,直接返回的參數包裹一層
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
相比於
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
是直接返回了。以爲就是寫法不一樣而已,更多細節但願大神解答!
@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執行結束
本章節主要是講解了
異步請求
的使用及相關配置,如超時,異常等處理。設置異步請求時,記得不要忘記設置超時時間。異步請求
只是提升了服務的吞吐量,提升單位時間內處理的請求數,並不會加快處理效率的,這點須要注意。。下一章節,講講使用@Async
進行異步調用相關知識。
目前互聯網上不少大佬都有
SpringBoot
系列教程,若有雷同,請多多包涵了。本文是做者在電腦前一字一句敲的,每一步都是本身實踐的。若文中有所錯誤之處,還望提出,謝謝。
499452441
lqdevOps
我的博客:http://blog.lqdev.cn
完整示例:https://github.com/xie19900123/spring-boot-learning/tree/master/chapter-20
原文地址:http://blog.lqdev.cn/2018/08/16/springboot/chapter-twenty/