Spring MVC 處理異步請求

2018-07-26 14:04 更新

Spring MVC 3.2開始引入了基于Servlet 3的異步請求處理。相比以前,控制器方法已經(jīng)不一定需要返回一個值,而是可以返回一個java.util.concurrent.Callable的對象,并通過Spring MVC所管理的線程來產(chǎn)生返回值。與此同時,Servlet容器的主線程則可以退出并釋放其資源了,同時也允許容器去處理其他的請求。通過一個TaskExecutor,Spring MVC可以在另外的線程中調(diào)用Callable。當(dāng)Callable返回時,請求再攜帶Callable返回的值,再次被分配到Servlet容器中恢復(fù)處理流程。以下代碼給出了一個這樣的控制器方法作為例子:

@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {

    return new Callable<String>() {
        public String call() throws Exception {
            // ...
            return "someView";
        }
    };

}

另一個選擇,是讓控制器方法返回一個DeferredResult的實(shí)例。這種場景下,返回值可以由任何一個線程產(chǎn)生,也包括那些不是由Spring MVC管理的線程。舉個例子,返回值可能是為了響應(yīng)某些外部事件所產(chǎn)生的,比如一條JMS的消息,一個計(jì)劃任務(wù),等等。以下代碼給出了一個這樣的控制器作為例子:

@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<String>();
    // Save the deferredResult somewhere..
    return deferredResult;
}

// In some other thread...
deferredResult.setResult(data);

如果對Servlet 3.0的異步請求處理特性沒有了解,理解這個特性可能會有點(diǎn)困難。因此,閱讀一下前者的文檔將會很有幫助。以下給出了這個機(jī)制運(yùn)作背后的一些原理:

  • 一個servlet請求ServletRequest可以通過調(diào)用request.startAsync()方法而進(jìn)入異步模式。這樣做的主要結(jié)果就是該servlet以及所有的過濾器都可以結(jié)束,但其響應(yīng)(response)會留待異步處理結(jié)束后再返回
  • 調(diào)用request.startAsync()方法會返回一個AsyncContext對象,可用它對異步處理進(jìn)行進(jìn)一步的控制和操作。比如說它也提供了一個與轉(zhuǎn)向(forward)很相似的dispatch方法,只不過它允許應(yīng)用恢復(fù)Servlet容器的請求處理進(jìn)程
  • ServletRequest提供了獲取當(dāng)前DispatherType的方式,后者可以用來區(qū)別當(dāng)前處理的是原始請求、異步分發(fā)請求、轉(zhuǎn)向,或是其他類型的請求分發(fā)類型。

有了上面的知識,下面可以來看一下Callable的異步請求被處理時所依次發(fā)生的事件:

  • 控制器先返回一個Callable對象
  • Spring MVC開始進(jìn)行異步處理,并把該Callable對象提交給另一個獨(dú)立線程的執(zhí)行器TaskExecutor處理
  • DispatcherServlet和所有過濾器都退出Servlet容器線程,但此時方法的響應(yīng)對象仍未返回
  • Callable對象最終產(chǎn)生一個返回結(jié)果,此時Spring MVC會重新把請求分派回Servlet容器,恢復(fù)處理
  • DispatcherServlet再次被調(diào)用,恢復(fù)對Callable異步處理所返回結(jié)果的處理

DeferredResult異步請求的處理順序也非常類似,區(qū)別僅在于應(yīng)用可以通過任何線程來計(jì)算返回一個結(jié)果:

  • 控制器先返回一個DeferredResult對象,并把它存取在內(nèi)存(隊(duì)列或列表等)中以便存取
  • Spring MVC開始進(jìn)行異步處理
  • DispatcherServlet和所有過濾器都退出Servlet容器線程,但此時方法的響應(yīng)對象仍未返回
  • 由處理該請求的線程對 DeferredResult進(jìn)行設(shè)值,然后Spring MVC會重新把請求分派回Servlet容器,恢復(fù)處理
  • DispatcherServlet再次被調(diào)用,恢復(fù)對該異步返回結(jié)果的處理

關(guān)于引入異步請求處理的背景和原因,以及什么時候使用它、為什么使用異步請求處理等問題,你可以從這個系列的博客中了解更多信息。

異步請求的異常處理

若控制器返回的Callable在執(zhí)行過程中拋出了異常,又會發(fā)生什么事情?簡單來說,這與一般的控制器方法拋出異常是一樣的。它會被正常的異常處理流程捕獲處理。更具體地說呢,當(dāng)Callable拋出異常時,Spring MVC會把一個Exception對象分派給Servlet容器進(jìn)行處理,而不是正常返回方法的返回值,然后容器恢復(fù)對此異步請求異常的處理。若方法返回的是一個DeferredResult對象,你可以選擇調(diào)Exception實(shí)例的setResult方法還是setErrorResult方法。

攔截異步請求

處理器攔截器HandlerInterceptor可以實(shí)現(xiàn)AsyncHandlerInterceptor接口攔截異步請求,因?yàn)樵诋惒秸埱箝_始時,被調(diào)用的回調(diào)方法是該接口的afterConcurrentHandlingStarted方法,而非一般的postHandleafterCompletion方法。

如果需要與異步請求處理的生命流程有更深入的集成,比如需要處理timeout的事件等,則HandlerInterceptor需要注冊一個CallableProcessingInterceptorDeferredResultProcessingInterceptor攔截器。具體的細(xì)節(jié)可以參考AsyncHandlerInterceptor類的Java文檔。

DeferredResult類還提供了onTimeout(Runnable)onCompletion(Runnable)等方法,具體的細(xì)節(jié)可以參考DeferredResult類的Java文檔。

Callable需要請求過期(timeout)和完成后的攔截時,可以把它包裝在一個WebAsyncTask實(shí)例中,后者提供了相關(guān)的支持。

HTTP streaming(不知道怎么翻)

如前所述,控制器可以使用DeferredResultCallable對象來異步地計(jì)算其返回值,這可以用于實(shí)現(xiàn)一些有用的技術(shù),比如 long polling技術(shù),讓服務(wù)器可以盡可能快地向客戶端推送事件。

如果你想在一個HTTP響應(yīng)中同時推送多個事件,怎么辦?這樣的技術(shù)已經(jīng)存在,與"Long Polling"相關(guān),叫"HTTP Streaming"。Spring MVC支持這項(xiàng)技術(shù),你可以通過讓方法返回一個ResponseBodyEmitter類型對象來實(shí)現(xiàn),該對象可被用于發(fā)送多個對象。通常我們所使用的@ResponseBody只能返回一個對象,它是通過HttpMessageConverter寫到響應(yīng)體中的。

下面是一個實(shí)現(xiàn)該技術(shù)的例子:

@RequestMapping("/events")
public ResponseBodyEmitter handle() {
    ResponseBodyEmitter emitter = new ResponseBodyEmitter();
    // Save the emitter somewhere..
    return emitter;
}

// In some other thread
emitter.send("Hello once");

// and again later on
emitter.send("Hello again");

// and done at some point
emitter.complete();

ResponseBodyEmitter也可以被放到ResponseEntity體里面使用,這可以對響應(yīng)狀態(tài)和響應(yīng)頭做一些定制。

Note that ResponseBodyEmitter can also be used as the body in a ResponseEntity in order to customize the status and headers of the response.

使用“服務(wù)器端事件推送”的HTTP Streaming

SseEmitterResponseBodyEmitter的一個子類,提供了對服務(wù)器端事件推送的技術(shù)的支持。服務(wù)器端事件推送其實(shí)只是一種HTTP Streaming的類似實(shí)現(xiàn),只不過它服務(wù)器端所推送的事件遵循了W3C Server-Sent Events規(guī)范中定義的事件格式。

“服務(wù)器端事件推送”技術(shù)正如其名,是用于由服務(wù)器端向客戶端進(jìn)行的事件推送。這在Spring MVC中很容易做到,只需要方法返回一個SseEmitter類型的對象即可。

需要注意的是,Internet Explorer并不支持這項(xiàng)服務(wù)器端事件推送的技術(shù)。另外,對于更大型的web應(yīng)用及更精致的消息傳輸場景——比如在線游戲、在線協(xié)作、金融應(yīng)用等——來說,使用Spring的WebSocket(包含SockJS風(fēng)格的實(shí)時WebSocket)更成熟一些,因?yàn)樗С值臑g覽器范圍非常廣(包括IE),并且,對于一個以消息為中心的架構(gòu)中,它為服務(wù)器端-客戶端間的事件發(fā)布-訂閱模型的交互提供了更高層級的消息模式(messaging patterns)的支持。

直接寫回輸出流OutputStream的HTTP Streaming

ResponseBodyEmitter也允許通過HttpMessageConverter向響應(yīng)體中支持寫事件對象。這可能是最常見的情形,比如寫返回的JSON數(shù)據(jù)的時候。但有時,跳過消息轉(zhuǎn)換的階段,直接把數(shù)據(jù)寫回響應(yīng)的輸出流OutputStream可能更有效,比如文件下載這樣的場景。這可以通過返回一個StreamingResponseBody類型的對象來實(shí)現(xiàn)。

以下是一個實(shí)現(xiàn)的例子:

@RequestMapping("/download")
public StreamingResponseBody handle() {
    return new StreamingResponseBody() {
        @Override
        public void writeTo(OutputStream outputStream) throws IOException {
            // write...
        }
    };
}

ResponseBodyEmitter也可以被放到ResponseEntity體里面使用,這可以對響應(yīng)狀態(tài)和響應(yīng)頭做一些定制。

異步請求處理的相關(guān)配置

Servlet容器配置

對于那些使用web.xml配置文件的應(yīng)用,請確保web.xml的版本更新到3.0:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance http://java.sun.com/xml/ns/javaee
                    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">

    ...

</web-app>

異步請求必須在web.xmlDispatcherServlet下的子元素<async-supported>true</async-supported>設(shè)置為true。此外,所有可能參與異步請求處理的過濾器Filter都必須配置為支持ASYNC類型的請求分派。在Spring框架中為過濾器啟用支持ASYNC類型的請求分派應(yīng)是安全的,因?yàn)檫@些過濾器一般都繼承了基類OncePerRequestFilter,后者在運(yùn)行時會檢查該過濾器是否需要參與到異步分派的請求處理中。

以下是一個例子,展示了web.xml的配置:

    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
                http://java.sun.com/xml/ns/javaee
                http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
        version="3.0">

        <filter>
            <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
            <filter-class>org.springframework.~.OpenEntityManagerInViewFilter</filter-class>
            <async-supported>true</async-supported>
        </filter>

        <filter-mapping>
            <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
            <url-pattern>/*</url-pattern>
            <dispatcher>REQUEST</dispatcher>
            <dispatcher>ASYNC</dispatcher>
        </filter-mapping>

    </web-app>

如果應(yīng)用使用的是Servlet 3規(guī)范基于Java編程的配置方式,比如通過WebApplicationInitializer,那么你也需要設(shè)置"asyncSupported"標(biāo)志和ASYNC分派類型的支持,就像你在web.xml中所配置的一樣。你可以考慮直接繼承AbstractDispatcherServletInitializerAbstractAnnotationConfigDispatcherServletInitializer來簡化配置,它們都自動地為你設(shè)置了這些配置項(xiàng),并使得注冊Filter過濾器實(shí)例變得非常簡單。

Spring MVC配置

MVC Java編程配置和MVC命名空間配置方式都提供了配置異步請求處理支持的選擇。WebMvcConfigurer提供了configureAsyncSupport方法,而<mvc:annotation-driven>有一個子元素<async-support>,它們都用以為此提供支持。

這些配置允許你覆寫異步請求默認(rèn)的超時時間,在未顯式設(shè)置時,它們的值與所依賴的Servlet容器是相關(guān)的(比如,Tomcat設(shè)置的超時時間是10秒)。你也可以配置用于執(zhí)行控制器返回值Callable的執(zhí)行器AsyncTaskExecutor。Spring強(qiáng)烈推薦你配置這個選項(xiàng),因?yàn)镾pring MVC默認(rèn)使用的是普通的執(zhí)行器SimpleAsyncTaskExecutor。MVC Java編程配置及MVC命名空間配置的方式都允許你注冊自己的CallableProcessingInterceptorDeferredResultProcessingInterceptor攔截器實(shí)例。

若你需要為特定的DeferredResult覆寫默認(rèn)的超時時間,你可以選用合適的構(gòu)造方法來實(shí)現(xiàn)。類似,對于Callable返回,你可以把它包裝在一個WebAsyncTask對象中,并使用合適的構(gòu)造方法定義超時時間。WebAsyncTask類的構(gòu)造方法同時也能接受一個任務(wù)執(zhí)行器AsyncTaskExecutor類型的參數(shù)。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號