無料で使えるシステムトレードフレームワーク「Jiji」 をリリースしました!

・OANDA Trade APIを利用した、オープンソースのシステムトレードフレームワークです。
・自分だけの取引アルゴリズムで、誰でも、いますぐ、かんたんに、自動取引を開始できます。

Servlet内で起動したThreadからSessionスコープのコンポーネントを利用できない。

今日はまったところ。
Seasar2では、Servlet内で起動したThreadからSessionスコープのコンポーネントを利用できません。(Seasar2.3.22で確認。)

原因

HttpSessionの取得先であるHttpServletRequestをThreadLocalで保持しているのが原因。Servletを実行するThreadでは、ServletFilterでHttpServletRequestが設定されますが、Servlet内で起動したThreadでは設定してくれる人がいないため、コンポーネント作成時に以下のようなエラーになります。

org.seasar.framework.exception.EmptyRuntimeException: [ESSR0007]sessionはnullあるいは空であってはいけません
	at org.seasar.framework.container.deployer.SessionComponentDeployer.deploy(SessionComponentDeployer.java:44)
	at org.seasar.framework.container.impl.ComponentDefImpl.getComponent(ComponentDefImpl.java:94)
	at org.seasar.framework.container.impl.S2ContainerImpl.getComponent(S2ContainerImpl.java:128)
	at s2.SessionScope$3.run(SessionScope.java:74)

サンプル

問題を再現するサンプルコードを書いてみました。(Servletにすると面倒そうなので、サンプルコード内でS2ContainerFilterなどの操作をシミュレーションしています。)

public static void main ( String[] args ) throws InterruptedException {

    // HttpServletRequest のモック。
    HttpServletRequest request = createHttpServletRequest();


    SingletonS2ContainerFactory.setConfigPath( "s2/test.dicon" );
    SingletonS2ContainerFactory.init();

    try {
        // HttpServletRequestの設定。
        // 実環境ではS2ContainerFilterで設定される。
        S2Container container = SingletonS2ContainerFactory.getContainer();
        container.setRequest( request );

        // --- ここから servlet  ---

        // Servlet内でコンテナを使う場合と同じ状態。
        // ここでは、Sessionスコープのコンポーネントが取得できる。
        System.out.println( "servlet: " + container.getComponent( "kitten" ) );

        // Servlet内でThreadを起動。
        // Thread 内部では Sessionスコープのデータを取得できない。
        Thread t = new Thread() {
            public void run() {
                try {
                    S2Container container = SingletonS2ContainerFactory.getContainer();
                    System.out.println( "Thread: " + container.getComponent( "kitten" ) );
                } catch ( Exception e ) {
                    e.printStackTrace();
                }
            }
        };
        t.start();
        t.join(); // Threadの終了を待つ。


        // --- servlet ここまで ---

    } finally {
        // HttpServletRequestの破棄。
        // これも実環境ではS2ContainerFilterが行う。
        S2Container container = SingletonS2ContainerFactory.getContainer();
        container.setRequest( null );
    }

}

/**
 * HttpServletRequest のモックを生成する。
 * @return HttpServletRequestのモック
 */
static HttpServletRequest createHttpServletRequest () {

    // HttpSessionのモック。 getAttribute()とsetAttribute()だけ実装。
    final HttpSession session = (HttpSession) Proxy.newProxyInstance( ClassLoader.getSystemClassLoader(),
        new Class[] {HttpSession.class}, new InvocationHandler() {
            private final Map map = new HashMap();
            public Object invoke ( Object proxy, Method method, Object[] args )
            throws Throwable {
                if ( "getAttribute".equals( method.getName()) ) {
                    return map.get( args[0] );
                } else if ( "setAttribute".equals( method.getName()) ) {
                    map.put( args[0], args[1] );
                    return null;
                }
                throw new UnsupportedOperationException();
            }
    } );

    // HttpServletRequest のモック。
    return  (HttpServletRequest) Proxy.newProxyInstance( ClassLoader.getSystemClassLoader(),
        new Class[] {HttpServletRequest.class}, new InvocationHandler() {
            public Object invoke ( Object proxy, Method method, Object[] args )
            throws Throwable {
                return session;
            }
    } );
}

DICONファイルは次の通りです。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.3//EN"
    "http://www.seasar.org/dtd/components23.dtd">
<components >

    <!-- Sessionスコープのコンポーネント -->
    <component name="kitten" class="java.lang.String" instance="session">
      <arg>"mii"</arg>
    </component>

</components>

実行結果です。シミュレーションしたServletの中ではSessionスコープのコンポーネントを使えますが、新規に起動したThread内ではエラーになっています。

servlet: mii
org.seasar.framework.exception.EmptyRuntimeException: [ESSR0007]sessionはnullあるいは空であってはいけません
	at org.seasar.framework.container.deployer.SessionComponentDeployer.deploy(SessionComponentDeployer.java:44)
	at org.seasar.framework.container.impl.ComponentDefImpl.getComponent(ComponentDefImpl.java:94)
	at org.seasar.framework.container.impl.S2ContainerImpl.getComponent(S2ContainerImpl.java:128)
	at s2.SessionScope$1.run(SessionScope.java:48)

対策

Sessionに積んでいたオブジェクトは、「生成コストが大なのでなるべく使い回したい。ただしユーザーごとに1つは必要」というたぐいのオブジェクトであったので、Sessionが使えない場合は再作成するコードにしてとりあえず回避。次のような感じです。

Thread t = new Thread() {
    public void run() {
        try {
            S2Container container = SingletonS2ContainerFactory.getContainer();
            if ( container.getRoot().getSession() == null ) {
                // セッションが使えない場合はprototypeの方で我慢する。
                System.out.println( "Thread: " + container.getComponent( "kitten-prototype" ) );
            } else {
                System.out.println( "Thread: " + container.getComponent( "kitten" ) );
            }
        } catch ( Exception e ) {
            e.printStackTrace();
        }
    }
};

というわけで

コンポーネントをSessionスコープにするときはご注意ください。ただ、個人的には、あまり使わない方がいい機能かなー、と感じてきています。そもそもTomcatがないと使えないのが痛い。ローカル環境でテストするときに、コンテナからコンポーネントを取得しようとしたらエラーになった..orz.とかいう思い出もあります。コンテナを使わずに単体テストするとか、テスト用DICONを用意すればいいのかもしれませんが、「本番用DICONで、最下層のDAO部分だけ差し替えて、ローカルで貫通テスト」とか、やりたいじゃないですか! > 誰?