Seasar DI Container with AOP

DIContainer

DIContainerとは、Dependency Injectionをおこなう軽量コンテナのことで、IoCコンテナといわれることもあります。Dependency InjectionについてはMartin Fowler の「Inversion of Control Containers and the Dependency Injection pattern」で分かりやすく説明されています。そこで説明されているとおり、IoCコンテナでは意味不明なので、S2ではDIContainer略してダイコン(DICon)と読んでいます。PicoContainerのプレゼン資料も参考になります。EJBコンテナの代替としての側面もあります。 POJO(Plain Old Java Object:ぽじょ) に対し、EJBのようなトランザクション管理やリモート呼び出し機能を透過的に提供しようというものです。ダイコンを使うと次のようなことが実現できるようになります。

  • コンポーネントのパラメータの設定や他のコンポーネントとの依存関係の解決などの構成管理のコードをアプリケーションから排除することで、設定やメンテナンスが簡単になる。
  • ダイコンは、アプリケーションサーバの外でも動作するので、テストが簡単になる。
  • コンポーネントのインターフェースと実装を分離することで、実行時に実装を切り替えることが簡単にできるようになり、Mockによるテストなどが簡単にできるようになる。

それでは、実際にダイコン上で動くコンポーネントを作成してみましょう。コンポーネントといっても、特別なインターフェースを実装する必要はなく、通常のJavaのオブジェクト(POJO)で大丈夫です。今回作成するコンポーネントの仕様は以下のようになります。

  • 自動採番の機能を提供する。
  • 引数として採番キーを受け取るとカウンタを1つ増やした値を返す。
  • 存在しない採番キーが指定された場合は、例外を発生させる。
package examples.dicon.service;

public interface AutoNumber {

    public int next(int numberKey) throws NumberKeyNotFoundRuntimeException;
}

また、DAOのコンポーネントとして以下のような仕様のコンポーネントを使います。

  • 採番キーをもとにカウンタを更新する。更新に成功した場合は1、存在しない採番キーだった場合0を返す。(JDBC APIのStatement.executeUpdate()を想定)
  • 採番キーをもとに最新のカウンタの値を返す。
package examples.dicon.dao;

public interface AutoNumberDao {

    public int increment(int numberKey);

    public int getCurrentNumber(int numberKey);
}

AutoNumberコンポーネントは、AutoNumberDaoコンポーネントの参照を必要とします。参照を取得するためには、コンストラクタやプロパティを使う方法がありますが、今回は、コンストラクタを使います。

AutoNumberImpl

package examples.dicon.service;

import examples.dicon.dao.AutoNumberDao;

public class AutoNumberImpl implements AutoNumber {

    private AutoNumberDao autoNumberDao_;

    public AutoNumberImpl(AutoNumberDao autoNumberDao) {
        autoNumberDao_ = autoNumberDao;
    }

    public int next(int numberKey) throws NumberKeyNotFoundRuntimeException {
        int updateCount = autoNumberDao_.increment(numberKey);
        if (updateCount == 1) {
            return autoNumberDao_.getCurrentNumber(numberKey);
        } else {
            throw new NumberKeyNotFoundRuntimeException(numberKey);
        }
    }
}

コンストラクタの引数であるAutoNumberDaoは誰が設定してくれるのでしょうか?これまでは、アプリケーションのコードで設定するのが普通でした。ダイコンを使った場合はこのような依存関係は、ダイコンが解決してくれます。

それでは、AutoNumberImplをテストしてみましょう。テストするためには、AutoNumberDaoを実装したクラスが必要です。実際にデータベースには接続しなくても良いように、モックを使います。S2ではモックを簡単に作成できるようにMockInterceptorが用意されています。

ダイコンを使うには、S2ContainerImplをnewして使います。

S2Container container = new S2ContainerImpl();

コンポーネントの登録は、S2Container#register(Class コンポーネントのクラス)を使います。

container.register(AutoNumberImpl.class);

AutoNumberDaoのモックを作成して、S2Containerに登録します。コンポーネントのクラスのかわりに直接オブジェクトを登録することもできます。

MockInterceptor mi = new MockInterceptor();
mi.setReturnValue("increment", new Integer(1));
mi.setReturnValue("getCurrentNumber", new Integer(1));
AutoNumberDao dao = (AutoNumberDao) mi.createProxy(AutoNumberDao.class);
container.register(dao);

コンポーネントを取得するには、S2Container#getComponent(Class コンポーネントのクラス)を使います。コンポーネントのクラスは、実装クラスも指定できますがインターフェースのクラスを指定したほうが良いでしょう。

AutoNumber autoNumber = (AutoNumber) container.getComponent(AutoNumber.class);

S2Containerで最低限知らなければいけないのはこれだけです。コンポーネントの依存関係は、型により自動的に解決されます。ただし、すべての型を自動的に解決するのは危険なので、型がインターフェースの場合のみ、自動解決するようになってます。今回の例は、コンストラクタを使ってますが、プロパティを使う場合でも、同様に型により依存関係は自動的に解決されます。

テストケースは以下のようになります。

package test.examples.dicon.service;

import org.seasar.framework.aop.interceptors.MockInterceptor;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.impl.S2ContainerImpl;

import examples.dicon.dao.AutoNumberDao;
import examples.dicon.service.AutoNumber;
import examples.dicon.service.AutoNumberImpl;
import examples.dicon.service.NumberKeyNotFoundRuntimeException;
import junit.framework.TestCase;

public class AutoNumberImplTest extends TestCase {

    public AutoNumberImplTest(String arg0) {
        super(arg0);
    }

    public static void main(String[] args) {
        junit.textui.TestRunner.run(AutoNumberImplTest.class);
    }

    public void testNext() {
        S2Container container = new S2ContainerImpl();
        container.register(AutoNumberImpl.class);
        MockInterceptor mi = new MockInterceptor();
        mi.setReturnValue("increment", new Integer(1));
        mi.setReturnValue("getCurrentNumber", new Integer(1));
        AutoNumberDao dao = (AutoNumberDao) mi.createProxy(AutoNumberDao.class);
        container.register(dao);
        AutoNumber autoNumber = (AutoNumber) container.getComponent(AutoNumber.class);
        assertEquals("1", 1, autoNumber.next(1));
    }

    public void testNextForNumberKeyNotFound() {
        S2Container container = new S2ContainerImpl();
        container.register(AutoNumberImpl.class);
        MockInterceptor mi = new MockInterceptor();
        mi.setReturnValue(new Integer(0));
        AutoNumberDao dao = (AutoNumberDao) mi.createProxy(AutoNumberDao.class);
        container.register(dao);
        AutoNumber autoNumber = (AutoNumber) container.getComponent(AutoNumber.class);
        try {
            autoNumber.next(-1);
            fail("1");
        } catch (NumberKeyNotFoundRuntimeException ex) {
            assertEquals("2", -1, ex.getNumberKey());
        }
    }
}

Dependency Injectionのタイプ

Dependency Injectionには、コンポーネントの構成に必要な値をインターフェースを利用して設定する(Interface Injection)のか、プロパティで設定する(Setter Injection)のか、コンストラクタで設定する(Constructor Injection)のかによって、初期化メソッドで設定する(Method Injection)のかで、タイプが分かれます。Method InjectionはS2のオリジナルです。S2はすべてのタイプとそのハイブリッド型もサポートします。

コンストラクタ・インジェクション

コンポーネントの依存関係は、型によって自動的に解決されますが、 1つの型に複数のコンポーネントが登録されている場合や、 Stringやintなどの基本的な値を設定したい場合、 明示的にコンストラクタを設定する必要があります。例えば、Integerのコンストラクタにintの0を渡したい場合次のようになります。

examples/dicon/xml/ConstructorInjection.dicon

<components>
<component name="foo" class="java.lang.Integer">
<arg>0</arg>
</component> <component class="java.util.Date>
<arg>foo</arg>
</component>
</components>

argタグのボディに設定したい値を記述します。記述の仕方はほとんどJavaと一緒です。詳しくは、OGNLのマニュアルを参照してください。引数が複数ある場合には、argタグを複数定義します。argタグの子タグにcomponentタグを使うこともできます。argタグのボディにコンポーネント名を指定することで、他のコンポーネントを参照することもできます。ダブルコート(")で囲むと文字列と解釈されてしまうので注意してください。この定義を使うサンプルは次(examples.dicon.xml.ConstructorInjectionClient)のようになります。

private static final String PATH =
"examples/dicon/xml/ConstructorInjection.dicon";
S2Container container = S2ContainerFactory.create(PATH); System.out.println(container.getComponent("foo")); System.out.println(container.getComponent(Date.class));

S2Containerを作成するには、S2ContainerFactory.create()の最初の引数にXMLのパスを指定します。XMLはCLASSPATHに含まれている必要があります。コンポーネントは名前でもクラスでも取得することができます。

セッター・インジェクション

Dateのプロパティtimeに0を設定したい場合次のようにします。

examples/dicon/xml/SetterInjection.dicon

<components>
<component class="java.util.Date">
<property name="time">0</property>
</component>
</components>

プロパティが複数ある場合には、propertyタグを複数定義します。propertyタグの子タグにcomponentタグを使うこともできます。プロパティの一部は明示的に指定し、残りは型により自動バインディングさせることも可能です。argタグと同様にpropertyタグのボディにコンポーネント名を指定することで、他のコンポーネントを参照することもできます。この定義を使うサンプルは次(examples.dicon.xml.SetterInjectionClient)のようになります。

private static final String PATH =
"examples/dicon/xml/SetterInjection.dicon";
S2Container container = S2ContainerFactory.create(PATH); System.out.println(container.getComponent(Date.class));

メソッド・インジェクション

コンポーネントによっては、プロパティやコンストラクタだけでなく、メソッドを呼び出して値を設定したい場合もあります。例えば、HashMapのputメソッドを呼び出したい場合次のようにします。

examples/dicon/xml/MethodInjection.dicon

<components>
<component class="java.util.HashMap">
<initMethod name="put">
<arg>"aaa"</arg>
<arg>"111"</arg>
</initMethod>
</component>
</components>

initMethodタグのname属性でメソッドの名前を指定します。argタグの使い方は、コンストラクタの場合と同様です。initMethodタグを複数定義することもできます。この定義を使うサンプルは次(examples.dicon.xml.MethodInjectionClient)のようになります。

private static final String PATH =
"examples/dicon/xml/MethodInjection.dicon";
S2Container container = S2ContainerFactory.create(PATH); Map map = (Map) container.getComponent(Map.class);
System.out.println(map.get("aaa"));
S2Container.destroy()のタイミングで、呼び出されるコンポーネントのメソッドをdestroyMethodタグで指定することもできます。OGNLを使えば、さらに簡単に呼び出すことができます。
<components>
    <component class="java.util.HashMap">
        <initMethod>#self.put("aaa", "111")</initMethod>
        <initMethod>#out.println("Hello")</initMethod>
    <component>
</components>

initMethodタグのボディで、#selfはそのコンポーネント自身を指すので、#self.メソッド名(引数, ...)のようにして呼び出すことができます。また、initMethodの引数がすべてインターフェース型の場合、メソッド名だけを指定し、argタグを省略すると、S2Containerが自動的に対応する型のコンポーネントを探して設定してくれます。#outはSystem.outのことなので、<initMethod>#out.println("Hello")</initMethod>のように呼び出すことができます。

OGNLによるコンポーネントの定義

OGNLを使って、コンポーネントを定義することもできます。OGNLでは、ほとんどJavaのように使うことができます。new する場合は、完全限定クラス名を使います。コンポーネントの名前をインスタンスへの参照として使い、ドット(.)記法でメソッドを呼び出すこともできます。ネストしたメソッドの呼び出しも可能です。class属性は省略することができますが、 自動バインディングを使う場合は、省略せずに指定してください。

<component name="initialContext">new javax.naming.InitialContext()</component>
<component name="dataSource">initialContext.lookup("jdbc/oracle")</component>

AOPの適用

コンポーネントにAOPを適用することもできます。例えば、ArrayListにTraceInterceptorを適用したい場合次のようにします。

<components>
<component name="traceInterceptor"
class="org.seasar.framework.aop.interceptors.TraceInterceptor"/>
<component class="java.util.ArrayList">
<aspect>traceInterceptor</aspect>
</component> <component class="java.util.Date">
<arg>0</arg>
<aspect pointcut="getTime, hashCode">traceInterceptor</aspect>
</component>
</components>

aspectタグのボディでInterceptorの名前を指定します。pointcut属性にカンマ区切りで対象となるメソッド名を指定することができます。pointcut属性を指定しない場合は、コンポーネントが実装しているインターフェースのすべてのメソッドが対象になります。メソッド名には正規表現(JDK1.4のreqex)も使えます。この定義を使うサンプルは次(examples.dicon.xml.AopClient)のようになります。

private static final String PATH =
"examples/dicon/xml/Aop.dicon";
S2Container container = S2ContainerFactory.create(PATH);
List list = (List) container.getComponent(List.class);
list.size();
Date date = (Date) container.getComponent(Date.class);
date.getTime();
date.hashCode();
date.toString();

実行結果は次のようになります。

BEGIN java.util.ArrayList#size()
END java.util.ArrayList#size() : 0
BEGIN java.util.Date#getTime()
END java.util.Date#getTime() : 0
BEGIN java.util.Date#hashCode()
BEGIN java.util.Date#getTime()
END java.util.Date#getTime() : 0
END java.util.Date#hashCode() : 0
BEGIN java.util.Date#getTime()
END java.util.Date#getTime() : 0

コンポーネント定義の分割とインクルード

すべてのコンポーネントを1つのファイルに記述すると、直ぐに肥大化してしまい管理が難しくなります。そのため、コンポーネントの定義を複数に分割する機能と分割された定義をインクルードして1つにまとめる機能がS2Containerにあります。コンポーネントの定義のインクルードは次のようにして行います。

<components>
    <include path="foo.dicon"/>
    <include path="bar.dicon"/>
</components>

foo.diconはCLASSPATHに含まれている必要があります。例えば、WEB-INF/classes/foo.diconに置いてあればOKです。WEB-INF/classes/aaa/foo.diconに置く場合は、<include path="aaa/foo.dicon"/>のように指定します。コンポーネントの検索順は、先ず自分自身に登録されているコンポーネントを探し、見つからない場合は、includeされている順に子供のコンテナに登録されているコンポーネントを検索し、最初に見つかったコンポーネントが返されます。

名前空間

コンポーネントの定義を分割した場合に、複数のコンポーネント定義間で名前が衝突しないように、componentsタグのnamespace属性で名前空間を指定することができます。

foo.dicon

<components namespace="foo">
    <component name="aaa" .../>
    <component name="bbb" ...>
        <arg>aaa</arg>
    </component>
</components>

bar.dicon

<components namespace="bar">
    <component name="aaa" .../>
    <component name="bbb" ...>
        <arg>aaa</arg>
    </component>
    <component name="ccc" ...>
        <arg>foo.aaa</arg>
    </component>
</components>

app.dicon

<components>
    <include path="foo.dicon"/>
    <include path="bar.dicon"/>
</components>

同一のコンポーネント定義内では、名前空間なしで参照できます。他のコンポーネント定義のコンポーネントを参照する場合は、名前空間.をコンポーネント名の頭につけます。foo.aaaとbar.aaaは同じ名前がついていますが、名前空間が異なっているので、違うコンポーネントとして認識されます。慣習として、定義ファイルの名前は、名前空間.diconにすることを推奨します。

インスタンス管理

コンテナで管理されるコンポーネントのインスタンスはデフォルトの場合、Singletonで管理されます。これは、S2Container.getComponent()によって返されるコンポーネントは常に同じだという意味です。S2Container.getComponent()を呼び出すたびに、新たに作成されたコンポーネントを返して欲しい場合は、componentタグのinstance属性にprototypeを指定します。instance属性は、singletonがデフォルトになってます。

プレゼンテーションのフレームワークと組み合わせるときに、プレゼンテーションフレームワークが作成したインスタンスに対して、コンテナで管理されているコンポーネントをセットしたい場合があります。そのようなコンテナ外のコンポーネントに対してDependency Injectionしたいときには、S2Container.injectDependency(Object outerComponent, Class componentClass)、S2Container.injectDependency(Object outerComponent, String componentName)を使います。例をあげると次のようになります。外部で作成されるコンポーネントの場合、instance属性にouterを指定します。

<components>
    <component name="employeeService" class="..."/>
    <component name="home" class="..HomePage" instance="outer">
        <property name=employeeService>employeeService</property>
    </componet>
</components>

プレゼンテーションフレームワークのオブジェクト作成例

IPage page = (IPage) super.instantiatePage(...) ;
if (_container.hasComponentDef(pageName) {
    _container.injectDependency(page, pageName);
}
return page;

ライフサイクル

initMethodやdestroyMethodでコンポーネントのライフサイクルもコンテナで管理することができます。コンテナの開始時(S2Container.init())にinitMethodタグで指定したメソッドが呼び出され、コンテナの終了時(S2Container.destroy())にdestroyMethodタグで指定したメソッドが呼び出されるようになります。initMethodはコンポーネントがコンテナに登録した順番に実行され、destroyMethodはその逆順に呼び出されることになります。instance属性がsingleton以外の場合、destroyMethodを指定しても無視されます。

自動バインディング

コンポーネント間の依存関係は、型がインターフェースの場合、コンテナによって自動的に解決されます。これがS2Containerのデフォルトですが、componentタグのautoBinding属性を指定することで細かく制御することもできます。

autoBinding 説明
auto コンストラクタの引数が明示的に指定されている場合は、それに従います。指定されていない場合、引数のないデフォルトコンストラクタが定義されている場合はそのコンストラクタを使ってコンポーネントが作成されます。デフォルトのコンストラクタがない場合、コンストラクタの引数の数が1以上で、引数の型がすべてインターフェースのコンストラクタを使ってコンポーネントを作成します。プロパティが明示的に指定されている場合はそれに従います。明示的に指定されていないプロパティで型がインターフェースの場合は自動的にバインドします。
constructor コンストラクタの引数が明示的に指定されている場合は、それに従います。指定されていない場合、引数のないデフォルトコンストラクタが定義されている場合はそのコンストラクタを使ってコンポーネントが作成されます。デフォルトのコンストラクタがない場合、コンストラクタの引数の数が1以上で、引数の型がすべてインターフェースのコンストラクタを使ってコンポーネントを作成します。プロパティが明示的に指定されている場合はそれに従います。
property コンストラクタの引数が明示的に指定されている場合は、それに従います。指定されていない場合は、デフォルトのコンストラクタでコンポーネントを作成します。型がインターフェースのプロパティを自動的にバインドします。
none コンストラクタの引数が明示的に指定されている場合は、それに従います。プロパティが明示的に指定されている場合はそれに従います。

コンポーネントでS2Containerを利用する

コンポーネントはコンテナに依存しないことが望ましいのですが、コンポーネントによっては、コンテナのサービスを呼び出したい場合もあるでしょう。S2Container自身もcontainerという名前で、コンテナに登録されているので、arg,propertyタグのボディでcontainerを指定することで、コンテナのインスタンスを取得できます。また、S2Container型のsetterメソッドを定義しておいて自動バインディングで設定することもできます。

誰がS2Containerを作成するのか

誰がS2Containerを作成するのでしょうか。その目的のためにS2ContainerServletが用意されています。S2ContainerServletを使うためには、web.xmlに次の項目を記述します。src/org/seasar/framework/container/servlet/web.xmlに記述例もあります。

<servlet>
<servlet-name>s2servlet</servlet-name>
<servlet-class>org.seasar.framework.container.servlet.S2ContainerServlet</servlet-class>
<init-param>
<param-name>configPath</param-name>
<param-value>app.dicon</param-value>
</init-param>
<load-on-startup/> </servlet>
<servlet-mapping>
<servlet-name>s2servlet</servlet-name>
<url-pattern>/s2servlet</url-pattern>
</servlet-mapping>

configPathでメインとなる定義ファイルを指定します。定義ファイルはWEB-INF/classesにおきます。S2ContainerServletは、他のサーブレットよりもはやく起動されるようにload-on-startupタグを調整してください。S2ContainerServletが起動した後は、SingletonS2ContainerFactory.getContainer()でS2Containerのインスタンスを取得できます。S2Containerのライフサイクルは、S2ContainerServletと連動します。

app.diconの役割

app.diconの役割は、分割されたXML定義をインクルードすることです。app.diconにはコンポーネントの定義はしないようにしてください。サンプルで、他の定義ファイルをインクルードしているケースもありますが、あくまでもサンプルなためで、実際の開発では、インクルードはapp.diconのみで行うことを推奨します。場所は、CLASSPATHに通してあるディレクトリに置く必要があります。通常はWEB-INF/classesにおくと良いでしょう。