Webアプリケーションで時間の掛かる処理をキックする場合、進捗表示をしたくなることがあります。
Threadを立てて裏で処理するようにすれば可能なのですが、いろいろと面倒があるのでざっくり指針を書いてみたいと思います。
HTMLから定期的にアクセスする
進捗状況を知るために、HTML側から定時で繰返しアクセスさせる必要があります。
METAタグを使ってリロードさせる手法が一般的ですね。
<META HTTP-EQUIV="Refresh" CONTENT="5">
上記例では5秒ごとのリロードです。
この方式だとページ全体を読み直すわけですが、
格好良くしようと思えばAJAX的手法を使って非同期通信する手法もあります。
とにかく、サーバに低的にアクセスします。これをポーリング(polling)といいます。
サーバ側ではセッションにThreadの管理オブジェクトを保管する
サーバ側ではThreadの管理オブジェクトをセッションに格納して管理します。
以下はJavaでのコーディング例。
import java.util.Timer;
import java.util.TimerTask;
public class ThreadManager extends Thread {
/** 状態ステータス */
public enum Status {
/** 開始待ち */
READY,
/** 実行中 */
RUNNIG,
/** 正常終了 */
FINISHED,
/** 異常終了 */
ERROR,
}
/** ポーリングのタイムアウト間隔 */
private static final long TIMER_DELAY = 10000;
/** スレッドの状態 */
private volatile Status status;
/** 停止用のタイマ */
private Timer timer;
/** コンストラクタ */
public ThreadManager() {
this.status = Status.READY;
// DeamonのTimerを作成
this.timer = new Timer(true);
}
/** 状態を返す */
public Status getStatus() {
return this.status;
}
/** Thread終了タイマーをリセット */
public void resetTimer() {
this.timer.cancel();
this.timer.schedule(this.new Canceler(), TIMER_DELAY);
}
/**
* {@inheritDoc}
* @see java.lang.Thread#start()
*/
@Override
public synchronized void start() {
this.status = Status.RUNNIG;
super.start();
this.resetTimer();
}
/** 処理結果を返す */
public Object getData() {
// ここではダミー実装なのでnullを返しているが、
// 必要な情報を格納したオブジェクトであるべき
return null;
}
/** エラー結果を返す */
public Object getError() {
// ここではダミー実装なのでnullを返しているが、
// 必要な情報を格納したオブジェクトであるべき
return null;
}
/**
* {@inheritDoc}
* @see java.lang.Thread#run()
*/
@Override
public void run() {
try {
while (!this.isInterrupted()) {
// ループ処理
// ...
}
// TODO 処理結果を格納
// 正常終了
this.status = Status.FINISHED;
} catch (RuntimeException e) {
// 異常終了
this.status = Status.ERROR;
}
}
/**
* ポーリングの反応がなくなった際に処理を停止させるためのTimerTask
*/
class Canceler extends TimerTask {
/**
* {@inheritDoc}
* @see java.util.TimerTask#run()
*/
@Override
public void run() {
// 割り込み
ThreadManager.this.interrupt();
}
}
}
ポイントとしてはループ処理の際、Thread#isInterrupted()を参照してループを回している点。
このほか時間の掛かる処理の前後で中断できるように仕込みを入れておく必要があります。
そして、10秒後にThreadを停止するタイマーを起動しています。
このタイマーはポーリングによるアクセスでリセットされます。
つまり、途中でユーザがブラウザを閉じるなどのキャンセルを行った場合に、
重い処理を途中で停止させるための仕掛けです。
一方Servletの方では
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Sessionから管理オブジェクトを取得
HttpSession session = request.getSession();
ThreadManager manager = (ThreadManager) session.getAttribute(MANAGER_KEY);
// 管理オブジェクトがない場合は作成してSessionに格納
if (manager == null) {
manager = new ThreadManager();
session.setAttribute(MANAGER_KEY, manager);
}
String dispatchUrl;
switch (manager.getStatus()) {
case READY:
// Threadの開始
manager.start();
// ポーリング画面を表示
dispatchUrl = URL_POLLING;
break;
case RUNNIG:
// Thread停止タイマをリセット
manager.resetTimer();
// ポーリング画面を表示
dispatchUrl = URL_POLLING;
break;
case FINISHED:
// 完了画面を表示
dispatchUrl = URL_FINISH;
request.setAttribute(DATA_KEY, manager.getData());
break;
case ERROR:
// エラー画面を表示
dispatchUrl = URL_ERROR;
request.setAttribute(ERROR_KEY, manager.getError());
break;
default:
throw new IllegalArgumentException();
}
RequestDispatcher rd = request.getRequestDispatcher(dispatchUrl);
rd.forward(request, response);
}
初回に管理オブジェクトを作成し、Threadを起動。
2回目以降は停止タイマのリセットを行い、進捗表示画面を表示。
完了、もしくはエラーだった場合はそれらの表示画面へと遷移させます。
注意点
適当にかいたソースなので同期をまじめに取っていません。
Sessionを触る際は同期を取る必要があります。2重にHTTPリクエストが飛んできた場合には
併走するThreadからセッションをいじることになるためです。
ともかく、時間の掛かる処理などを裏で実行して完了時に結果を表示する場合は、こんな感じのつくりになります。
投稿日時 : 2007年10月11日 18:02