前言
關於
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<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執行結束
參考資料
- 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
系列教程,如有雷同,請多多包涵了。本文是作者在電腦前一字一句敲的,每一步都是自己實踐的。若文中有所錯誤之處,還望提出,謝謝。
老生常談
- 個人QQ:
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/