Strutsから非同期にバッチ処理などを行う場合のサンプルを作ってみます。例えば、非常に時間のかかる処理を起動だけして、後からジョブの実行結果を確認したりします。
ちなみにこの実装については、ほとんどStrutsと無関係なので、Web系の処理で非同期処理を実装する場合には、同じように考えられるかと思います。
非同期処理は、処理の終了を待たずにブラウザにレスポンスを返します。
処理の起動だけを行い、すぐに画面を描画できるため長々と待たされる感じはなくなります。
そのかわり、実行結果を表示することが難しくなります。処理が終わる前に先に進んでしまうので実行結果を表示したり、実行結果を使用してその後の処理を進める場合には、やはり処理の終了を待たなければならないためです。
今回のサンプルでは、次のように実行結果を表示します。
- 非同期処理を実行し、実行を開始したことをブラウザにレスポンスする
- 処理の終了を一定間隔で問い合わせる(ポーリング)
- 処理が終了していたら問い合わせは終了する
非同期処理の実装方法は、いくつか考えられます。
教科書的なやり方は、Java Message Service(JMS)を使う方法です。JMSは非同期にメッセージを送受信するための技術で、これとメッセージ駆動型のMessage Driven Bean(MDB)を使って非同期処理を実装します。
これが教科書的なやり方なのですが、実は最もマイナーな方法だと思っています。JMSもMDBも情報が少なくスタンダードに使われている実装方法とは思えません。よく使われる方法はThreadやプロセスを使用する方法です。
サンプルでは、非同期処理をJavaのスレッド(Thread)を使って実装します。
プロセス起動の実装方法よりも低コストで起動できます。一方、下手な記述をするとメインの処理にも影響を与えかねないので、コーディングには注意が必要です。
プロセスを使う場合には、mainメソッドを持つJavaアプリケーションを作成してそれをRuntime.execで実行するようになります。別にJavaアプリケーションでなくても構いません。
※後から知ったのですがJ2EE1.4仕様ではサーブレットから新たなThreadを生成することを禁じているようです。
したがって、製品によっては正常に動作するものもありますがスレッドを起動して非同期に処理を行う実装方法はオススメできる方法ではありません。
非同期処理クラス
非同期処理はRunnableインターフェースを実装して作成し子Threadとして実行します。Runnableインターフェースはrun()メソッドの記述が必要です。ここに長い時間がかかる処理を記述します。
サンプルは、長い時間がかかるようにわざと10秒間スリープしています。
このRunnableインターフェースを実装したクラスをnewし、Threadをnewするときの引数にします。
public void run() {
try {
this.end = false;
//長い時間が掛かる処理
this.answer = this.value1 + this.value2;
Thread.sleep(10000);
} catch (InterruptedException e) {
//処理中に発生した例外を保存
this.ex = e;
} finally {
this.end = true;
}
}
処理の終了確認
処理を非同期に実行するだけではその実行結果を受け取ることができません。
非同期処理の実行結果を何らかの方法でどこかに保持し、それをメインの処理が参照することで実行結果を得ることができます。この実装方法はいろいろな手段が考えられます。
例えばDBに処理の進捗状況(%)や実行結果を書き込むようにして、終了チェック時にDBを検索するとか。DBは大抵一元化されているので、排他的な処理についても考慮されていて扱いやすいです。
今回は、Javaの「非同期処理オブジェクト」内のフィールドに実行結果を保持します。そして「非同期処理オブジェクト」は、staticでスレッドセーフなMapオブジェクトに保持するようにします。
このMapをオブジェクトのことを以降は“プロセスストア”と呼びます。
簡易的な実装ですが、static変数に保持しているだけなので、サーバが分散されていたりすると使えません。
/** 起動された非同期処理を保持するMap */
private static Map processStore = Collections.synchronizedMap(new HashMap());
ポーリング
一定間隔で終了問い合わせする方法は、metaタグによるポーリングによって実装します。時間がかかる処理の終了を、n秒おきに「終わった?」としつこく問い合わせるわけです。
metaタグによって再リクエストする方法は、画面が再描画されるのであまり見栄えの良い実装でないのですが簡単に記述できます。(他の方法としてはAjaxなど)
<meta http-equiv="Refresh"
content="3; URL=check.do?executeID=${fn-145:escapeXml(BatchForm.executeID)}">
処理IDをパラメータとして渡します。処理IDとは非同期処理を起動した時に発行された一意なIDで、これを渡さないと複数のユーザが起動したときに、どの処理が自分が起動した処理なのか分からなくなってしまいます。
プロセスストアには、処理IDをキーに「非同期処理オブジェクト」が格納されているので、自分の起動した処理を取得することができます。
注意点
このあたりは、一定時間経過したプロセスは削除するなどの処理が必要になってくると思います。
DBに保持しているならば、ログのように残してもいてもすぐには問題になりませんが、今回のように変数で保持している場合には、メモリを消費していくので、何らかの対策を考えなければなりません。
まずスレッドセーフなオブジェクトを渡さなければなりません。子スレッドに渡して、スレッドを起動した後に、非スレッドセーフなオブジェクトを書き換えたりすると危険です。
また、Webコンテナで生成された、HttpServletRequest,HttpServletResponse,HttpSessionなどは、Webコンテナがライフサイクルを管理するのでこれらを渡すことも危険です。
Webコンテナ上でDB接続したコネクションを渡すのも、切断のタイミングを考えると辞めたほうがよいです。子スレッド新たにDB接続するほうがよいです。
サンプルソースコード
https://app.box.com/s/3bvy6iw78aubr5kyd4q5h20mi4ctoihs