JSR 236: Timer for Application Servers

前回はJava EE環境でスレッドプログラミングを可能にするAPIとしてJSR 237: Work Manager for Application Serversを紹介した。今回はJSR 237と同じく、Java EE環境において非同期並列処理を実現するためのもうひとつのAPI「JSR 236: Timer for Application Servers」を紹介する。

JSR 237がjava.lang.Threadの代替となるAPIであるのに対して、JSR 236が提供するのはjava.util.Timerのようなタスクのスケジューリング機能だ。つまりこのAPIを使うことで、あるタスクが定期的に繰り返し実行されるような処理を実装することができる。

java.util.Timerクラスにおいて、スケジュールされたタスクは現在のスレッドとは独立したバックグラウンドスレッドで動作する。前回説明したように、Java EE環境の下では通常の方法でスレッドを扱うことが推奨されないため、java.util.Timerも使うことができない。そこで、JSR 236を使えばJava EE環境でも安全にタスクのスケジューリングができるようになるというわけだ。

JSR 236もJSR 237と同様にIBMとBEA Systemsの共同プロジェクトであるCommonJプロジェクトの成果が元になっている。すなわち、同プロジェクトによって仕様が策定された「Timer and Work Manager for Application Servers」のうちの"Timer"の部分の標準化を進めているのがJSR 236である。

CommonJのTimer APIを試す

CommonJの「Timer and Work Manager for Application Servers」仕様やインタフェース実装はBEAによるプロジェクトサイトまたはIBMによるプロジェクトサイトで公開されている。また、これらのAPIは「IBM WebSphere Application Server V6.0以降」および「BEA WebLogic Server 9.0以降」に実装されているほか、オープンな実装としてはGlobus Toolkitなどがある。

CommonJのTimer APIのクラス/インタフェースはcommonj.timersパッケージ以下に用意されている。中心となるのはタスクをスケジュールするためのTimerManagerインタフェースと、スケジュールされたタスクを実行するためのTimerListenerインタフェースだ。

TimerManagerオブジェクトは、WorkManagerのときと同様にアプリケーションさー場からJNDI経由で取得する。したがって、準備段階としてアプリケーションサーバのネーミングサービスにTimerManagerオブジェクトを登録しなくてはならない。 TimerManagerオブジェクトの登録の仕方は各アプリケーションサーバによって異なるので、それぞれのマニュアルを参考にしてほしい。

JNDIでTimerManagerへのリソース参照を有効にするために、Java EEコンポーネントのDeployment Descriptorファイル(Servletの場合web.xml、EJBの場合ejb-jar.xml)に設定を追加する。今回はServletを使うのでweb.xmlにリスト1のように記述する。

リスト1 web.xml - リソース参照の設定を追加

    <resource-ref>
        <res-ref-name>tm/MyTimerManager</res-ref-name>
        <res-type>commonj.timers.TimerManager</res-type>
        <res-auth>Container</res-auth>
        <res-sharing-scope>Unshareable</res-sharing-scope>
    </resource-ref>

スケジューリングを行うサーブレットTimerServletはリスト2のようにした。まずinit()メソッドにおいて、InitialContextを利用してJNDI経由でWTimerManagerオブジェクトを取得する。

リクエストが送られるとschedule()メソッドを用いてタスクがスケジューリングされる。schedule()メソッドには、TimerListenerオブジェクトと開始後の遅延時間、TimerListenerのtimerExpired()メソッドを呼び出す時間間隔を指定する。java.util.Dateオブジェクトを渡すことで指定時刻に実行されるタスクをスケジュールすることもできる。また、scheduleAtFixedRate()メソッドは決められた周期(時間間隔ではなく)でタスクを実行するためのメソッドである。

リスト2 TimerServlet.java - Timer APIによるスケジューリングを利用したサーブレットの例

package sample;

import commonj.timers.Timer;
import commonj.timers.TimerListener;
import commonj.timers.TimerManager;
import java.io.*;
import java.net.*;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import javax.servlet.*;
import javax.servlet.http.*;

public class TimerServlet extends HttpServlet {
    TimerManager myTimerManager = null;

    @Override
    public void init() throws ServletException {
      try {
         InitialContext ic = new InitialContext();
         // TimerManagerオブジェクトをJNDI経由で取得
         this.myTimerManager
            =(TimerManager)ic.lookup("java:comp/env/tm/MyTimerManager");
      } catch(NamingException e) {
        e.printStackTrace();
      }
    }

    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
                    throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");

        // タイマーのスケジュール
        myTimerManager.schedule(new MyTimerListener(), 0, 10000);
        System.out.println("TimerServlet: タイマーがスケジュールされました.");
    } 

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    } 

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }
}

TimerListenerインタフェースにはtimerExpired()というメソッドがひとつだけ定義されており、TimerManagerのスケジューラがタイマーを起動するとこのメソッドが呼ばれるようになっている。したがって、ここに定期的に実行したいタスクを実装すればよい。今回はリスト3のように標準出力にテキストを表示するだけの処理とした。

Timerオブジェクトはスケジュールされたタイマーの状態を保持している。TimerListenerのtimerExpired()メソッド内でcancel()メソッドを呼び出せば、タイマーを停止することができる。今回はタスクが10回実行されたら終了するようにした。

リスト3 MyTimerListener.java - スケジュールされた時間ごとにTimerManagerによって呼び出されるリスナ

    class MyTimerListener implements TimerListener {
        private int count = 0;

        /**
         * TimerManagerのスケジュールに従って呼び出される
         */
        public void timerExpired(Timer timer) {
            if (count < 10) {
                System.out.println("[" + timer + 
                        "] MyTimerListener: タイマーが呼ばれました.");
                count++;
            }
            else {
                // タイマーのキャンセル
                System.out.println("MyTimerListener: タイマーを終了します.");
                timer.cancel();
            }
        }
    }

最後に、web.xmlにはServletを有効にするための設定を追加し、最終的にはリスト4のようになる。

リスト4 web.xml - サーブレットの設定を追加

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" 
         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_2_5.xsd">

    <resource-ref>
        <res-ref-name>tm/MyTimerManager</res-ref-name>
        <res-type>commonj.timers.TimerManager</res-type>
        <res-auth>Container</res-auth>
        <res-sharing-scope>Unshareable</res-sharing-scope>
    </resource-ref>

    <servlet>
        <servlet-name>TimerServlet</servlet-name>
        <servlet-class>sample.TimerServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>TimerServlet</servlet-name>
        <url-pattern>/TimerServlet</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
</web-app>

完成したらコンパイルしてWARファイルにまとめ、アプリケーションサーバにデプロイする。ブラウザからTimerServletサーブレットにアクセスすると、アプリケーションサーバの標準出力にはプロンプト1のように表示されるはずだ。今回は簡略化のためWebページ側のデザインは省略した。

プロンプト1 TimerServletの実行結果

TimerServlet: タイマーがスケジュールされました.
[1185099677223.115846(-10000)] MyTimerListener: タイマーが呼ばれました.
[1185099687233.115847(-10000)] MyTimerListener: タイマーが呼ばれました.
[1185099697257.115860(-10000)] MyTimerListener: タイマーが呼ばれました.
[1185099707261.115875(-10000)] MyTimerListener: タイマーが呼ばれました.
[1185099717266.115887(-10000)] MyTimerListener: タイマーが呼ばれました.
[1185099727270.115900(-10000)] MyTimerListener: タイマーが呼ばれました.
[1185099737275.115930(-10000)] MyTimerListener: タイマーが呼ばれました.
[1185099747279.115946(-10000)] MyTimerListener: タイマーが呼ばれました.
[1185099757283.115959(-10000)] MyTimerListener: タイマーが呼ばれました.
[1185099767288.115973(-10000)] MyTimerListener: タイマーが呼ばれました.
MyTimerListener: タイマーを終了します.