IoC(Inversion of Control) は、制御の逆転を意味しますが、いったい何が逆転するのでしょうか。 これまでのフレームワークを使った開発では、アプリケーション側がフレームワークのAPIを呼び出すことが普通でした。あたりまえジャンと思うかもしれません。この場合、正しいAPIを正しいタイミングで呼び出すのは、 アプリケーション側の責任になります。
これが、IoCになるとフレームワークがアプリケーションのコードを呼び出すようになります。 アプリケーション側は、自分のやりたい最小限のコードを書いておけば、フレームワークが適切なタイミングで、アプリケーションのコードを呼び出してくれます。制御をフレームワークにまかせることで、 APIを間違って使ったりすることもなくなります。開発者の責任を減らすことができるため、アプリケーションの品質も向上します。
IoCコンテナは、IoCの考え方をコンポーネントのコンテナに応用したものです。Martin Fowler の「Inversion of Control Containers and the Dependency Injection pattern」やPicoContainerのプレゼン資料で分かりやすく説明されています。EJBコンテナの代替としての側面もあります。 POJO(Plain Old Java Object:ぽじょ) に対し、EJBのようなトランザクション管理やリモート呼び出し機能を透過的に提供しようというものです。IoCコンテナを使うと次のようなことが実現できるようになります。
それでは、実際にIoCコンテナ上で動くコンポーネントを作成してみましょう。コンポーネントといっても、特別なインターフェースを実装する必要はなく、通常のJavaのオブジェクト(POJO)で大丈夫です。今回作成するコンポーネントの仕様は以下のようになります。
package examples.ioc.service;
public interface AutoNumber {
public int next(int numberKey) throws NumberKeyNotFoundRuntimeException;
}
また、DAOのコンポーネントとして以下のような仕様のコンポーネントを使います。
package examples.ioc.dao;
public interface AutoNumberDao {
public int inclement(int numberKey);
public int getCurrentNumber(int numberKey);
}
AutoNumberコンポーネントは、AutoNumberDaoコンポーネントの参照を必要とします。参照を取得するためには、コンストラクタ(Type3)やプロパティ(Type2)を使う方法がありますが、今回は、コンストラクタを使います。
package examples.ioc.service;
import examples.ioc.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_.inclement(numberKey);
if (updateCount == 1) {
return autoNumberDao_.getCurrentNumber(numberKey);
} else {
throw new NumberKeyNotFoundRuntimeException(numberKey);
}
}
}
コンストラクタの引数であるAutoNumberDaoは誰が設定してくれるのでしょうか?これまでは、アプリケーションのコードで設定するのが普通でした。IoCコンテナを使った場合はこのような依存関係は、IoCコンテナが解決してくれます。
それでは、AutoNumberImplをテストしてみましょう。テストするためには、AutoNumberDaoを実装したクラスが必要です。実際にデータベースには接続しなくても良いように、モックとなるDummyAutoNumberDaoを作成します。採番キーが1の場合のみ正常に処理する簡易実装になってます。
package test.examples.ioc.service;
import examples.ioc.dao.AutoNumberDao;
public class DummyAutoNumberDao implements AutoNumberDao {
private int number_;
public int inclement(int numberKey) {
if (numberKey == 1) {
number_++;
return 1;
} else {
return 0;
}
}
public int getCurrentNumber(int numberKey) {
if (numberKey == 1) {
return number_;
} else {
return 0;
}
}
}
SeasarのIoCコンテナを使うには、S2ContainerImplをnewして使います。
S2Container container = new S2ContainerImpl();
コンポーネントの登録は、S2Container#register(Class コンポーネントのクラス)を使います。
container.register(AutoNumberImpl.class); container.register(DummyAutoNumberDao.class);
コンポーネントを取得するには、S2Container#getComponent(Class コンポーネントのクラス)を使います。コンポーネントのクラスは、実装クラスも指定できますがインターフェースのクラスを指定したほうが良いでしょう。
AutoNumber autoNumber = (AutoNumber) container.getComponent(AutoNumber.class);
S2Containerで最低限知らなければいけないのはこれだけです。コンポーネントの依存関係は、型により自動的に解決されます。ただし、すべての型を自動的に解決するのは危険なので、型がインターフェースの場合のみ、自動解決するようになってます。今回の例は、コンストラクタを使ってますが、プロパティを使う場合でも、同様に型により依存関係は自動的に解決されます。XMLベースの設定の仕方もこの後説明します。
テストケースは以下のようになります。
package test.examples.ioc.service;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.impl.S2ContainerImpl;
import examples.ioc.service.AutoNumber;
import examples.ioc.service.AutoNumberImpl;
import examples.ioc.service.NumberKeyNotFoundRuntimeException;
import junit.framework.TestCase;
public class AutoNumberImplTest extends TestCase {
private AutoNumber autoNumber_;
public AutoNumberImplTest(String arg0) {
super(arg0);
}
public static void main(String[] args) {
junit.textui.TestRunner.run(AutoNumberImplTest.class);
}
protected void setUp() throws Exception {
S2Container container = new S2ContainerImpl();
container.register(AutoNumberImpl.class);
container.register(DummyAutoNumberDao.class);
autoNumber_ = (AutoNumber) container.getComponent(AutoNumber.class);
}
public void testNext() {
assertEquals("1", 1, autoNumber_.next(1));
assertEquals("2", 2, autoNumber_.next(1));
}
public void testNextForNumberKeyNotFound() {
try {
autoNumber_.next(-1);
fail("1");
} catch (NumberKeyNotFoundRuntimeException ex) {
assertEquals("2", -1, ex.getNumberKey());
}
}
}
IoCコンテナは、コンポーネントの構成に必要な値をプロパティ(Type2)で設定するのか、コンストラクタ(Type3)で設定するかによって、タイプが分かれます。どちらが良いのかは好みの問題になってしまうのですが、私は、コンストラクタベース(Type3)が良いと考えています。よく言われるType3のメリット・デメリットは以下のようなものです。
私は、コンテナで管理する業務ロジックコンポーネントは、 ステートレスなサービスで他のコンポーネントとはできるだけ疎結合にすべきだと考えているので、コンストラクタの引数がそれほど増えることはないと思ってます。典型的なサービスは、AutoNumberImplのようにDAOへの参照だけを持ち、DAOはDataSourceへの参照だけを持っているような感じになると思います。
他のコンポーネントとの依存関係があまりなければ、テストに時に特定のプロパティだけを設定したいなんてことも問題にはなりません。
循環参照が生じる場合は、サービスの正規化に失敗しているのではないかと思います。そのような場合には、互いに参照し合ってるメソッドを共通サービスにくくりだすなどの対応を取った方がいいと考えます。Type2だと循環参照があっても実装できてしまいますが、Type3だとエラーになってしまうので、確実に気づくことができます。
S2Containerは、Type2、Type3のどちらもサポートします。コンテナによるコンポーネントのメソッド呼び出しというType4(仮称)もサポートします。 Type2,3,4を併用するハイブリット型もOKです。
コンポーネントの依存関係は、型によって自動的に解決されますが、 1つの型に複数のコンポーネントが登録されている場合や、 Stringやintなどの基本的な値を設定したい場合、 手動でコンストラクタを設定する必要があります。例えば、Integerのコンストラクタにintの0を渡したい場合次のようにします。
S2Container container = new S2ContainerImpl();
ComponentDef componentDef = new ComponentDefImpl(Integer.class, "foo");
ArgDef argDef = new ArgDefImpl(new Integer(0));
componentDef.addArgDef(argDef);
container.register(componentDef);
System.out.println(container.getComponent("foo"));
コンポーネントは、ComponentDefImplを使って定義します。コンストラクタの最初の引数には、コンポーネントのクラスを指定します。2番目の引数はコンポーネントの名前を指定します。名前をつけるとS2Container.getComponent(コンポーネント名)のように名前でコンポーネントが取得できます。名前をつけない場合は、 S2Container.getComponent(型)のように型でコンポーネントを取得します。S2Container.register(型)はS2Container.register(new ComponentDefImpl(型))と同じです。
引数は、ArgDefImplを使って定義します。コンストラクタの引数は、コンポーネントに渡したいオブジェクトを指定します。intのようなプリミティブ型を渡したい場合、ラッパークラスを使います。引数が複数ある場合には、ComponentDef.addArgDef()を複数回呼び出します。ArgDef.setChildComponentDef()を使い、再帰的にコンポーネントを組み立てることもできます。
XMLを使って同様のことをより簡単にできるようになります。XMLは次の様に記述します。
<components>
<component name="foo" class="java.lang.Integer">
<arg>0</arg>
</component>
</components>
argタグのボディに設定したい値を記述します。文字列の場合は、シングルコーテーション(')で囲むこと以外は、Javaの場合と同様です。例えば、文字列"abc"を設定したい場合は、'abc'と記述します。XMLで設定する場合のJavaのコーディングは次(examples.ioc.xml.ConstructorByXml)のようになります。
private static final String PATH =
"examples/ioc/xml/ConstructorByXml-config.xml";
S2Container container = S2ContainerFactory.create(PATH); System.out.println(container.getComponent("foo"));
S2Containerを作成するには、S2ContainerFactory.create()の最初の引数にXMLのパスを指定します。XMLはCLASSPATHに含まれている必要があります。
プロパティも手動で設定することができます。例えば、Dateのプロパティtimeに0を設定したい場合次のようにします。
S2Container container = new S2ContainerImpl();
ComponentDef componentDef = new ComponentDefImpl(Date.class);
PropertyDef propertyDef = new PropertyDefImpl("time", new Long(0));
componentDef.addPropertyDef(propertyDef);
container.register(componentDef);
System.out.println(container.getComponent(Date.class));
プロパティは、PropertyDefImplを使って定義します。コンストラクタの最初の引数は、プロパティ名です。2番目の引数はコンポーネントに渡したいオブジェクトを指定します。longのようなプリミティブ型を渡したい場合、ラッパークラスを使います。プロパティが複数ある場合には、ComponentDef.addPropertyDef()を複数回呼び出します。PropertyDef.setChildComponentDef()を使い、再帰的にコンポーネントを組み立てることもできます。
XMLを使って同様のことをより簡単にできるようになります。XMLは次の様に記述します。
<components>
<component class="java.util.Date">
<property name="time">0</property>
</component>
</components>
XMLで設定する場合のJavaのコーディングは次(examples.ioc.xml.PropertyByXml)のようになります。
private static final String PATH =
"examples/ioc/xml/PropertyByXml-config.xml";
S2Container container = S2ContainerFactory.create(PATH); System.out.println(container.getComponent(Date.class));
コンストラクタの引数やプロパティに値を直接指定するのではなく、他のコンポーネントの参照を指定することもできます。例えば、DateのプロパティtimeにIntegerコンポーネントfooを設定したい場合次のようにします。
S2Container container = new S2ContainerImpl();
ComponentDef componentDef = new ComponentDefImpl(Integer.class, "foo");
ArgDef argDef = new ArgDefImpl(new Integer(0));
componentDef.addArgDef(argDef);
container.register(componentDef);
ComponentDef componentDef2 = new ComponentDefImpl(Date.class);
PropertyDef propertyDef = new PropertyDefImpl("time");
propertyDef.setExpression("foo");
componentDef2.addPropertyDef(propertyDef);
container.register(componentDef2);
System.out.println(container.getComponent(Date.class));
他のコンポーネントを参照するには、ComponentDef.setExpression(コンポーネント名)を使います。同様に、ArgDef.setExpression(コンポーネント名)も使えます。
XMLを使って同様のことをより簡単にできるようになります。XMLは次の様に記述します。
<components>
<component name="foo" class="java.lang.Integer">
<arg>0</arg>
</component>
<component class="java.util.Date">
<property name="time">foo</property>
</component>
</components>
propertyタグのボディでコンポーネントの名前を指定します。argタグでも同様にボディで他のコンポーネントを参照することができます。XMLで設定する場合のJavaのコーディングは次(examples.ioc.xml.ReferenceByXml)のようになります。
private static final String PATH =
"examples/ioc/xml/ReferenceByXml-config.xml";
S2Container container = S2ContainerFactory.create(PATH); System.out.println(container.getComponent(Date.class));
コンポーネントによっては、プロパティやコンストラクタだけでなく、メソッドを呼び出して値を設定したい場合もあります。例えば、HashMapのputメソッドを呼び出したい場合次のようにします。
S2Container container = new S2ContainerImpl();
ComponentDef componentDef = new ComponentDefImpl(HashMap.class);
InitMethodDef methodDef = new InitMethodDefImpl("put");
ArgDef arg1Def = new ArgDefImpl("aaa");
ArgDef arg2Def = new ArgDefImpl("111");
methodDef.addArgDef(arg1Def);
methodDef.addArgDef(arg2Def);
componentDef.addInitMethodDef(methodDef);
container.register(componentDef);
System.out.println(container.getComponent(Map.class));
メソッドは、InitMethodDefImplを使って定義します。コンストラクタの引数は、メソッド名です。メソッドの引数はArgDefImplで定義し、InitMethodDef.addArgDef()で追加します。InitMethodDefは複数設定できます。同様に、DestroyMethodDefを使って、S2Container.destroy()のタイミングで、呼び出されるコンポーネントのメソッドを指定することもできます。
XMLを使って同様のことをより簡単にできるようになります。XMLは次の様に記述します。
<components>
<component class="java.util.HashMap">
<initMethod name="put">
<arg>'aaa'</arg>
<arg>'111'</arg>
</initMethod>
</component>
</components>
initMethodタグのname属性でメソッドの名前を指定します。argタグの使い方は、コンストラクタの場合と同様です。XMLで設定する場合のJavaのコーディングは次(examples.ioc.xml.InitMethodByXml)のようになります。
private static final String PATH =同様にdestroyMethodタグを使って、 S2Container.destroy()のタイミングで、呼び出されるコンポーネントのメソッドを指定することもできます。
"examples/ioc/xml/MethodByXml-config.xml";
S2Container container = S2ContainerFactory.create(PATH); Map map = (Map) container.getComponent(Map.class);
System.out.println(map.get("aaa"));
コンポーネントにAOPを適用することもできます。例えば、ArrayListにTraceAdviceを適用したい場合次のようにします。
S2Container container = new S2ContainerImpl(); ComponentDef componentDef = new ComponentDefImpl(ArrayList.class); AspectDef aspectDef = new AspectDefImpl(new TraceAdvice()); componentDef.addAspectDef(aspectDef); container.register(componentDef); List list = (List) container.getComponent(List.class); list.size();
Aspectは、AspectDefImplを使って定義します。コンストラクタの最初の引数は、AroundAdviceのインスタンスです。2番目の引数でPointcutを指定することもできます。Pointcutを指定しなかった場合は、コンポーネントが実装しているインターフェースのすべてのメソッドが対象になります。
XMLを使って同様のことをより簡単にできるようになります。XMLは次の様に記述します。
<components>
<component name="traceAdvice"
class="org.seasar.framework.aop.advices.TraceAdvice"/>
<component class="java.util.ArrayList">
<aspect>traceAdvice</aspect>
</component> <component class="java.util.Date">
<arg>0</arg>
<aspect pointcut="getTime, hashCode">traceAdvice</aspect>
</component>
</components>
aspectタグのボディでAdviceの名前を指定します。pointcut属性にカンマ区切りで対象となるメソッド名を指定することができます。pointcut属性を指定しない場合は、コンポーネントが実装しているインターフェースのすべてのメソッドが対象になります。メソッド名には正規表現(JDK1.4のreqex)も使えます。XMLで設定する場合のJavaのコーディングは次(examples.ioc.xml.AopByXml)のようになります。
private static final String PATH =
"examples/ioc/xml/AopByXml-config.xml";
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();
すべてのコンポーネントを1つのファイルに記述すると、直ぐに肥大化してしまい管理が難しくなります。そのため、コンポーネントの定義を複数に分割する機能と分割された定義をインクルードして1つにまとめる機能がS2Containerにあります。ユースケースごとにコンポーネントを定義していくのがお勧めです。コンポーネントの定義のインクルードは次のようにして行います。
<components>
<include path="ユースケース1のコンポーネントの定義.xml"/>
<include path="ユースケース2のコンポーネントの定義.xml"/>
</components>
ユースケース1のコンポーネントの定義.xmlはCLASSPATHに含まれている必要があります。例えば、WEB-INF/classes/ユースケース1のコンポーネントの定義.xmlに置いてあればOKです。WEB-INF/classes/aaa/hoge.xmlに置く場合は、<include path="aaa/hoge.xml"/>のように指定します。
コンテナで管理されるコンポーネントのインスタンスはデフォルトの場合、Singletonで管理されます。これは、S2Container.getComponent()によって返されるコンポーネントは常に同じだという意味です。S2Container.getComponent()を呼び出すたびに、新たに作成されたコンポーネントを返して欲しい場合は、componentタグのinstance属性にprototypeを指定します。instance属性は、singletonがデフォルトになってます。
initMethodやdestroyMethodでコンポーネントのライフサイクルもコンテナで管理することができます。コンテナの開始時(S2Container.init())にinitMethodタグで指定したメソッドが呼び出され、コンテナの終了時(S2Container.destroy())にdestroyMethodタグで指定したメソッドが呼び出されるようになります。initMethodはコンポーネントがコンテナに登録した順番に実行され、destroyMethodはその逆順に呼び出されることになります。instance属性がprototypeの場合、destroyMethodを指定しても無視されます。
コンポーネント間の依存関係は、型がインターフェースの場合、コンテナによって自動的に解決されます。これがS2Containerのデフォルトですが、componentタグのautoBinding属性を指定することで細かく制御することもできます。
| autoBinding | 説明 |
|---|---|
| auto | コンストラクタの引数が明示的に指定されている場合は、それに従います。プロパティが明示的に指定されている場合はそれに従います。どちらも指定されていない場合、コンストラクタの引数の数が1以上で、引数の型がすべてインターフェースのコンストラクタを探します。見つかった場合には、そのコンストラクタでコンポーネントを作成します。見つからなかった場合は、デフォルトのコンストラクタでコンポーネントを作成し、型がインターフェースのプロパティを自動的にバインドします。 |
| constructor | コンストラクタの引数が明示的に指定されている場合は、それに従います。指定されていない場合、コンストラクタの引数の数が1以上で、引数の型がすべてインターフェースのコンストラクタを探します。見つかった場合には、そのコンストラクタでコンポーネントを作成します。見つからなかった場合は、デフォルトのコンストラクタでコンポーネントを作成します。 |
| property | デフォルトのコンストラクタでコンポーネントを作成し、型がインターフェースのプロパティを自動的にバインドします。 |
| none | コンストラクタの引数が明示的に指定されている場合は、それに従います。プロパティが明示的に指定されている場合はそれに従います。 |