Spring MVCのドキュメント「コントローラを実装する」を読む(3)
前回に引き続き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の非同期処理機能についていくばくかの知識がないとこれらの理解は難しいかもしれません。それらの知識が理解の助けになることは確実です。最低限、次の事項について認識しておいてください:
ServletRequest
はrequest.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つ追加されます。このメソッドは、非同期処理の起動後かつ当初のリクエスト処理スレッドが中断された段階で呼び出されます。より詳しい情報については、AsyncHandlerInterceptor
のJavadocを参照してください。
DeferredResult
に対して非同期処理のライフサイクルの段階ごとのコールバックを設定する方法として、onTimeout(Runnable)
とonCompletion(Runnable)
があります。前者は非同期リクエストがタイムアウトした場合に呼び出され、後者は非同期リクエストが完了した場合に呼び出されます。タイムアウト・イベントは DeferredResult
に〔訳注:setResult
もしくはsetErrorResult
により〕何かしらの値を設定することで処理できます。完了コールバックが終了したあとでは、もはやどのようにしても結果値の設定はできません。
同様のことがCallable
を非同期処理に用いる場合にも可能です。しかし、WebAsyncTask
のインスタンスによりCallable
をラップしてから、タイムアウト時と完了時のコールバックとして登録する必要があります。 ちょうどDeferredResult
の場合と同様に、完了コールバックが終了するまでは、タイムアウト・イベントを処理して結果を返すことが可能です。
JavaコードによるコンフィギュレーションやMVC名前空間を使用したXMLによるコンフィギュレーションで、アプリケーション全体で有効なCallableProcessingInterceptor
やDeferredResultProcessingInterceptor
を登録することもできます。これらのインターセプタはコールバックを提供し、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>
DispatcherServlet
と Filter
の設定情報には< 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によるコンフィギュレーションは、いずれも、非同期リクエスト処理のためのオプションを提供しています。WebMvcConfigurer
はconfigureAsyncSupport
メソッドを持っています。〔訳注: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現在)です。