S2Containerとは、Dependency Injectionをおこなう軽量コンテナのSeasar2における実装です。Dependency Injectionとは、コンポーネントが別のコンポーネントをインスタンス変数に持つなどして利用(依存)している場合に、その依存性の解決(インスタンス変数の設定)をコンポーネントのソースから取り除き、第3者にまかそうというものです。その第3者がS2Containerになります。複数のコンポーネントの器になって管理しているのでコンテナと呼んでいます。軽量コンテナの軽量は、EJBのコンテナのような重量コンテナとは違うんだよという意味です。Dependency InjectionについてはMartin Fowler の「Inversion of Control Containers and the Dependency Injection pattern」で分かりやすく説明されています。
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定義リファレンスを参照してください。
S2ContainerFactory.create(String path)でS2Containerを作成します。path属性はファイルがCLASSPATHに通されているディレクトリの直下にある場合は、ファイル名そのものになります。例えば、WEB-INF/classes/aaa.diconの定義を元にS2Containerを作成する場合、pathはaaa.diconになります。CLASSPATHに通されているディレクトリのサブディレクトリにファイルがある場合、pathは、ディレクトリのセパレータに/を使って表します。例えば、WEB-INF/classes/aaa/bbb/ccc.diconの場合、pathは、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には、コンポーネントの構成に必要な値をコンストラクタで設定する(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();
}
}
すべてのコンポーネントを1つのファイルに記述すると、直ぐに肥大化してしまい管理が難しくなります。そのため、コンポーネントの定義を複数に分割する機能と分割された定義をインクルードして1つにまとめる機能がS2Containerにあります。S2Container定義ファイルのインクルードは次のようにして行います。
<components>
<include path="foo.dicon"/>
<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">
<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>
同一のコンポーネント定義内では、名前空間なしで参照できます。他のS2Container定義のコンポーネントを参照する場合は、名前空間.をコンポーネント名の頭につけます。foo.aaaとbar.aaaは同じ名前がついていますが、名前空間が異なっているので、違うコンポーネントとして認識されます。慣習として、定義ファイルの名前は、名前空間.diconにすることを推奨します。
S2Container管理されるコンポーネントのインスタンスはデフォルトの場合、Singletonで管理されます。これは、S2Container.getComponent()によって返されるコンポーネントは常に同じだという意味です。S2Container.getComponent()を呼び出すたびに、新たに作成されたコンポーネントを返して欲しい場合は、componentタグのinstance属性にprototypeを指定します。instance属性は、singletonがデフォルトになってます。
プレゼンテーションのフレームワークと組み合わせるときに、プレゼンテーションフレームワークが作成したインスタンスに対して、S2Containerで管理されているコンポーネントをセットしたい場合があります。そのようなS2Container外のコンポーネントに対してDependency Injectionしたいときには、S2Container.injectDependency(Object outerComponent, Class componentClass)、S2Container.injectDependency(Object outerComponent, String componentName)を使います。
initMethodやdestroyMethodでコンポーネントのライフサイクルもコンテナで管理することができます。S2Containerの開始時(S2Container.init())にinitMethodタグで指定したメソッドが呼び出され、S2Containerの終了時(S2Container.destroy())にdestroyMethodタグで指定したメソッドが呼び出されるようになります。initMethodはコンポーネントがコンテナに登録した順番に実行され、destroyMethodはその逆順に呼び出されることになります。instance属性がsingleton以外の場合、destroyMethodを指定しても無視されます。
コンポーネント間の依存関係は、型がインターフェースの場合、コンテナによって自動的に解決されます。これがS2Containerのデフォルトですが、componentタグのautoBinding属性を指定することで細かく制御することもできます。
autoBinding |
説明 |
auto |
コンストラクタの引数が明示的に指定されている場合は、それに従います。指定されていない場合、引数のないデフォルトコンストラクタが定義されている場合はそのコンストラクタを使ってコンポーネントが作成されます。デフォルトのコンストラクタがない場合、コンストラクタの引数の数が1以上で、引数の型がすべてインターフェースのコンストラクタを使ってコンポーネントを作成します。プロパティが明示的に指定されている場合はそれに従います。明示的に指定されていないプロパティで型がインターフェースの場合は自動的にバインドします。 |
constructor |
コンストラクタの引数が明示的に指定されている場合は、それに従います。指定されていない場合、引数のないデフォルトコンストラクタが定義されている場合はそのコンストラクタを使ってコンポーネントが作成されます。デフォルトのコンストラクタがない場合、コンストラクタの引数の数が1以上で、引数の型がすべてインターフェースのコンストラクタを使ってコンポーネントを作成します。プロパティが明示的に指定されている場合はそれに従います。 |
property |
コンストラクタの引数が明示的に指定されている場合は、それに従います。指定されていない場合は、デフォルトのコンストラクタでコンポーネントを作成します。型がインターフェースのプロパティを自動的にバインドします。 |
none |
コンストラクタの引数が明示的に指定されている場合は、それに従います。プロパティが明示的に指定されている場合はそれに従います。 |
コンポーネントはS2Containerに依存しないことが望ましいのですが、コンポーネントによっては、S2Containerのメソッドを呼び出したい場合もあるでしょう。S2Container自身もcontainerという名前で、登録されているので、arg,propertyタグのボディでcontainerを指定することで、コンテナのインスタンスを取得できます。また、S2Container型のsetterメソッドを定義しておいて自動バインディングで設定することもできます。
これまでは、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にはコンポーネントの定義はしないようにしてください。。場所は、CLASSPATHに通してあるディレクトリに置く必要があります。通常はWEB-INF/classesにおくと良いでしょう。
コンポーネントに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.AopClient)のようになります。
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
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>
ルートのタグになります。namespace属性で名前空間を指定することができます。
<?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>
分割されたS2Containerの定義を取り込む場合に使います。path属性で定義ファイルのパスを指定することができます。path属性はファイルがCLASSPATHに通されているディレクトリの直下にある場合は、ファイル名そのものになります。例えば、WEB-INF/classes/aaa.diconの定義をincludeする場合、path属性の値はaaa.diconになります。CLASSPATHに通されているディレクトリのサブディレクトリにファイルがある場合、path属性の値は、ディレクトリのセパレータに/を使って表します。例えば、WEB-INF/classes/aaa/bbb/ccc.diconの場合、path属性の値は、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属性で名前を指定することもできます。コンポーネントの取得を参照してください。instance属性で、S2Containerがどのようにコンポーネントのインスタンスを管理するのかを指定することができます。singleton、prototype、outerを指定することができ、デフォルトは、singletonになっています。詳しくは、インスタンス管理を参照してください。autoBinding属性で、S2Containerがコンポーネントの依存関係をどのように解決するのかを指定できます。自動バインディングを参照してください。
componentタグの子タグとして使った場合は、コンストラクタの引数になります。記述した順番でコンストラクタに渡されます。
initMethodTag、destroyMethodTagの子タグとして使った場合は、メソッドの引数になります。記述した順番でメソッドに渡されます。
引数として渡される実際の値は、ボディで、OGNL式を使うか、子タグで、componentタグを使います。
componentタグの子タグとして使います。name属性でプロパティ名を指定します。プロパティとして設定される実際の値は、ボディで、OGNL式を使うか、子タグで、componentタグを使います。
componentタグの子タグとして使います。name属性でメソッド名を指定します。引数は、子タグで、argタグを使います。name属性を書かずに、OGNL式を使って、コンポーネントのメソッドを呼び出すこともできます。initMethodタグが定義されているコンポーネント自身を表す#self、System.outを表す#out、System.errを表す#errがinitMethodタグないだけで有効なオブジェクトとして使えます。
<?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>
initMethodタグと同様です。
アスペクトをコンポーネントに組み込みます。pointcut属性でカンマ区切りで対象となるメソッド名を指定することができます。pointcut属性を指定しない場合は、コンポーネントが実装しているインターフェースのすべてのメソッドが対象になります。メソッド名には正規表現(JDK1.4のreqex)も使えます。MethodInterceptorの指定は、ボディで、OGNL式を使うか、子タグで、componentタグを使います。
componentsタグ、componentタグ、argタグ、propertyタグの子タグとしてdescriptionタグを使うことができます。自由に説明を記述できます。
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のマニュアルを参照してください。
|