在 Servlet3.0 就引入了異步請求的支持,可是在實際的業務開發中,可能用過這個特性的童鞋並很少?前端
本篇博文做爲異步請求的掃盲和使用教程,將包含如下知識點java
<!-- more -->git
異步對於咱們而言,應該屬於常常能夠聽到的詞彙了,在實際的開發中多多少少都會用到,那麼什麼是異步請求呢github
先介紹一下同步與異步:web
一個正常調用,吭哧吭哧執行完畢以後直接返回,這個叫同步;spring
接收到調用,本身不幹,新開一個線程來作,主線程本身則去幹其餘的事情,等後臺線程吭哧吭哧的跑完以後,主線程再返回結果,這個就叫異步後端
異步請求:websocket
咱們這裏講到的異步請求,主要是針對 web 請求而言,後端響應請求的一種手段,同步/異步對於前端而言是無感知、無區別的網絡
同步請求,後端接收到請求以後,直接在處理請求線程中,執行業務邏輯,並返回
異步請求,後端接收到請求以後,新開一個線程,來執行業務邏輯,釋放請求線程,避免請求線程被大量耗時的請求沾滿,致使服務不可用
經過上面兩張圖,能夠知道異步請求的最主要特色
從特色出發,也能夠很容易看出異步請求,更適用於耗時的請求,快速的釋放請求處理線程,避免 web 容器的請求線程被打滿,致使服務不可用
舉一個稍微極端一點的例子,好比我之前作過的一個多媒體服務,提供圖片、音視頻的編輯,這些服務接口有同步返回結果的也有異步返回結果的;同步返回結果的接口有快有慢,大部分耗時可能<10ms
,而有部分接口耗時則在幾十甚至上百
這種場景下,耗時的接口就能夠考慮用異步請求的方式來支持了,避免佔用過多的請求處理線程,影響其餘的服務
接下來介紹四種異步請求的使用姿式,原理一致,只是使用的場景稍有不一樣
在 Servlet3.0+以後就支持了異步請求,第一種方式比較原始,至關於直接藉助 Servlet 的規範來實現,固然下面的 case 並非直接建立一個 servlet,而是藉助AsyncContext
來實現
@RestController @RequestMapping(path = "servlet") public class ServletRest { @GetMapping(path = "get") public void get(HttpServletRequest request) { AsyncContext asyncContext = request.startAsync(); asyncContext.addListener(new AsyncListener() { @Override public void onComplete(AsyncEvent asyncEvent) throws IOException { System.out.println("操做完成:" + Thread.currentThread().getName()); } @Override public void onTimeout(AsyncEvent asyncEvent) throws IOException { System.out.println("超時返回!!!"); asyncContext.getResponse().setCharacterEncoding("utf-8"); asyncContext.getResponse().setContentType("text/html;charset=UTF-8"); asyncContext.getResponse().getWriter().println("超時了!!!!"); } @Override public void onError(AsyncEvent asyncEvent) throws IOException { System.out.println("出現了m某些異常"); asyncEvent.getThrowable().printStackTrace(); asyncContext.getResponse().setCharacterEncoding("utf-8"); asyncContext.getResponse().setContentType("text/html;charset=UTF-8"); asyncContext.getResponse().getWriter().println("出現了某些異常哦!!!!"); } @Override public void onStartAsync(AsyncEvent asyncEvent) throws IOException { System.out.println("開始執行"); } }); asyncContext.setTimeout(3000L); asyncContext.start(new Runnable() { @Override public void run() { try { Thread.sleep(Long.parseLong(request.getParameter("sleep"))); System.out.println("內部線程:" + Thread.currentThread().getName()); asyncContext.getResponse().setCharacterEncoding("utf-8"); asyncContext.getResponse().setContentType("text/html;charset=UTF-8"); asyncContext.getResponse().getWriter().println("異步返回!"); asyncContext.getResponse().getWriter().flush(); // 異步完成,釋放 asyncContext.complete(); } catch (Exception e) { e.printStackTrace(); } } }); System.out.println("主線程over!!! " + Thread.currentThread().getName()); } }
完整的實現如上,簡單的來看一下通常步驟
javax.servlet.ServletRequest#startAsync()
獲取AsyncContext
asyncContext.addListener(AsyncListener)
(這個是可選的)
asyncContext.setTimeout(3000L)
(可選)asyncContext.start(Runnable)
相比較於上面的複雜的示例,SpringMVC 能夠很是 easy 的實現,直接返回一個Callable
便可
@RestController @RequestMapping(path = "call") public class CallableRest { @GetMapping(path = "get") public Callable<String> get() { Callable<String> callable = new Callable<String>() { @Override public String call() throws Exception { System.out.println("do some thing"); Thread.sleep(1000); System.out.println("執行完畢,返回!!!"); return "over!"; } }; return callable; } @GetMapping(path = "exception") public Callable<String> exception() { Callable<String> callable = new Callable<String>() { @Override public String call() throws Exception { System.out.println("do some thing"); Thread.sleep(1000); System.out.println("出現異常,返回!!!"); throw new RuntimeException("some error!"); } }; return callable; } }
請注意上面的兩種 case,一個正常返回,一個業務執行過程當中,拋出來異常
分別請求,輸出以下
# http://localhost:8080/call/get do some thing 執行完畢,返回!!!
異常請求: http://localhost:8080/call/exception
do some thing 出現異常,返回!!! 2020-03-29 16:12:06.014 ERROR 24084 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] threw exception java.lang.RuntimeException: some error! at com.git.hui.boot.async.rest.CallableRest$2.call(CallableRest.java:40) ~[classes/:na] at com.git.hui.boot.async.rest.CallableRest$2.call(CallableRest.java:34) ~[classes/:na] at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:328) ~[spring-web-5.2.1.RELEASE.jar:5.2.1.RELEASE] at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_171] at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266) ~[na:1.8.0_171] at java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:1.8.0_171] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_171] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_171] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_171]
callable 的方式,很是直觀簡單,可是咱們常常關注的超時+異常的處理卻不太好,這個時候咱們能夠用WebAsyncTask
,實現姿式也很簡單,包裝一下callable
,而後設置各類回調事件便可
@RestController @RequestMapping(path = "task") public class WebAysncTaskRest { @GetMapping(path = "get") public WebAsyncTask<String> get(long sleep, boolean error) { Callable<String> callable = () -> { System.out.println("do some thing"); Thread.sleep(sleep); if (error) { System.out.println("出現異常,返回!!!"); throw new RuntimeException("異常了!!!"); } return "hello world"; }; // 指定3s的超時 WebAsyncTask<String> webTask = new WebAsyncTask<>(3000, callable); webTask.onCompletion(() -> System.out.println("over!!!")); webTask.onTimeout(() -> { System.out.println("超時了"); return "超時返回!!!"; }); webTask.onError(() -> { System.out.println("出現異常了!!!"); return "異常返回"; }); return webTask; } }
DeferredResult
與WebAsyncTask
最大的區別就是前者不肯定何時會返回結果,
DeferredResult
的這個特色,能夠用來作實現不少有意思的東西,如後面將介紹的SseEmitter
就用到了它
下面給出一個實例
@RestController @RequestMapping(path = "defer") public class DeferredResultRest { private Map<String, DeferredResult> cache = new ConcurrentHashMap<>(); @GetMapping(path = "get") public DeferredResult<String> get(String id) { DeferredResult<String> res = new DeferredResult<>(); cache.put(id, res); res.onCompletion(new Runnable() { @Override public void run() { System.out.println("over!"); } }); return res; } @GetMapping(path = "pub") public String publish(String id, String content) { DeferredResult<String> res = cache.get(id); if (res == null) { return "no consumer!"; } res.setResult(content); return "over!"; } }
在上面的實例中,用戶若是先訪問http://localhost:8080/defer/get?id=yihuihui
,不會立馬有結果,直到用戶再次訪問http://localhost:8080/defer/pub?id=yihuihui&content=哈哈
時,前面的請求才會有結果返回
那麼這個能夠設置超時麼,若是一直把前端掛住,貌似也不太合適吧
new DeferredResult<>(3000L)
@Configuration @EnableWebMvc public class WebConf implements WebMvcConfigurer { @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { // 超時時間設置爲60s configurer.setDefaultTimeout(TimeUnit.SECONDS.toMillis(10)); } }
相關博文
系列博文
源碼
盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛