|
DIContainerとは、Dependency Injectionをおこなう軽量コンテナのことで、IoCコンテナといわれることもあります。Dependency InjectionについてはMartin Fowler の「Inversion of Control Containers and the Dependency Injection pattern」で分かりやすく説明されています。そこで説明されているとおり、IoCコンテナでは意味不明なので、S2ではDIContainer略してダイコン(DICon)と読んでいます。EJBコンテナの代替としての側面もあります。 POJO(Plain Old Java Object:ぽじょ) に対し、EJBのようなトランザクション管理やリモート呼び出し機能を透過的に提供しようというものです。
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では、ほとんどJavaのように使うことができます。new する場合は、完全限定クラス名を使います。コンポーネントの名前をインスタンスへの参照として使い、ドット(.)記法でメソッドを呼び出すこともできます。ネストしたメソッドの呼び出しも可能です。class属性は省略することができますが、
自動バインディングを使う場合は、省略せずに指定してください。
<component name="initialContext">new javax.naming.InitialContext()</component>
<component name="dataSource">initialContext.lookup("jdbc/oracle")</component>
コンポーネントに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自身もcontainerという名前で、コンテナに登録されているので、arg,propertyタグのボディでcontainerを指定することで、コンテナのインスタンスを取得できます。また、S2Container型のsetterメソッドを定義しておいて自動バインディングで設定することもできます。
誰が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の役割は、分割されたXML定義をインクルードすることです。app.diconにはコンポーネントの定義はしないようにしてください。サンプルで、他の定義ファイルをインクルードしているケースもありますが、あくまでもサンプルなためで、実際の開発では、インクルードはapp.diconのみで行うことを推奨します。場所は、CLASSPATHに通してあるディレクトリに置く必要があります。通常はWEB-INF/classesにおくと良いでしょう。
|