Spring MVC的異步模式(ResponseBodyEmitter、SseEmitter、StreamingResponseBody) 高級使用篇

DeferredResult高級使用

上篇博文介紹的它的基本使用,那麼本文主要結合一些特殊的使用場景,來介紹下它的高級使用,讓能更深入的理解DeferredResult的強大之處。javascript

它的優勢也是很是明顯的,可以實現兩個徹底不相干的線程間的通訊。處理的時候請注意圖中標記的線程安全問題~~~java

 

 

實現長輪詢服務端推送消息(long polling)

簡單科普雙向通訊的方式

WebSocket協議以前(它是2011年發佈的),有三種實現雙向通訊的方式:輪詢(polling)長輪詢(long-polling)和iframe流(streaming)web

  • 輪詢(polling):這個不解釋了。優勢是實現簡單粗暴,後臺處理簡單。缺點也是大大的,耗流量、耗CPU。。。
  • 長輪詢(long-polling):長輪詢是對輪詢的改進版。客戶端發送HTTP給服務器以後,看有沒有新消息,若是沒有新消息,就一直等待(而不是一直去請求了)。當有新消息的時候,纔會返回給客戶端。 優勢是對輪詢作了優化,時效性也較好。缺點是:保持鏈接會消耗資源; 服務器沒有返回有效數據,程序超時~~~
  • iframe流(streaming):是在頁面中插入一個隱藏的iframe,利用其src屬性在服務器和客戶端之間建立一條長鏈接,服務器向iframe傳輸數據(一般是HTML,內有負責插入信息的javascript),來實時更新頁面。(我的以爲還不如長輪詢呢。。。)
  • WebSocket:WebSocket協議是基於TCP的一種新的網絡協議。它實現了瀏覽器與服務器全雙工(full-duplex)通訊——容許服務器主動發送信息給客戶端。它將TCP的Socket(套接字)應用在了webpage上。 它的有點一大把:支持雙向通訊,實時性更強;可發送二進制文件;很是節省流量。 但也是有缺點的:瀏覽器支持程度不一致,不支持斷開重連 (實際上是最推薦的~~~)

以前看apollo配置中心的實現原理,apollo的發佈配置推送變動消息就是用DeferredResult實現的。它的大概實現步驟以下:ajax

  1. apollo客戶端會像服務端發送長輪詢http請求,超時時間60秒
  2. 當超時後返回客戶端一個304 httpstatus,代表配置沒有變動,客戶端繼續這個步驟重複發起請求
  3. 當有發佈配置的時候,服務端會調用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

ResponseBodyEmitter和SseEmitter

CallbackDeferredResult用於設置單個結果,若是有多個結果須要set返回給客戶端時,可使用SseEmitter以及ResponseBodyEmitter,each object is written with a compatible HttpMessageConverter。返回值能夠直接寫他們自己,也能夠放在ResponseEntity裏面

它倆都是Spring4.2以後提供的類。由ResponseBodyEmitterReturnValueHandler負責處理。 這個和Spring5提供的webFlux技術已經很像了,後續講到的時候還會提到他們~~~~ Emitter:發射器

它們的使用方式幾乎同:DeferredResult,這裏我只把官方的例子拿出來你就懂了

 

 

SseEmitterResponseBodyEmitter的子類,它提供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類型的值作到.

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是一個易學難精的技術,想要把各類技術融匯貫通,還有後續更紮實的深挖~

相關文章
相關標籤/搜索