seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

はじめてのS2Container

経緯

最近、あちこちで「S2Container + S2Struts + Mayaa」を使った案件があるんだけど…、っていう話を聞くようになってしまった。Springって話は聞かない。S2はSpringの類似品だと思っていたのに違うのか。

  • 「どんなもの、それって簡単?」って聞いたら、「EJBをやってた人はドツボにはまるよ」といわれてしまった。マジですか。
  • S2を使っているエンジニアが「セッションタイムアウトの時間をどうしようか。1時間、2時間?」とか悩んでいて「セッションタイムアウトは短いほうがいいんじゃない?」といったら、「S2Strutsではアクションマップをセッションに入れていてタイムアウトするとログインしなおしなんですぅ。」とか言う。マジですか!?

なんだか、いろんな意味で不安になってきた。これは試しておかないとマズイんじゃないか。
道のりは長いが、まずは簡単なところから。

とりあえず、スタンドアロンでDIコンテナとAOPを試す。

S2Containerの設定ファイルはXML形式で記述するけれど、拡張子はdiconと書く。Dependency Injection CONtainerの略だと思うが、どうやら、これは「ダイコン」と読むらしい…。
S2Containerの特徴は、どうやら以下の2点のようだ。

  • 積極的に名前なしでコンポーネント同士でバインドさせる
  • EL式みたいな方法 (OGNL)で値を操作

ってところですか。

必要なもの

http://s2container.seasar.org/ja/ から、S2Containerのzipファイルをもらってくる。
もってきたのは、S2.3.15.zipだった。
DIとAOPを試すだけなら、以下のものだけをクラスパスに通せばよい。

log4jも同梱されているので、これを使ってもよいのだが、とりあえずCommons LoggingのSimpleLogを使ってみる。
SimpleLogを使うために、クラスパス上に以下のファイルを配置する。

Commons-Loggingのログ実装をSimpleLogに選択する
commons-logging.properties

org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog

SimpleLogの設定
simplelog.properties

org.apache.commons.logging.simplelog.defaultlog=trace
実験コード

Main.java

package jp.seraphyware.test;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.SingletonS2ContainerFactory;

public final class Main {

    private static final Log log = LogFactory.getLog(Main.class);
    
    static {
        // SingletonS2ContainerFactoryは静的メソッドしかないユーテリティクラスである。
        // 初回initの呼び出しで、S2ContainerFactory.create("app.dicon")を実行し、
        // そのインスタンスを保持する。
        SingletonS2ContainerFactory.init();
        
        final Thread shutdownThread = new Thread() {
            @Override
            public void run() {
                // コンテナに終了することを通知する。
                // これにより、コンテナは登録されているオブジェクトに対して
                // 破棄メソッドの呼び出しを行う。
                SingletonS2ContainerFactory.destroy();
            }
        };
        Runtime.getRuntime().addShutdownHook(shutdownThread);
    }

    public static void main(final String[] args) throws Exception {
        // 初期化されたコンテナを取得する。
        // 初期化されていない場合はエラーになる。
        final S2Container container = SingletonS2ContainerFactory.
            getContainer();
        
        // 登録されたオブジェクトを取得する。
        final MyComponent obj1 = (MyComponent) container.
            getComponent(MyComponent.class);
        
        // MyComponentに対してAspectを指定しているため、
        // インターセプトが埋め込まれた新しいクラスが生成されている。
        // (つまり、コンテナで作成されたときにのみAOPが埋め込まれる。)
        log.info("クラス名: " + obj1.getClass());
        
        // 自動的に設定されているオブジェクトを取得する。
        final MySubComponent sub1 = obj1.getSubComponent();
        log.debug("name=" + sub1.getName());
        
        // 同じ名前で、もう一度取得する。
        // singletonで宣言されている場合、同じインスタンスを返す。
        // prototypeで宣言されている場合、異なるインスタンスを返す。
        final MyComponent obj2 = (MyComponent) container.
            getComponent(MyComponent.class);
        log.debug("obj1とobj2は等しいか? " + (obj1 == obj2));
        
        final MySubComponent sub2 = obj2.getSubComponent();
        
        // コンテナからインターフェイス名でサブコンポーネントを取得してみる。
        final MySubComponent sub3 = (MySubComponent) container.
            getComponent(MySubComponent.class);
        
        // MySubComponentImplは、singletonとしてコンテナに登録されているため
        // いずれも同じインスタンスを返す。
        log.debug("sub1とsub2は等しいか? " + (sub1 == sub2));
        log.debug("sub1とsub3は等しいか? " + (sub1 == sub3));
    }
    
}

コンテナで管理する雑多なクラスの定義

public interface MyComponent {

    MySubComponent getSubComponent();

}

public class MyComponentImpl implements MyComponent {

    private MySubComponent _subcomp;
    
    public MyComponentImpl(final MySubComponent v_subcomp) {
        if (v_subcomp == null) {
            throw new IllegalArgumentException();
        }
        this._subcomp = v_subcomp;
    }
    
    public MySubComponent getSubComponent() {
        return this._subcomp;
    }
}

public interface MySubComponent {

    void setName(String v_name);

    String getName();
}

public class MySubComponentImpl implements MySubComponent {
    
    private static final Log log = LogFactory.getLog(MySubComponentImpl.class);
    
    private String _name;
    
    public String getName() {
        return this._name;
    }
    
    public void setName(String v_name) {
        this._name = v_name;
    }
    
    public void close() {
        log.info("MySubComponentImpl#close()");
    }

    @Override
    public String toString() {
        return this._name == null ? "(null)" : this._name;
    }
    
}

メソッドインターセプタの定義

package jp.seraphyware.test;

import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.seasar.framework.aop.interceptors.AbstractInterceptor;

public class MyInterceptor extends AbstractInterceptor {

    private static final long serialVersionUID = -8723170966433962705L;

    public Object invoke(MethodInvocation v_invocation) throws Throwable {
        final Class clz = super.getTargetClass(v_invocation);
        final Log log = LogFactory.getLog(clz);
        
        log.trace("begin: " + v_invocation.getMethod());
        try {
            final Object ret = v_invocation.proceed();
            log.trace("end (result=" + ret + ")");
            return ret;
        }
        catch (Throwable e) {
            log.error(e);
            throw e;
        }
    }

}

今回の主役である、app.dicon (その1)

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

    <!-- 
        S2Containerでは登録されたオブジェクトを名前ではなくクラス名で索引することが推奨される。
        (ただし、同型のクラスが複数ある場合は名前でなければ索引できない。)
        instance属性がsingletonの場合、一度作成されたあとコンテナは同じオブジェクトを返す。
        この属性をprototypeとすると、オブジェクトをコンテナから取得する毎に新しいオブジェクトを返す。
        デフォルトはsingletonである。
     -->
    <component class="jp.seraphyware.test.MySubComponentImpl" instance="singleton">
        <!-- 
            プロパティ、setName()に対して自動的に設定する。
            コンストラクタに指定する場合は<arg>タグを用いる。
         -->
        <property name="name">"hello, world!"</property>

        <!-- 
            シングルトンインスタンスの場合、コンテナが破棄(destroy)されたときに
            任意のメソッドを呼び出すことができる。
         -->
        <destroyMethod name="close"/>
    </component>

    <!-- 
        アスペクトをコンテナに登録する。
     -->
    <component name="trace" class="jp.seraphyware.test.MyInterceptor" />
    
    <component class="jp.seraphyware.test.MyComponentImpl" instance="prototype">
        <!-- 
            MyComponentクラスのコンストラクタはMySubComponentインターフェイスを引数にとるが、
            argタグを省略した場合は、コンテナに登録されている同型のオブジェクトが1つだけあれば、それを
            自動的に適用するため、省略できる。
         -->
         <!-- <arg></arg> -->
         
         <!--
             メソッドの呼び出しについてAOPによるフックをかけられる。
             アスペクトは、AbstractInterceptorから派生する。
             pointcutにはカンマ区切りでメソッド名を指定できる。メソッド名には正規表現が使える。
             pointcutを省略した場合、そのクラスが実装しているインターフェイスの全てのメソッドに
             適用される。(インターフェイスをもっていることが必須である。)
           -->
         <aspect pointcut="get.*Component">trace</aspect>
    </component>

</components>

実行結果

[INFO] Main - クラス名: class jp.seraphyware.test.MyComponentImpl$$EnhancedByS2AOP$$b82368
[TRACE] MyComponentImpl - begin: public jp.seraphyware.test.MySubComponent jp.seraphyware.test.MyComponentImpl.getSubComponent()
[TRACE] MyComponentImpl - end (result=hello, world!)
[DEBUG] Main - name=hello, world!
[DEBUG] Main - obj1とobj2は等しいか? false
[TRACE] MyComponentImpl - begin: public jp.seraphyware.test.MySubComponent jp.seraphyware.test.MyComponentImpl.getSubComponent()
[TRACE] MyComponentImpl - end (result=hello, world!)
[DEBUG] Main - sub1とsub2は等しいか? true
[DEBUG] Main - sub1とsub3は等しいか? true
[INFO] MySubComponentImpl - MySubComponentImpl#close()
実験コード2

app.diconだけ差し替えて、自動登録をさせてみる。

app.dicon (その2)

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

    <!--
         コンポーネントの自動登録。
         一括で登録するため、個々にプロパティやコンストラクタの指定、destroyMethodの指定などはできない。
         ただし、コンストラクタやプロパティは自動バインドされるので、コンテナ上に、その引数に合致する型が
         登録されている場合、それを適用することができる。
     -->
    <component class="org.seasar.framework.container.autoregister.ComponentAutoRegister">
        <!-- 
            登録されるコンポーネントのinstance属性を指定する。
         -->
        <property name="instanceDef">
            @org.seasar.framework.container.deployer.InstanceDefFactory@SINGLETON
        </property>
        <initMethod name="addReferenceClass">
            <!-- クラスを検索するクラスローダを発見するために同一クラスローダで読み込まれるクラス名を明示する -->
            <arg>@jp.seraphyware.test.Main@class</arg>
        </initMethod>
        <initMethod name="addClassPattern">
            <!-- パッケージ名 -->
            <arg>"jp.seraphyware.test"</arg>
            <!-- クラス名。クラス名は正規表現を指定できる。 -->
            <arg>"My.*ComponentImpl"</arg>
        </initMethod>
    </component>
    
    <!-- 
        アスペクトをコンテナに登録する。
     -->
    <component name="trace" class="jp.seraphyware.test.MyInterceptor" />

    <!--
         アスペクトの自動登録。
         アスペクトの登録は、コンポーネントの登録後でなければならない。
     -->
    <component class="org.seasar.framework.container.autoregister.AspectAutoRegister">
        <!-- 適用するアスペクト名 -->
        <property name="interceptor">trace</property>
        <initMethod name="addClassPattern">
            <!-- パッケージ名 -->
            <arg>"jp.seraphyware.test"</arg>
            <!-- 適用するクラス名。正規表現を指定できる。 -->
            <arg>"My.*ComponentImpl"</arg>
        </initMethod>
    </component>
    

</components>

実行結果

[INFO] Main - クラス名: class jp.seraphyware.test.MyComponentImpl$$EnhancedByS2AOP$$1960f05
[TRACE] MyComponentImpl - begin: public jp.seraphyware.test.MySubComponent jp.seraphyware.test.MyComponentImpl.getSubComponent()
[TRACE] MyComponentImpl - end (result=(null))
[TRACE] MySubComponentImpl - begin: public java.lang.String jp.seraphyware.test.MySubComponentImpl.getName()
[TRACE] MySubComponentImpl - end (result=null)
[DEBUG] Main - name=null
[DEBUG] Main - obj1とobj2は等しいか? true
[TRACE] MyComponentImpl - begin: public jp.seraphyware.test.MySubComponent jp.seraphyware.test.MyComponentImpl.getSubComponent()
[TRACE] MyComponentImpl - end (result=(null))
[DEBUG] Main - sub1とsub2は等しいか? true
[DEBUG] Main - sub1とsub3は等しいか? true

なんというか、簡単だが…。
むしろ、簡単にするために自動バインドで型を積極的に使うことになっているような感じであろうか。

とりあえず、JTAJ2EE的宣言トランザクションを使ってみる

必要なもの

前述のjarに加えて、以下を追加。

実験コード

Main.java

package jp.seraphyware.test;

import java.util.List;

import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.SingletonS2ContainerFactory;

public class Main {

    static {
        SingletonS2ContainerFactory.init();
        final Thread shutdownThread = new Thread() {
            @Override
            public void run() {
                SingletonS2ContainerFactory.destroy();
            }
        };
        Runtime.getRuntime().addShutdownHook(shutdownThread);
    }
    
    public static void main(final String[] args) throws Exception {
        final S2Container container = SingletonS2ContainerFactory.getContainer();

        // DAOを取得
        final TestDAO dao = (TestDAO) container.getComponent(TestDAO.class);
        
        // DAOの実行
        final List<TestDTO> results = dao.select("select * from TESTTBL");
        for (final TestDTO result: results) {
            System.out.println(result);
        }
    }
}

まともな実装はせず、単にS2がどのよう設定で、どのように動くのか、ログで見るだけ。

DAO、DTOの雑多なコード

public interface TestDTO {
    // 省略
}

public class TestDTOImpl implements TestDTO {
    // 省略
}



/**
 * Testテーブルに対するデータアクセス
 */
public interface TestDAO {

    /**
     * データベースから条件でレコードを検索する。
     * @param v_condition 条件
     * @return レコードを格納した{@link Test2DTO}のリスト
     * @throws SQLException 失敗
     */
    List<TestDTO> select(String v_condition) throws SQLException;
}


/**
 * Testテーブルに対するデータアクセスの実装
 */
public class TestDAOImpl implements TestDAO {

    /**
     * ログ
     */
    private static final Log log = LogFactory.getLog(TestDAOImpl.class);

    /**
     * データソース
     */
    private final DataSource _dataSource;
    
    /**
     * データソースを指定してDAOを構築します。
     * @param v_dataSource データソース
     */
    public TestDAOImpl(final DataSource v_dataSource) {
        if (v_dataSource == null) {
            throw new IllegalArgumentException("データソースが指定されていません。");
        }
        this._dataSource = v_dataSource;
        log.debug(v_dataSource);
    }
    
    /**
     * データベースから条件でレコードを検索する。
     * @param v_condition 条件
     * @return レコードを格納した{@link Test2DTO}のリスト
     * @throws SQLException 失敗
     */
    public List<TestDTO> select(String v_condition) throws SQLException {
        // AOPによる「j2ee.xxxTx」インターセプタを使わない場合は、
        // j2ee.diconによりコンテナに登録されるUserTransactionを明示的に取得することができる。
        // final UserTransaction userTx = (UserTransaction)
        //      container.getComponent(UserTransaction.class);
        
        final List<TestDTO> results = new ArrayList<TestDTO>();
        final Connection con = _dataSource.getConnection();
        try {
            // 省略
        }
        finally {
            con.close();
        }
        return results;
    }
}

app.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>
    <!--
        JTAやJDBCのコンテナ、データソースを利用するためにはj2ee.diconが必要。
        ここでUserTransactionなどの登録が行われる。
        j2ee.diconはs2-extentionのjarに含まれているため修正する必要ないが、
        このj2ee.diconは、更にjdbc.diconを要求する。
        jdbc.diconはデータベースの設定とコネクションプール、データソースの設定を行ったものを
        クラスパス上に配置しておく必要がある。
    -->
    <include path="j2ee.dicon" />
    
    <!-- xxxDAOImplの自動登録 -->
    <component class="org.seasar.framework.container.autoregister.ComponentAutoRegister">
        <initMethod name="addReferenceClass">
            <arg>@jp.seraphyware.test.Main@class</arg>
        </initMethod>
        <initMethod name="addClassPattern">
            <arg>"jp.seraphyware.test"</arg>
            <arg>".*DAOImpl"</arg>
        </initMethod>
    </component>
    
    <!--
        AOPにより、xxxDAOの全てのメソッドに対して、
        j2ee.requiredTx のコンテナ管理トランザクションを設定する。
        これにより、メソッドの開示時にJTAに対してbeginされ、終了時にcommitされる。
        例外が発生した場合はrollbackされる。
    -->
    <component class="org.seasar.framework.container.autoregister.AspectAutoRegister">
        <property name="interceptor">j2ee.requiredTx</property>
        <initMethod name="addClassPattern">
            <arg>"jp.seraphyware.test"</arg>
            <arg>".*DAOImpl"</arg>
        </initMethod>
    </component>
    
    <!-- xxxDTOImplの自動登録 -->
    <component class="org.seasar.framework.container.autoregister.ComponentAutoRegister">
        <property name="instanceDef">
            @org.seasar.framework.container.deployer.InstanceDefFactory@SINGLETON
        </property>
        <initMethod name="addReferenceClass">
            <arg>@jp.seraphyware.test.Main@class</arg>
        </initMethod>
        <initMethod name="addClassPattern">
            <arg>"jp.seraphyware.test"</arg>
            <arg>".*DTOImpl"</arg>
        </initMethod>
    </component>
</components>

TestDAOImplクラスのコンストラクタは、DataSourceを引数として受け取るのだが、app.diconの中では指定していない。
これは、明示的に指定しなくても、コンテナ内に登録されているDataSource型のオブジェクトが1つだけあれば、自動登録するときに勝手にバインドする為である。

app.diconの中でj2ee.diconをincludeすると、jdbc.diconが必要とされるので、これは自分で設定する。
jdbc.dicon

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR2.1//DTD S2Container//EN"
    "http://www.seasar.org/dtd/components21.dtd">
<components namespace="jdbc">
    <include path="jta.dicon"/>

    <!-- Derbyのコネクション -->
    <component name="xaDataSource"
        class="org.seasar.extension.dbcp.impl.XADataSourceImpl">
        <!-- ドライバクラス名 -->
        <property name="driverClassName">
            "org.apache.derby.jdbc.EmbeddedDriver"
        </property>

        <!-- 接続文字列 -->
        <property name="URL">
            "jdbc:derby:testdb;create=true"
        </property>
        
        <!-- ユーザ/パスワード -->
        <property name="user">"user1"</property>
        <property name="password">"user1"</property>
    </component>

    <!--
         コネクションプール。
         コネクションプールは、プールするだけでなく、
         データソースとJTAを連携するためにも必要となる。
      -->
    <component name="connectionPool"
        class="org.seasar.extension.dbcp.impl.ConnectionPoolImpl">
        <!--
             データベースのコネクション。
             コンテナにXADataSource型が1つしかない場合は省略可能。
          -->
        <property name="XADataSource">xaDataSource</property>

        <!-- プールが枯渇した場合のタイムアウト -->
        <property name="timeout">600</property>

        <!-- 最大コネクション数 -->
        <property name="maxPoolSize">10</property>

        <!-- JTAなしにコネクションをオープンすることを許可するか? -->
        <property name="allowLocalTx">false</property>

        <!-- コンテナが終了したときに呼び出されプールの後始末をする -->
        <destroyMethod name="close"/>
    </component>

    <!--
        アプリケーションからアクセスするデータソース。
        このデータソースを通じてコネクションを取得すると、プールからコネクションが
        取得されると同時に、JTAの分散トランザクションにも参加する。
      -->
    <component name="dataSource"
        class="org.seasar.extension.dbcp.impl.DataSourceImpl">
        <!--
            プールを指定する。
            コンテナにConnectionPool型が1つしかない場合は省略可能。
        -->
        <arg>connectionPool</arg>
    </component>

</components>
実行結果
[DEBUG] TestDAOImpl - org.seasar.extension.dbcp.impl.DataSourceImpl@1542a75
[DEBUG] TransactionImpl - トランザクションを開始しました
[DEBUG] ConnectionPoolImpl - 物理的なコネクションを取得しました
[DEBUG] DataSourceImpl - 論理的なコネクションを取得しました
[DEBUG] ConnectionWrapperImpl - 論理的なコネクションを閉じました
[DEBUG] TransactionImpl - トランザクションをコミットしました
[DEBUG] ConnectionWrapperImpl - 物理的なコネクションを閉じました

ちゃんとJTAのメカニズムが動いているようだ。
一応、ソースをみて確認したが、たしかにorg.seasar.extension.dbcp.impl.DataSourceImplでコネクションを取得したとき、コネクションプールを経由してトランザクションマネージャに対しenlistするように要求している。
また、j2ee.requiredTxのインターセプタもソースを確認したところ、単純に、トランザクションマネージャに対して、まだトランザクションが開始されていなければbeginし、メソッドを呼び出して、トランザクションを自分が開始していればcommitする、という動きになっていて、呼び出し元はトランザクションに対して意識することなく、AOPによって自動的に呼び出されるため、たしかにCMT(Container Managed Transaction)的である。
今回はDAOにRequiredを指示しているが、現実的な利用ではDAOはMandatoryで、ビジネスロジックに対してRequiredかRequiredNewをかけることになるだろう。
J2EEサーバを使わずにEJB的な宣言的トランザクションが使えることは大きなメリットだと思う。

結論

S2Container単体、およびJTA/CMT的なサポートは非常によく出来ていると思う。
JavaSEのレベルで、これが普通に使えるのはうれしい。
普通にファンになっちゃったよ。


だが、S2StrutsやらS2Daoが、本当に使いやすいのかは分からない。
これを調べるのが大変なのだと思うのだが。まだまだ先は長いな…。