Spring MVC 處理異步請(qǐng)求

2018-07-26 14:04 更新

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

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

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

}

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

@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);

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

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

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

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

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

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

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

異步請(qǐng)求的異常處理

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

攔截異步請(qǐng)求

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

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

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

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

HTTP streaming(不知道怎么翻)

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

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

下面是一個(gè)實(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體里面使用,這可以對(duì)響應(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的一個(gè)子類,提供了對(duì)服務(wù)器端事件推送的技術(shù)的支持。服務(wù)器端事件推送其實(shí)只是一種HTTP Streaming的類似實(shí)現(xiàn),只不過它服務(wù)器端所推送的事件遵循了W3C Server-Sent Events規(guī)范中定義的事件格式。

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

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

直接寫回輸出流OutputStream的HTTP Streaming

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

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

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

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

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

Servlet容器配置

對(duì)于那些使用web.xml配置文件的應(yīng)用,請(qǐ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>

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

以下是一個(gè)例子,展示了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來簡(jiǎn)化配置,它們都自動(dòng)地為你設(shè)置了這些配置項(xiàng),并使得注冊(cè)Filter過濾器實(shí)例變得非常簡(jiǎn)單。

Spring MVC配置

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

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

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


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)