読者です 読者をやめる 読者になる 読者になる

Jの衝動書き日記

さらりーまんSEの日記でございます。

Servlet3.0における非同期処理の調査メモ

 久々にアクセス解析を見てみたら数値が跳ね上がっていた。何故だと見てみたら、過去に書いた記事がホットエントリーに上がったからっぽい。同様にハマった事例でもあたのだろうか。ただ、改めて読んだら事象が分かりづらかったので図を入れておいた。


 さてそれはさておき、お仕事でServlet3.0の非同期処理に関する調査をしたので、メモを残しておく。内容は非同期処理のcomplete()で例外が発生したときの挙動を調べたものである。jettyでは、このときなぜか自動的にget処理が実行された形跡があったため、不思議に思って調査することになった。以下の話はjettyはjetty8、tomcatはtomcat7が対象のバージョンである。

非同期処理の基本

  非同期処理は、実装したServletで処理が重い機能がある場合、response時間を改善したい時などに使う。なお、非同期処理実行中もConnectionは維持されている。

 

 非同期処理の使い方

  • アノテーションを定義する(※asyncSupported=trueとする必要がある)
    • @WebServlet(urlPatterns = { "xxx" }, asyncSupported = true) 

 ちなみに、Servletは本来web.xmlにmapping定義をする必要があるが、Servlet3.0の場合は、Servletクラスにアノテーション定義しておけばweb.xmlのmapping定義が不要になる。この機能を利用するためにはweb.xmlのversion定義を3.0としておく必要がある。
 

  • 非同期処理の起動方法

 ※requestはHttpServletRequest
 AsyncContext acontext = request.startAsync();
 acontext.start(new Runnable() { /* 非同期処理 */});

 非同期処理側では処理完了時にAsyncContextのcomplete()を実行する必要がある。
 
 詳しくは以下を参照
 JavaEE使い方メモ(Servlet・JSP・EL式) - Qiita

 

非同期処理の応答について

 非同期処理の実行結果を返す方法は2通りある。

 

 転送する

  まず重い処理を機能として切り出しおき、Servletはその機能へ転送する形をとる。AsyncContextのdispatch()を使用した場合、転送した処理は非同期処理で実行されるためServlet自体の応答はすぐ返せる。詳細は未検証のためここでは詳しくは触れない。 

 

 Writer/OutputStreamに書き出す

 AsyncContextのstart()で非同期処理を起動した場合はこの方法を取ることになる。HttpResponseからServletOutputStreamかPrintWriterを取得し、これに応答結果を書き出す。非同期処理を起動した時のHttpResponseをAsyncContextからは取得できるため、これからServletOutputStreamかPrintWriterを取得して応答結果を書き出す。

 主な用途としては、Servletをwebapi的に使用(HTMLは返さず、JSON等のデータのみを返す)する場合などに使用する。

 Servletと非同期処理でWriter/OutputStreamを共用することになるのでServlet側でこれらをcloseすると応答を返せなくなる。また、PrintWriterかServletOutputStreamはどちらか一方のみ取得可能である。ServletでPrintWriterを取得したあと、非同期処理側でServletOutputStreamを取得することはできない。

 

非同期処理の応答転送形式について

  非同期処理の転送形式は以下のうちどちらかになる。

  • 非同期処理が完了したタイミングで一括送信
  • 随時結果を送信(転送形式はTransfer-Encoding:.chunked)

 Writer/OutputStreamで応答内容を書き出す度にflush()を実行すると転送形式はTransfer-Encoding:.chunkedとなる。chunkedは、HTTPのbodyの内容を送信バイト数・送信内容という形式で転送し、送信バイト数が0となると通信終了と判断する転送方式である。

 非同期処理を実行していてもflush()を実行しないとServlet終了時には応答が返らない。応答が返るのは、非同期処理でAsyncContextのcomplete()を実行するタイミングとなる。

  flush()を実行した場合、flush()のタイミングで応答結果が送信される。AsyncContextのcomplete()を実行するとchunkedの終了を表す0が送信される。

 

 図にするとこんな感じ。

f:id:newWell:20150519130703j:plain

 

非同期処理のタイムアウトについて

  Servletから起動する非同期処理はアプリケーションサーバ(jetty・tomcatなど)からタイムアウト監視されている。タイムアウト発生はListenerを設定することで検知することが可能である。

 Listenerを設定していない、設定していてもタイムアウトイベントを適切に処理していない場合、アプリケーションサーバのデフォルトの動作が実行される。だが、非同期処理自体は停止していないため、非同期処理終了時にcomplete()を実行したタイミングで例外が発生する。これは、アプリケーションサーバ側ですでに通信を終了しているため発生する。

 

jettyの場合

  非同期処理を起動したpathに対して再度get処理(パラメータなし)を行い、その結果をサーブレットの呼び元に返した後、通信を終了する。ただし、この処理はHTTP通信を発行するわけではなく、javaのクラスの直接コールの形をとる。

 

tomcatの場合

  単純に通信を終了する。ただし、非同期処理側でcomplete()実行前にWriter/OutputStreamをcloseしていると、次回接続時にタイムアウト処理後に書き込んだ内容が最初に表示される。この点に関してはjettyでは未検証のためjettyも同様なのかもしれない。
 

 

 非同期処理のタイムアウト処理でデフォルトの動作をさせたくない場合は、Listenerを設定し、onTimeout()メソッドを実装する必要がある。Servlet3.0の規格上(AsyncContext)では、complete()かdispatch()ぐらいしかやれることはないが、jettyの実装クラスを使えば細かい制御も可能であるようだ(だが未検証)。

 

おまけ

  Windowsではローカルホストの通信はWiresharkではキャプチャ出来ない。だが、RawCapというツールを使えば可能である。なお、ローカルのtomcatを指定する場合、http://localhost:8080/servlet/hogeではなく、http://127.0.0.1:8080/servlet/hogeという形にしないとうまくキャプチャできなかった(Windows7の場合)。
RawCap - A raw socket sniffer for Windows