DeferredResult
高級使用上篇博文介紹的它的基本使用,那麼本文主要結合一些特殊的使用場景,來介紹下它的高級使用,讓能更深入的理解DeferredResult
的強大之處。javascript
它的優勢也是很是明顯的,可以實現兩個徹底不相干的線程間的通訊。
處理的時候請注意圖中標記的線程安全問題~~~
java
在WebSocket
協議以前(它是2011年發佈的),有三種實現雙向通訊的方式:輪詢(polling)、長輪詢(long-polling)和iframe流(streaming)。web
隱藏的iframe
,利用其src屬性在服務器和客戶端之間建立一條長鏈接,服務器向iframe傳輸數據(一般是HTML,內有負責插入信息的javascript),來實時更新頁面。(我的以爲還不如長輪詢呢。。。)瀏覽器支持程度不一致
,不支持斷開重連 (實際上是最推薦的~~~)以前看apollo配置中心
的實現原理,apollo的發佈配置推送變動消息就是用DeferredResult
實現的。它的大概實現步驟以下:ajax
長輪詢http請求
,超時時間60秒繼續這個步驟重複發起請求
DeferredResult.setResult
返回200狀態碼。客戶端收到響應結果後,會發起請求獲取變動後的配置信息(注意這裏是另一個請求哦~)。爲了演示,簡單的按照此方式,寫一個Demo:編程
@Configuration
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer { @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { // 超時時間設置爲60s configurer.setDefaultTimeout(TimeUnit.SECONDS.toMillis(60)); } }
服務端簡單代碼模擬以下:瀏覽器
@Slf4j
@RestController
public class ApolloController { // 值爲List,由於監視同一個名稱空間的長輪詢可能有N個(畢竟可能有多個客戶端用同一份配置嘛) private Map<String, List<DeferredResult<String>>> watchRequests = new ConcurrentHashMap<>(); @GetMapping(value = "/all/watchrequests") public Object getWatchRequests() { return watchRequests; } // 模擬長輪詢:apollo客戶端來監聽配置文件的變動~ 能夠指定namespace 監視指定的NameSpace @GetMapping(value = "/watch/{namespace}") public DeferredResult<String> watch(@PathVariable("namespace") String namespace) { log.info("Request received,namespace is" + namespace + ",當前時間:" + System.currentTimeMillis()); DeferredResult<String> deferredResult = new DeferredResult<>(); //當deferredResult完成時(不管是超時仍是異常仍是正常完成),都應該移除watchRequests中相應的watch key deferredResult.onCompletion(() -> { log.info("onCompletion,移除對namespace:" + namespace + "的監視~"); List<DeferredResult<String>> list = watchRequests.get(namespace); list.remove(deferredResult); if (list.isEmpty()) { watchRequests.remove(namespace); } }); List<DeferredResult<String>> list = watchRequests.computeIfAbsent(namespace, (k) -> new ArrayList<>()); list.add(deferredResult); return deferredResult; } //模擬發佈namespace配置:修改配置 @GetMapping(value = "/publish/{namespace}") public void publishConfig(@PathVariable("namespace") String namespace) { //do Something for update config if (watchRequests.containsKey(namespace)) { List<DeferredResult<String>> deferredResults = watchRequests.get(namespace); //通知全部watch這個namespace變動的長輪訓配置變動結果 for (DeferredResult<String> deferredResult : deferredResults) { deferredResult.setResult(namespace + " changed,時間爲" + System.currentTimeMillis()); } } } }
apollo處理超時時候會拋出一個異常AsyncRequestTimeoutException
,所以咱們全局處理一下就成:安全
@Slf4j
@ControllerAdvice
class GlobalControllerExceptionHandler { @ResponseStatus(HttpStatus.NOT_MODIFIED)//返回304狀態碼 效果同HttpServletResponse#sendError(int) 但這樣更優雅 @ResponseBody @ExceptionHandler(AsyncRequestTimeoutException.class) //捕獲特定異常 public void handleAsyncRequestTimeoutException(AsyncRequestTimeoutException e) { System.out.println("handleAsyncRequestTimeoutException"); } }
用Ajax
模擬Client端的僞代碼以下:服務器
//長輪詢:一直去監聽指定namespace的配置文件
function watchConfig(){ $.ajax({ url:"http://localhost:8080/demo_war/watch/classroomconfig", method:"get", success:function(response,status){ if(status == 304){ watchConfig(); //超時,沒有更改,那就繼續去監聽 }else if(status == 200){ getNewConfig(); //監聽到更改後,立馬去獲取最新的配置文件內容回來作事 ... watchConfig(); // 昨晚過後又去監聽着 } } }); } // 調用去監聽獲取配置文件的函數 watchConfig();
這樣子咱們就基本模擬了一個長輪詢
的案例~網絡
長輪詢的應用場景也是不少的,好比咱們如今要實現這樣一個功能:瀏覽器要實時展現服務端計算出來的數據。(這個用普通輪詢就會有延遲且浪費資源,可是用這種相似長鏈接
的方案就很合適)mvc
Callback
和DeferredResult
用於設置單個結果,若是有多個結果須要set返回給客戶端時,可使用SseEmitter以及ResponseBodyEmitter
,each object is written with a compatible HttpMessageConverter
。返回值能夠直接寫他們自己,也能夠放在ResponseEntity
裏面
它倆都是Spring4.2以後提供的類。由
ResponseBodyEmitterReturnValueHandler
負責處理。 這個和Spring5提供的webFlux技術已經很像了,後續講到的時候還會提到他們~~~~ Emitter:發射器
它們的使用方式幾乎同:DeferredResult
,這裏我只把官方的例子拿出來你就懂了
SseEmitter
是ResponseBodyEmitter
的子類,它提供Server-Sent Events(Sse)
.服務器事件發送是」HTTP Streaming」的另外一個變種技術.只是從服務器發送的事件按照W3C Server-Sent Events
規範來的(推薦使用) 它的使用方式上,徹底同上
Server-Sent Events
這個規範可以來用於它們的預期使用目的:就是從server發送events到clients(服務器推).在Spring MVC中能夠很容易的實現.僅僅須要返回一個SseEmitter
類型的值.
向這種場景在在線遊戲、在線協做、金融領域等等都有很好的應用。固然,若是你對穩定性什麼的要求都很是高,官方也推薦最好是使用
WebSocket
來實現~
ResponseBodyEmitter
容許經過HttpMessageConverter
把發送的events寫到對象到response中.這多是最多見的狀況。例如寫JSON數據 但是有時候它被用來繞開message轉換直接寫入到response的OutputStream。例如文件下載.這樣能夠經過返回StreamingResponseBody
類型的值作到.
它用於直接將結果寫出到Response的OutputStream
中; 如文件下載等
接口源碼很是簡單:
@FunctionalInterface
public interface StreamingResponseBody { void writeTo(OutputStream outputStream) throws IOException; }
Spring內部默認不使用線程池處理的(經過源碼分析後面咱們是能看到的),爲了提升處理的效率,咱們能夠本身優化,建議本身在配置裏注入一個線程池供給使用,參考以下:
// 提供一個mvc裏專用的線程池。。。 這是全局的方式~~~~
@Bean
public ThreadPoolTaskExecutor mvcTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setQueueCapacity(100); executor.setMaxPoolSize(25); return executor; } // 最優解決方案不是像上面同樣配置通用的,而是配置一個單獨的專用的,以下~~~~ @Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { // 配置異步支持~~~~ @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { // 設置一個用於異步執行的執行器~~~AsyncTaskExecutor configurer.setTaskExecutor(mvcTaskExecutor()); configurer.setDefaultTimeout(60000L); } }
總的來講,Spring MVC提供的便捷的異步支持,可以大大的提升Tomcat容器等的性能。同時也給咱們的應用提供了更多的便利。這也爲Spring5之後的Reactive編程模型提供了有利的支持和保障。 Spring是一個易學難精的技術,想要把各類技術融匯貫通,還有後續更紮實的深挖~