Seasar DI Container with AOP

S2Container

S2Containerとは、Dependency Injectionをおこなう軽量コンテナです。Dependency Injectionとは、コンポーネント同士がインターフェースのみで会話するようにし、実際のコンポーネントの生成や設定を、外出しにしようというものです。Dependency InjectionについてはMartin Fowler の「Inversion of Control Containers and the Dependency Injection pattern」で分かりやすく説明されています。複数のコンポーネントを格納・管理しているのでコンテナと呼び、軽量は、EJBのコンテナなどと比べ手軽に使えるよという意味です。

S2Containerの定義

S2Containerの定義は、コンポーネントを組み立てるための設計書のようなものです。形式はXMLで、拡張子は、diconです。中身は次のようになります。

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components.dtd">
<components>
<component name="..." class="...">
...
</component> <component name="..." class="...">
...
</component>
</components>

DOCTYPEは省略できません。このサンプルどおりにコピペしてください。ルートはcomponentsタグです。コンポーネントごとに、componentタグを定義していきます。詳細は、S2Container定義リファレンスを参照してください。

S2Containerの生成

S2Containerは、S2ContainerFactory.create(String path)で作成します。引数pathはCLASSPATHで指定されているディレクトリをルートとする設定ファイルの絶対パスです。例えば、WEB-INF/classes/aaa.dicon の場合は aaa.dicon に、WEB-INF/classes/aaa/bbb/ccc.dicon の場合は aaa/bbb/ccc.dicon になりますセパレータは、WindowsでもUnixでも/です。

private static final String PATH = "aaa/bbb/ccc.dicon";
...
S2Container container = S2ContainerFactory.create(PATH);

コンポーネントの取得

S2Containerからコンポーネントを取り出すには、S2Container.getComponent()を使います。引数には、コンポーネントのクラスもしくはコンポーネント名を指定できます。詳しくは、componentタグを参照してください。コンポーネントのクラスを指定する場合、コンポーネント instanceof クラスがtrueを返すクラスなら指定することができます。しかし、S2Containerの中に指定したクラスを実装しているコンポーネントが複数ある場合、S2Containerは、どのコンポーネントを返せばよいのか判断できないため、TooManyRegistrationRuntimeExceptionが発生します。実装コンポーネントがユニークに決まるクラスを指定してください。コンポーネント名で取得することもできます。その場合も、同一の名前をもつコンポーネントが複数登録されている場合、TooManyRegistrationRuntimeExceptionが発生します。コンポーネント名指定の場合、スペルミスをする可能性もあるので、できるだけクラス指定のほうが良いでしょう。

Dependency Injectionのタイプ

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

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

さっそくコンポーネントを作ってみましょう。先ず最初はインターフェースを考えます。インターフェースと実装を分離することで、コンポーネントの利用者は、インターフェースを知っていれば実装のことは知らなくても済むようになります。また、テストの時には実装をモックに置き換えることで簡単にテストできるようになります。

package examples.dicon;

public interface Hello {

    public void showMessage();
}

次はいよいよ実装です。コンストラクタでメッセージを受け取り、showMessage()で受け取ったメッセージを出力します。

package examples.dicon;

public class HelloConstructorInjection implements Hello {

    private String message;
    
    public HelloConstructorInjection(String message) {
        this.message = message;
    }
    
    public void showMessage() {
        System.out.println(message);
    }
}

メッセージをコンポーネントに設定するのは、S2Containerの仕事です。定義ファイルに基づいてコンポーネントを組み立てます。

examples/dicon/HelloConstructorInjection.dicon

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components.dtd">
<components>
<component name="hello" class="examples.dicon.HelloConstructorInjection">
<arg>"Hello World!"</arg>
</component>
</components>

コンポーネントは、componentタグで組み立てます。class属性でクラス名を指定します。name属性でコンポーネントに名前を付けることもできます。コンストラクタの引数の設定は、componentタグの子タグであるargタグを使います。文字列の場合は、ダブルコーテーション(")で囲みます。それではS2Containerからコンポーネントを取り出し使ってみましょう。

package examples.dicon;

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

public class HelloConstructorInjectionClient {

    private static final String PATH =
        "examples/dicon/HelloConstructorInjection.dicon";
        
    public static void main(String[] args) {
        S2Container container = S2ContainerFactory.create(PATH);
        Hello hello = (Hello) container.getComponent(Hello.class);
        hello.showMessage();
        
        Hello hello2 = (Hello) container.getComponent("hello");
        hello2.showMessage();
    }
}

S2ContainerはS2ContainerFactory.create(String path)を呼び出して作成しますS2Containerからコンポーネントを取り出すには、getComponent()を使います。

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

セッター・インジェクションを試してみましょう。といっても、インターフェースはコンストラクタ・インジェクションの場合と同じです。プロパティに対するゲッター・メソッド、セッター・メソッドを定義する必要はありません。なぜなら、Dependency Injectionするのにコンストラクタを使うのかセッター・メソッドを使うのかは実装の問題だからです。

package examples.dicon;

public interface Hello {

    public void showMessage();
}

次は実装です。セッター・メソッドでメッセージを受け取り、showMessage()で受け取ったメッセージを出力します。

package examples.dicon;

public class HelloSetterInjection implements Hello {

    private String message;
    
    public HelloSetterInjection() {
    }
    
    public void setMessage(String message) {
        this.message = message;
    }

    public void showMessage() {
        System.out.println(message);
    }
}

examples/dicon/HelloSetterInjection.dicon

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components.dtd">
<components>
<component class="examples.dicon.HelloSetterInjection">
<property name="message">"Hello World!"</property>
</component>
</components>

argタグのかわりに、propertyタグを使っている以外は前の例とほとんど同じです。

package examples.dicon;

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

public class HelloSetterInjectionClient {

    private static final String PATH =
        "examples/dicon/HelloSetterInjection.dicon";
        
    public static void main(String[] args) {
        S2Container container = S2ContainerFactory.create(PATH);
        Hello hello = (Hello) container.getComponent(Hello.class);
        hello.showMessage();
    }
}

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

メソッド・インジェクションとは、任意のメソッドを呼び出して、Dependency Injectionするものです。追加のメソッドを複数回呼び出すようなケースが代表的な使い方でしょう。今回の例では、インターフェースはコンストラクタ・インジェクションの場合と同じにしました。

package examples.dicon;

public interface Hello {

    public void showMessage();
}

次は実装です。addMessage(String message)でメッセージを複数回追加して、showMessage()で受け取ったメッセージを出力します。

package examples.dicon;

public class HelloMethodInjection implements Hello {

    private StringBuffer buf = new StringBuffer();
    
    public HelloMethodInjection() {
    }
    
    public void addMessage(String message) {
        this.buf.append(message);
    }

    public void showMessage() {
        System.out.println(buf.toString());
    }
}

examples/dicon/HelloMethodInjection.dicon

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components.dtd">
<components>
<component class="examples.dicon.HelloMethodInjection">
<initMethod name="addMessage">
<arg>"Hello "</arg>
</initMethod>
<initMethod>#self.addMessage("World!")</initMethod>
</component>
</components>

initMethodタグを使って、コンポーネントの任意のメソッドを呼び出します。name属性で、メソッド名を指定します。引数は、argタグを子タグに使います。name属性を省略して、ボディで、OGNL式を使うこともできます。その際、コンポーネント自身は#selfで表します。詳しくは、initMethodタグを参照してください。

package examples.dicon;

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

public class HelloMethodInjectionClient {

    private static final String PATH =
        "examples/dicon/HelloMethodInjection.dicon";
        
    public static void main(String[] args) {
        S2Container container = S2ContainerFactory.create(PATH);
        Hello hello = (Hello) container.getComponent(Hello.class);
        hello.showMessage();
    }
}

S2Container定義の分割とインクルード

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

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

includeタグのpath属性で取り込みたいS2Container定義ファイルのパスを指定します。詳しくは、includeタグを参照してください。コンポーネントの検索順は、先ず自分自身に登録されているコンポーネントを探し、見つからない場合は、includeされている順に子供のS2Containerに登録されているコンポーネントを検索し、最初に見つかったコンポーネントが返されます。

名前空間

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

foo.dicon

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

bar.dicon

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

app.dicon

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

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

インスタンス管理

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

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

instance属性 説明
singleton(default) S2Container.getComponent()を何度呼び出しても同じインスタンスが返されます。
prototype S2Container.getComponent()を呼び出すたびに新たなインスタンスが返されます。
outer コンポーネントのインスタンスは、S2Container外で作成し、Dependency Injectionだけを行います。アスペクトコンストラクタ・インジェクションは適用できません。

ライフサイクル

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

自動バインディング

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

autoBinding 説明
auto(default)

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

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

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

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

誰がS2Containerを作成するのか

これまでは、Javaアプリケーションで、明示的にS2Containerを作成していましたが、Webアプリケーションの場合、誰が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でメインとなるS2Container定義のパスを指定します。定義ファイルはWEB-INF/classesにおきます。S2ContainerServletは、他のサーブレットよりもはやく起動されるようにload-on-startupタグを調整してください。S2ContainerServletが起動した後は、SingletonS2ContainerFactory.getContainer()でS2Containerのインスタンスを取得できます。S2Containerのライフサイクルは、S2ContainerServletと連動します。

app.diconの役割

すべてのS2Container定義のルートになる定義ファイルは、慣例でapp.diconという名前にします。app.diconにはコンポーネントの定義はしないようにしてください。通常はWEB-INF/classesにおくと良いでしょう。

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のregex)も使えます。この定義を使うサンプルは次のようになります。

private static final String PATH =
"examples/dicon/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

S2Container定義リファレンス

DOCTYPE

DOCTYPEは、XML宣言の次に指定します。下記のように指定してください。

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components.dtd">
<components>
<component name="hello" class="examples.dicon.HelloConstructorInjection">
<arg>"Hello World!"</arg>
</component>
</components>

componentsタグ(必須)

ルートのタグになります。

namespace属性(任意)

名前空間を指定することができます。Javaの識別子として使えるものにします

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components.dtd">
<components namespace="hoge">
...
</components>

includeタグ(任意)

分割されたS2Containerの定義を取り込む場合に使います。

path属性(必須)

定義ファイルのパスを指定することができます。CLASSPATHで指定されているディレクトリをルートとする設定ファイルの絶対パスです。例えば、WEB-INF/classes/aaa.dicon の場合は aaa.dicon に、WEB-INF/classes/aaa/bbb/ccc.dicon の場合は aaa/bbb/ccc.dicon になりますセパレータは、WindowsでもUnixでも/です。componentタグの前に記述する必要があります。

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components.dtd">
<components>
<include path="aaa/bbb/ccc.dicon" />
</components>

componentタグ(任意)

コンポーネントを定義します。

class属性(任意)

クラスの完全限定名を指定します。ボディで、OGNL式を使ってコンポーネントを指定した場合は、class属性を省略することができます。OGNL式を使った場合にclass属性を指定すると、型チェックを行います。

name属性(任意)

名前を指定することもできます。Javaの識別子として使えるものにします。詳しくは、コンポーネントの取得を参照してください。

instance属性(任意)

S2Containerがどのようにコンポーネントのインスタンスを管理するのかを指定することができます。singleton(デフォルト)、prototype、outerを指定することができます。詳しくは、インスタンス管理を参照してください。

autoBinding属性(任意)

S2Containerがコンポーネントの依存関係をどのように解決するのかを指定できます。auto(デフォルト)、constructor、property、noneを指定することができます。詳しくは、自動バインディングを参照してください。

argタグ(任意)

componentタグの子タグとして使った場合は、コンストラクタの引数になります。記述した順番でコンストラクタに渡されます。 initMethodTagdestroyMethodTagの子タグとして使った場合は、メソッドの引数になります。記述した順番でメソッドに渡されます。 引数として渡される実際の値は、ボディで、OGNL式を使うか、子タグで、componentタグを使います。

propertyタグ(任意)

componentタグの子タグとして使います。プロパティとして設定される実際の値は、ボディで、OGNL式を使うか、子タグで、componentタグを使います。

name属性(必須)

プロパティ名を指定します。

initMethodタグ(任意)

componentタグの子タグとして使います。引数は、子タグで、argタグを使います。name属性を書かずに、OGNL式を使って、コンポーネントのメソッドを呼び出すこともできます。initMethodタグが定義されているコンポーネント自身を表す#self、System.outを表す#out、System.errを表す#errがinitMethodタグ内だけで有効なオブジェクトとして使えます。

name属性(任意)

メソッド名を指定します。

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components.dtd">
<components>
<component class="java.util.HashMap"> <initMethod name="put"> <arg>"aaa"</arg> <arg>111</arg> </initMethod> <initMethod>#self.put("aaa", 111)</initMethod> <initMethod>#out.println("Hello")</initMethod> </component>
</components>

destroyMethodタグ(任意)

initMethodタグと同様です。

aspectタグ(任意)

アスペクトをコンポーネントに組み込みます。MethodInterceptorの指定は、ボディで、OGNL式を使うか、子タグで、componentタグを使います。

pointcut属性(任意)

カンマ区切りで対象となるメソッド名を指定することができます。pointcutを指定しない場合は、コンポーネントが実装しているインターフェースのすべてのメソッドが対象になります。メソッド名には正規表現(JDK1.4のregex)も使えます。

descriptionタグ(任意)

componentsタグcomponentタグargタグpropertyタグの子タグとしてdescriptionタグを使うことができます。自由に説明を記述できます。

OGNL式

S2Containerでは、式言語としてOGNLを利用しています。XMLの中で、文字列で記述した内容(式)をJavaのオブジェクトに変換するためのものだと思って間違いないと思います。

  • 文字列は、"hoge"のように"で囲みます。
  • charは、'a'のように'で囲みます。
  • 数値は、123のようにそのまま記述します。
  • 論理値は、true,falseのようにそのまま記述します。
  • new java.util.Date(0)のようにクラスの完全限定名でコンストラクタを呼び出すことができます。
  • @java.lang.Math@max(1, 2)のようにstaticなメソッドを呼び出した結果を参照することができます。
  • @java.lang.String@classのようにクラスを参照できます。
  • hoge.toString()のようにコンポーネントのメソッドを呼び出した結果を参照することができます。この例は、どこかでhogeという名前のコンポーネントが定義されているという前提です。
詳しくは、OGNLのマニュアルを参照してください。