(點選上方公眾號,可快速關註)
來源: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;
}
說明:
-
onStartAsync:非同步執行緒開始時呼叫
-
onError:非同步執行緒出錯時呼叫
-
onTimeout:非同步執行緒執行超時呼叫
-
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執行結束
參考資料
-
https://blog.csdn.net/paincupid/article/details/52266905
-
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技能