M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

Spring MVCのドキュメント「コントローラを実装する」を読む(3)

f:id:m12i:20141115150916p:plain

前回に引き続きSpring Web MVCフレームワークに関するドキュメントの訳出です。

原典は、Springフレームワーク(本体)のリファレンス・マニュアルである"Spring Framework Reference Documentation"の第5部"The Web"の第17章"Web MVC framework"の第3節(バージョン4.1.1.RELEASE現在)です。

          * * *

17.3 コントローラを実装する(3)

17.3.4 非同期リクエストの処理

Spring MVC バージョン3.2ではServlet バージョン3ベースの非同期リクエスト処理が導入されました。コントローラのメソッドは従来通り値を返すのではなく、 java.util.concurrent.Callableを返すことで、戻り値を別のスレッド上で生成させることができます。これはつまり、サーブレット・コンテナのスレッドは現在のリクエストの処理から解放され、他のリクエストの処理に移ることができるということです。Spring MVC TaskExecutorの助けを借りて別のスレッド上でCallableを実行します。Callableにより結果が生成されると、リクエストは再度サーブレット・コンテナにディスパッチされて、Callableの返した結果の値とともに処理が再開されます。この仕組みを使用したコントローラのメソッドの例を示します:

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

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

}

非同期処理を実現するもう一つの選択肢は、コントローラのメソッドDeferredResultインスタンスを戻り値として返させるというものです。この場合も結果値の生成は別スレッドで行われることになります。しかしSpring MVCはそのスレッドについて関知しません。例えば、結果値がJMSメッセージやスケジュールされたタスク、その他の外部イベントにより生成されるものである場合が該当します。この仕組みを使用したコントローラのメソッドの例を示します:

@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<String>();
    // その後deferredResultはインメモリのキューに保存する…
    return deferredResult;
}

// そして別のスレッド上で結果は設定される…
deferredResult.setResult(data);

Servletバージョン3の非同期処理機能についていくばくかの知識がないとこれらの理解は難しいかもしれません。それらの知識が理解の助けになることは確実です。最低限、次の事項について認識しておいてください:

  • ServletRequestrequest.startAsync()を呼び出すことで非同期モードに遷移させることができます。このモード変更により、サーブレットはフィルタと同様に処理を中断できるようになります。レスポンスはオープンされたまま残り、別のスレッドで処理を完了させることができます。
  • request.startAsync()を呼び出すと、AsyncContextが返されます。このオブジェクトによりあとから非同期処理に対して制御を行うことができます。例えば、アプリケーション・スレッド〔訳注:サーブレット・コンテナのスレッドとは別のスレッド〕から dispatchメソッドを呼び出すことで、リクエストをサーブレット・コンテナのスレッドに再度ディスパッチすることができます。この非同期ディスパッチはフォワードの機能に似ていますが、あるスレッド(アプリケーション・スレッド)からもうひとつのスレッド(サーブレット・コンテナのスレッド)に処理の移譲を行う点が異なります。フォワードでは、同じ(サーブレット・コンテナの管理下にある)スレッドの間で同期的に処理の移譲が行われます。
  • ServletRequest は現在の DispatcherTypeへのアクセスを提供します。これにより、現在のサーブレットやフィルタの処理がクライアントからのリクエストに基づいて起動されたものなのか、非同期ディスパッチにより起動されたものなのかが区別できます。

以上のことを念頭に置きつつ、 Callable による非同期処理のフローを見てみましょう: ①コントローラが Callable を返す。 ②Spring MVCが非同期処理を開始し、Callableを別のスレッドで処理するため TaskExecutor 登録する。 ③ DispatcherServlet やフィルタはレスポンスをオープンしたままリクエスト処理スレッドを中断する。 ④Callableが結果を生成し、Spring MVCがリクエストを再度サーブレット・コンテナにディスパッチする。 ⑤ DispatcherServlet は再度呼び出され、Callableから非同期に生成された結果の値とともに処理を再開する。②・③・④のプロセスが〔訳注:他のリクエストの処理などの割り込みなしに〕間断なく実行されるかどうかは非同期処理の実行スピード次第です。

DeferredResult による非同期処理のフローも同じ原則に基づくものですが、どのような仕組みで非同期処理の結果を生成するかはアプリケーション次第です: ①コントローラがDeferredResult を返す。同じオブジェクト参照を何かしらのインメモリ・キューやリストにも保存する。 ②Spring MVCが非同期処理を開始する。 ③ DispatcherServlet やフィルタはレスポンスをオープンしたままリクエスト処理スレッドを中断する。 ④アプリケーションが他のスレッドからDeferredResultに対して結果値を設定する。そしてSpring MVCがリクエストを再度サーブレット・コンテナにディスパッチする。 ⑤ DispatcherServlet が再度呼び出され、非同期処理により生成された結果とともに処理を再開する。

非同期のリクエスト処理を採用する誘因や、それがいつ・なぜ使用されるかといった事項について解説することは、このドキュメントのスコープに入っていません。そうした情報についてはこのブログ投稿を参照してみるとよいでしょう。

非同期リクエストのための例外処理

コントローラのメソッドが返したCallableを実行中に例外がスローされた場合どうなるのでしょうか? この場合、コントローラのメソッドが例外をスローしたときと同じような結果になります。スローされた例外は同じコントローラ上の該当する @ExceptionHandlerで処理されるか、コンフィギュレーションの過程で登録されたHandlerExceptionResolverインスタンスの.うち該当するもので処理されるかします。

[NOTE] 内部的には、 Callableが例外をスローした時でも、Spring MVCは再度サーブレット・コンテナにディスパッチを行い、処理を再開させます。リクエストの処理の場合とのちがいとしては、Callableの結果値が例外オブジェクトであり、その処理はかならず HandlerExceptionResolverインスタンスにより実施されるというだけです。

DeferredResultを使用する場合、setErrorResult(Object) メソッドを呼び出し Exceptionもしくはそれ以外の何かしらのオブジェクト──処理の結果を示すために使用される──を提供することができます。結果値として例外オブジェクトを使用した場合、同じコントローラ上の該当する @ExceptionHandlerで処理されるか、コンフィギュレーションの過程で登録されたHandlerExceptionResolverインスタンスの.うち該当するもので処理されるかします。

非同期リクエストの事前処理

既存のHandlerInterceptor実装にAsyncHandlerInterceptorを実装させることもできます。このインターフェースにより afterConcurrentHandlingStartedというメソッドが1つ追加されます。このメソッドは、非同期処理の起動後かつ当初のリクエスト処理スレッドが中断された段階で呼び出されます。より詳しい情報については、AsyncHandlerInterceptorJavadocを参照してください。

DeferredResultに対して非同期処理のライフサイクルの段階ごとのコールバックを設定する方法として、onTimeout(Runnable)onCompletion(Runnable)があります。前者は非同期リクエストがタイムアウトした場合に呼び出され、後者は非同期リクエストが完了した場合に呼び出されます。タイムアウト・イベントは DeferredResultに〔訳注:setResultもしくはsetErrorResultにより〕何かしらの値を設定することで処理できます。完了コールバックが終了したあとでは、もはやどのようにしても結果値の設定はできません。

同様のことがCallableを非同期処理に用いる場合にも可能です。しかし、WebAsyncTaskインスタンスによりCallableをラップしてから、タイムアウト時と完了時のコールバックとして登録する必要があります。 ちょうどDeferredResultの場合と同様に、完了コールバックが終了するまでは、タイムアウト・イベントを処理して結果を返すことが可能です。
JavaコードによるコンフィギュレーションMVC名前空間を使用したXMLによるコンフィギュレーションで、アプリケーション全体で有効なCallableProcessingInterceptorDeferredResultProcessingInterceptorを登録することもできます。これらのインターセプタはコールバックを提供し、Callable DeferredResultが使用される際に毎回適用されます。

非同期リクエスト処理のための設定

Servlet バージョン3 の非同期設定

Servlet バージョン3の非同期リクエスト処理を使うためには、web.xmlを書き換える必要があります:

<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>

DispatcherServletFilterの設定情報には< async-supported>true</async-supported>という子要素を追加します。さらに、各Filterでは、非同期ディスパッチを有効にするため、 ASYNC ディスパッチャ・タイプをサポートすることを宣言します。Springフレームワークが提供するすべてのインターフェースについてASYNCディスパッチャ・タイプの設定が可能です。それらのフィルタは非同期ディスパッチにより必要になったときにしかはたらきません。

[NOTE] フィルタによっては非同期ディスパッチの有効化が絶対必須となる点に注意してください。例えば、 OpenEntityManagerInViewFilter がデータベース・コネクションの解放の責任を負っている場合、非同期リクエストのあとで必ず実行されなくてはなりません。

以下に、適切にフィルタを設定する例を示します:

<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>

バージョン3でWebApplicationInitializerなどを通じたJavaコードによるコンフィギュレーションを行う場合も、web.xmlのときと同様に、"asyncSupported"フラグとASYNCディスパッチ・タイプの設定が必要になります。 これらの設定を簡便に行うため、AbstractDispatcherServletInitializerもしくはAbstractAnnotationConfigDispatcherServletInitializerの拡張を検討してください。これらのイニシアライザは前述のオプションを自動で設定し、Filterインスタンスの登録を非常に簡単にしてくれます。

Spring MVC の非同期設定

MVCフレームワークが提供するJavaコードによるコンフィギュレーションMVC名前空間を使用したXMLによるコンフィギュレーションは、いずれも、非同期リクエスト処理のためのオプションを提供しています。WebMvcConfigurerconfigureAsyncSupportメソッドを持っています。〔訳注:XMLによるコンフィギュレーションについて言えば〕<mvc:annotation-driven><async-support> 子要素を持っています。

これらのAPIにより非同期リクエストのデフォルトのタイムアウト値を設定することができます。この設定値はサーブレット・コンテナのデフォルトの設定(例えばTomcatにおいては10秒)とは独立したものです。コントローラのメソッドが返したCallableインスタンスを実行するのに使用するAsyncTaskExecutorを設定することもできます。しかしこのプロパティについては、Spring MVCのデフォルトの設定── SimpleAsyncTaskExecutor を使用する──のままとしておくことが強く推奨されています。MVCフレームワークが提供するJavaコードによるコンフィギュレーションMVC名前空間を使用したXMLによるコンフィギュレーションでは、CallableProcessingInterceptorインスタンス およびDeferredResultProcessingInterceptorインスタンスを登録することも可能です。

特定の DeferredResultについてデフォルトのタイムアウト値を上書きする必要がある場合、適切な DeferredResultコンストラクタを使用して設定を行います。Callableの場合も同様に、適切なコンストラクタで初期化されたWebAsyncTaskでラップしてすることでタイムアウト値のカスタマイズが可能です。WebAsyncTaskコンストラクタにはAsyncTaskExecutorを渡すこともできます。

17.3.5 コントローラをテストする

spring-testモジュールは、MVCアノテーションが付与されたコントローラをテストするための完璧なサポートを提供しています。これについてはセクション11.3.6の「Spring MVC テスト・フレームワーク」を参照してください。

          * * *

いったんは以上です。17.3以外にも気になるところが見つかれば、またその時に訳出してみようと思っています。

原典は、Springフレームワーク(本体)のリファレンス・マニュアルである"Spring Framework Reference Documentation"の第5部"The Web"の第17章"Web MVC framework"の第3節(バージョン4.1.1.RELEASE現在)です。


Spring関連の訳出記事まとめ - M12i.