Last modified: Mon Sep 05 18:27:56 JST 2005
DI(Dependecy Injection)は、日本語では「依存性の注入」と訳されていますが、 PicoContainer, SpringではIoC(Inverse of Control)日本語では「制御の逆転」 と呼ばれていました。
IoCとは、アプリケーションが部品であるコンポーネントを生成し組み立てる 代わりに、コンテナーがコンポーネントを生成し組み立てた後、アプリケーション に渡すので「コンポーネントを生成し組み立てる」という制御がアプリケーション からコンテナーに移行したためにこのように名付けられました。
しかし、「制御の逆転」と言われても意味が伝わりにくいため、 Matin Fowler氏(2004)[4]で、DI(Dependecy Injection)と呼ばれるようになりました。
DIがどのようなものなのかを、通常のアプリケーションでの部品の組み立て方とDIを使った場合を例に図を使って説明します。
DIを使わない通常のアプリケーションで、部品A、部品Bを生成する場合を図[通常のアプリケーション]に示します。
アプリケーションAのソースコードには、明示的に部品Aのコンストラクタが記述され、 同様に部品Aのソースコードには部品Bのコンストラクタを明記しなくてなりません。
DIを使ってコンフィグファイルから部品A,部品Bを生成する場合を図[DIを使ったアプリケーション]に示します。
アプリケーションAのソースコードにはコンテナーのコンストラクタとコンフィグファイル を指定しますが、もはや部品Aのコンストラクタは指定しません。 同様に部品Aも部品Bを生成する必要は無くなります。
Matin Fowler氏(2004)[4]ではDIのタイプとして、
が紹介されています。
コンストラクタ注入タイプとは、コンストラクタの引数に注入する属性を渡す方式です。 渡す属性が増えれば、それに対応したコンストラクタが必要になります。
Setter注入タイプとは、属性のSetterメソッドを使って注入する方式です。注入する 属性は、SetterメソッドをPublicで公開するだけです。
インタフェース注入タイプは、注入するためのインタフェースを定義し、そのインタフェース を通じて属性を注入する方式です。初期のDIフレームワークであるAvalonで採用されている 方式です。
Springでは、コンストラクタ注入タイプとSetter注入タイプをサポートしています。
DIを使うことによって享受することができる恩恵を以下に示します。
Springを使って簡単なサンプルプログラム(例題1)でDIを試してみましょう。
例題1では、複数のメンバーを管理し、登録、検索、プリントする機能を提供するMemoryMemberDAOを作成します。
図[DIを使ったアプリケーション]の部品Aに相当するのがDAOで、部品Bに相当するのが、 メンバーのMAPです。
最初にメンバーのクラスMember.javaを作成します。
Memberクラスは、メンバーを一意に識別するidと名前(name)及び住所(address)を属性に持ち、それらのgetter/setterから構成されたクラスです(リスト[Member]を参照)。
1: package diaop.model; 2: 3: public class Member { 4: private Integer id; 5: private String name; 6: private String address; 7: // Eclipse で自動生成された getter/setter 8: public String getAddress() { 9: return address; 10: } 11: public void setAddress(String address) { 12: this.address = address; 13: } 14: public Integer getId() { 15: return id; 16: } 17: public void setId(Integer id) { 18: this.id = id; 19: } 20: public String getName() { 21: return name; 22: } 23: public void setName(String name) { 24: this.name = name; 25: } 26: }
メンバーを管理するためにインタフェースとしてIMemberに登録(insertMember)、検索(findMember, findAllMember)、プリント(printMember)機能を定義します(リスト[IMember]を参照)。
1: package diaop.dao; 2: 3: import diaop.model.Member; 4: 5: public interface IMember { 6: void insertMember(Member member); 7: void deleteMember(Member member); 8: Member findMember(Integer id); 9: Member[] findAllMembers(); 10: String printMember(Member member); 11: }
メモリ上でメンバを管理するDAOとして、MemoryMemberDaoをリスト[MemoryMemberDao]のように定義します。
6行目にメンバ情報を入れるMapとして、属性membersを定義していますが、ソースにはsetMembersでmembersを設定する部分がありません。このmembersがDIによって注入される属性です。
1: package diaop.dao; 2: import java.util.Map; 3: import diaop.model.Member; 4: 5: public class MemoryMemberDao implements IMember { 6: private Map members; 7: public void insertMember(Member member) { 8: members.put(member.getId().toString(), member); 9: } 10: public void deleteMember(Member member) { 11: members.remove(member.getId().toString()); 12: } 13: public Member findMember(Integer id) { 14: return (Member)members.get(id.toString()); 15: } 16: public Member[] findAllMembers() { 17: return (Member[])members.values().toArray(new Member[members.size()]); 18: } 19: public String printMember(Member member) { 20: if (member != null) { 21: StringBuffer buf = new StringBuffer("MEMBER:"); 22: if (member.getId() != null) buf.append(" ID=" + member.getId()); 23: if (member.getName() != null) buf.append(" NAME=" + member.getName()); 24: if (member.getAddress() != null) buf.append(" ADDRESS=" + member.getAddress()); 25: return (buf.toString()); 26: } 27: else { 28: return ("NULL"); 29: } 30: } 31: public void setMembers(Map members) { 32: this.members = members; 33: } 34: }
SpringではXML形式のコンフィグファイルからDIを埋め込みます。コンフィグファイルの構造をリスト[コンフィグファイル-1.xml]を例に説明します(詳しくはSpringのリファレンスマニュアルを参照してください)。
1: <?xml version="1.0" encoding="Windows-31J"?> 2: <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 3: "http://www.springframework.org/dtd/spring-beans.dtd"> 4: <beans> 5: <!-- メンバ takemoto を定義 --> 6: <bean id="takemoto" class="diaop.model.Member"> 7: <property name="id"> 8: <value>1</value> 9: </property> 10: <property name="name"> 11: <value>たまねぎ</value> 12: </property> 13: <property name="address"> 14: <value>中野区</value> 15: </property> 16: </bean> 17: <!-- memberDao を定義 --> 18: <bean id="memberDao" 19: class="diaop.dao.MemoryMemberDao"> 20: <!-- membersにマップを注入 --> 21: <property name="members"> 22: <map> 23: <entry key="1"> 24: <!-- メンバ takemoto を参照 --> 25: <ref bean="takemoto"/> 26: </entry> 27: </map> 28: </property> 29: </bean> 30: </beans>
コンフィグファイルを作成するのに必要な構文はわずかです。
propertyタグには、何通りかの指定方法がありますので、例を見ながら使い方を 説明します。
valueタグには、int, float, Integer, Stringの値を指定します。文字列からの 型変換は Spring が自動的に行います。 nullをセットしたいときには、 <null/> を使用します。
属性にbeanをセットする場合、beanタグを使って新たにBeanを定義する方法と refタグを使って他の定義を参照する方法があります リスト[コンフィグファイル-1.xml]の25行目では、refタグを使っていますが、 これをbeanタグを使って定義するとリスト[entry内のbeanタグ定義] のように記述することができます。この場合他のbeanから参照されない ようにするために、beanタグのid属性を省略することもできます。
<entry key="1"> <bean class="diaop.model.Member"> <property name="id"> <value>1</value> </property> <property name="name"> <value>たまねぎ</value> </property> <property name="address"> <value>中野区</value> </property> </bean> </entry>
propertyにコレクションをセットするには、list, set, map, propsを 使用します。 表[Springがサポートしているコレクション]にSpringがサポートしているコレクションと対応する javaの型を示します。
XMLの定義 | Javaの型 |
---|---|
listタグ | java.util.Listと配列 |
setタグ | java.util.Set |
mapタグ | java.util.Map |
propsタグ | java.util.Properties |
<list> <!-- オブジェクト定義 --> <bean class="diaop.model.Member"/> <bean class="diaop.model.Order"/> <bean class="diaop.model.OrderItem"/> <bean class="diaop.model.Product"/> </list>
例題1のmainをリスト[例題1のmainメソッド]に示します。
1: package diaop.app; 2: import org.springframework.beans.factory.BeanFactory; 3: import org.springframework.context.support.ClassPathXmlApplicationContext; 4: import diaop.dao.IMember; 5: import diaop.model.Member; 6: 7: public class Ex_1 { 8: public static void main(String[] args) { 9: // コンフィグファイルからコンテナを定義 10: BeanFactory factory = 11: new ClassPathXmlApplicationContext("/" + args[0]); 12: // コンテナからmemberDaoを取得 13: IMember dao = (IMember)factory.getBean("memberDao"); 14: // findMember, printmemberのテスト 15: Member takemoto = dao.findMember(new Integer(1)); 16: System.out.println(dao.printMember(takemoto)); 17: // insertMemberのテスト 18: Member hanako = new Member(); 19: hanako.setId(new Integer(2)); 20: hanako.setName("華子"); 21: hanako.setAddress("杉並区"); 22: dao.insertMember(hanako); 23: // findAllMemberのテスト 24: Member[] all = dao.findAllMembers(); 25: for (int i = 0; i < all.length; i++) { 26: System.out.println(dao.printMember(all[i])); 27: } 28: // DBを初期状態に戻す 29: dao.deleteMember(hanako); 30: } 31: }
実行結果を図[サンプルプログラム-1の実行結果]に示します。
MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区 MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区 MEMBER: ID=2 NAME=華子 ADDRESS=杉並区
実行に必要なjarファイルは、
です。
Springコンテナで定義されたBeanは、デフォルトではシングルトン(コンテナ内で1個のインスタンスしか持たない)として生成されます。
コンテナからBeanを取り出す度に新しいインスタンスを生成する場合には、 リスト[プロトタイプの例] のようにsingleton="false"とします。コンテナからprotoMemberとして 取り出したMemberインスタンスには、addressとして「中野区」がセット されているので、必要な属性をセットしたプロトタイプデザインパターン と同じ役割をします。
<bean id="protoMember" class="diaop.model.Member" singleton="false"/> <property name="address"> <value>中野区</value> </property> </bean>
また、シングルトンをデザインパターンに忠実に実装するとMemberクラスは、
1: package diaop.model; 2: 3: public class Member { 4: private static Member instance = new Member(); 5: 6: protected Member() { 7: } 8: 9: public static Member getInstance() { 10: return instance; 11: } 12: 13: private Integer id; 14: private String name; 15: private String address; 16: // 自動生成されたgetter/setterは省略 17: }となり、デフォルトコンストラクタがPublic ではないのでSpringから 使えなくなってしまい、再利用もできないクラスになってしまいます。
Springのシングルトンの機能を使った方がクラスの実装も簡単になり、再利用も可能となります。
同じインタフェースに対する異なった実装の例として、ユーザ認証を例に見てみましょう。Springに対応しているAcegi Security SystemではインタフェースAuthenticationManagerの種類は表[AuthenticationManagerの種類]のようなっています。
認証方式 | 用途 | 規模 |
---|---|---|
インメモリDao認証 | 開発とテスト | 小 |
Dao認証(認証用に別途DBを持たない) | 開発と運用 | 中 |
Dao認証(認証用に別途DBを使用) | 運用 | 中 |
パスワードを暗号化したDao認証 | 運用 | 中 |
LDAPを使った認証 | 運用 | 大 |
このように多様な認証要求にコンフィグファイルの変更のみで対応できるところがDIのすばらしいところだと感じます。この他にもセキュリティレベルでログの出力形式を変えたいときにも便利です。
20年前にCox氏が[6]で提唱したソフトウェアICという考え方が、DIによって一気に普及する予感がします。
javaのクラスを入れ替えることは、同じ規格のICを消費電力、処理能力、価格 等を検討して使い分けるのとまったく同じ感覚です。DIはまさに 「コロンブスの卵」です。
Springの構成をリファレンスマニュアル[5]から引用します(図[Spring の構成])。
Spring Coreがベースとなっており、各コンポーネントが独立しているため、必要なものだけを選択して使用することができます。各コンポーネントがどのような処理をするかを簡単に説明します。
Spring構成をその融合方式で見てみると
のタイプに分類できます。
Springの拡張性と柔軟性はコンポーネントに対する共通の処理を自由に組み込むことができることに由来しています。共通処理に於いてプロキシーを生成することでプロキシーを使った融合、AOPを使った融合を実現しているのです。
メソッドが呼ばれる前にメソッド名を出力する BeforeLogProxy を 使って Spring がどのようにプロキシーを生成し、AOPを実現して いるのか覗いてみましょう。
コンテナー内のコンポーネントBeanが生成される直前と属性が セットされた直後に呼び出される共通メソッドが BeanPostProcessor インタフェースに定義されています。
public interface BeanPostProcessor { public Object postProcessAfterInitialization(Object bean, String beanName); public Object postProcessBeforeInitialization(Object bean, String beanName); }
また、BeforeLogProxyはプロキシーなので、InvocationHandlerインタフェース の invoke メソッドも実装する必要があります。 BeforeLogProxy のソースをリスト[BeforeLogProxy]に示します。
1: package diaop.proxy; 2: import java.lang.reflect.*; 3: import org.springframework.beans.BeansException; 4: import org.springframework.beans.factory.config.BeanPostProcessor; 5: 6: public class BeforeLogProxy 7: implements BeanPostProcessor, InvocationHandler { 8: private Object orgObj; 9: private String targetName; 10: 11: public Object invoke(Object proxy, Method method, Object[] args) 12: throws Throwable { 13: System.out.println("Enter: " + method.getName()); 14: return method.invoke(orgObj, args); 15: } 16: public Object postProcessAfterInitialization(Object bean, String beanName) 17: throws BeansException { 18: if (beanName.equals(targetName)) { 19: orgObj = bean; 20: return Proxy.newProxyInstance( 21: bean.getClass().getClassLoader(), 22: bean.getClass().getInterfaces(), 23: this); 24: } 25: else { 26: return bean; 27: } 28: } 29: public Object postProcessBeforeInitialization(Object bean, String beanName) 30: throws BeansException { 31: // 何もしないのでオリジナルを返す 32: return bean; 33: } 34: public void setTargetName(String string) { 35: targetName = string; 36: } 37: }
後はコンフィグファイルにリスト[BeforeLogProxyのコンフィグ] を追加します。 属性 targetName に memberDao をセットしているだけです。
<!-- ログプロキシー --> <bean id="logProxy" class="diaop.proxy.BeforeLogProxy"> <property name="targetName"> <value>memberDao</value> </property> </bean>
実行結果を図[例題2(BeforeLogProxy)の実行結果] に示します。各メソッド呼び出しの前に「Enter: メソッド名」が出力 されているのが分かります。
Enter: findMember Enter: printMember MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区 Enter: insertMember Enter: findAllMembers Enter: printMember MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区 Enter: printMember MEMBER: ID=2 NAME=華子 ADDRESS=杉並区
Springでの永続化の実装は、JDBC, Hibernate等の既存機能をDIに組み入れる場合の良い例題です。
Spring では様々なO/Rマッピングを
Spring で定義されているデータアクセス例外を表[Spring DAO例外]に示します。
例外 | 発生の原因 |
---|---|
CleanupFailureDataAccessException | DBへの操作は成功したが、DBリソースの解放に失敗した場合 |
DataAccessResourceFailureException | 接続以外の資源へのアクセスに失敗した場合 |
DataIntegrityViolationException | 挿入、更新の結果データの一貫性が保てなくなった場合 |
DataRetrievalFailureException | データの取得に失敗した場合 |
DeadlockLoserDataAccessException | カレントプロセスがデッドロックで失敗した場合 |
IncorrectUpdateSemanticsData-AccessException | データの更新時に意図しない事象が発生した場合 (トランザクション処理でロールバックできない時) |
InvalidDataAccessApiUsageException | JAVA API によるデータアクセスが不適当な場合 |
InvalidDataAccessResourceUsage-Exception | データアクセス資源の利用が不適当な場合 (SQL文法間違い等) |
OptimisticLockingFailureException | O/RマッピングツールなどDBが直接検出不可能な例外が発生した場合 |
TypeMismatchDataAccessException | javaの型とDBの型が一致しない場合 |
UncategorizedDataAccessException | 特定不可能な例外が発生した場合 |
最初に節[MemoryMemberDAO]で作成したMember DAOをSpringの提供するHibernateTemplateを使ったDAOに移行してみます。
HibernateではDBの値を保持するPOJO(Plain Old Java Object)と同じディレクトリに
POJOクラス名.hbm.xml
の名称のマッピングファイルを作成します。
永続化で使用するテーブルは、以下の方針で作成しました。
CREATE TABLE T_MEMBER(ID INTEGER NOT NULL PRIMARY KEY,ADDRESS VARCHAR,NAME VARCHAR)
Member クラスに対応した Member.hbm.xml は、以下のようになります。
1: <?xml version="1.0" encoding="Windows-31J"?> 2: <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN" 3: "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"> 4: <hibernate-mapping> 5: <class 6: name="diaop.model.Member" 7: table="T_MEMBER"> 8: <id name="id"> 9: <generator class="increment"/> 10: </id> 11: <property name="address"/> 12: <property name="name"/> 13: </class> 14: </hibernate-mapping>id タグで
<generator class="increment"/>
とある部分を除けば、popertiy タグに各属性名がセットされているだけです。
(1)
<property name="foo" column="bar"/>
のようにcolumn属性を追加します。Hibernateを使ったメンバーDAOは、HibernateTemplateを使って非常に簡単に作成できます。HibernateMemberDaoのソースをに示します。
1: package diaop.dao; 2: import java.util.List; 3: import org.springframework.orm.hibernate.HibernateTemplate; 4: import diaop.model.Member; 5: 6: public class HibernateMemberDao extends MemoryMemberDao 7: implements IMember { 8: protected HibernateTemplate template; 9: 10: public Member[] findAllMembers() { 11: List list = template.loadAll(Member.class); 12: return (list != null 13: ? (Member[])list.toArray(new Member[list.size()]) 14: : null); 15: } 16: public void deleteMember(Member member) { 17: template.delete(member); 18: } 19: public Member findMember(Integer id) { 20: return ((Member)template.load(Member.class, id)); 21: } 22: public void insertMember(Member member) { 23: template.save(member); 24: } 25: public void setTemplate(HibernateTemplate template) { 26: this.template = template; 27: } 28: }
SpringコンフィグファイルをHibernateTemplateを使用できるように変更します。
最初にデータベースの定義を記述したプロパティファイル jdbc.properties を リスト[jdbc.properties]の様に定義します。 (2)
db.url=jdbc:hsqldb:hsql://localhost db.driver=org.hsqldb.jdbcDriver db.username=sa db.password=
コンフィグファイルの変更はリスト[コンフィグファイルのHibernate対応への変更部分]の部分です。
33-37行目mappingDirectoryLocationsでマッピングファイルの置かれているディレクトリを指定します
classpath:/diaop/model
のようにパッケージ名のピリオド.を/で区切って指定します
1: <!-- プロパティ配置 --> 2: <bean id="propertyConfigurer" 3: class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> 4: <property name="locations"> 5: <list> 6: <value>jdbc.properties</value> 7: </list> 8: </property> 9: </bean> 10: <!-- DataSource --> 11: <bean id="dataSource" 12: class="org.springframework.jdbc.datasource.DriverManagerDataSource"> 13: <property name="driverClassName"> 14: <value>${db.driver}</value> 15: </property> 16: <property name="url"> 17: <value>${db.url}</value> 18: </property> 19: <property name="username"> 20: <value>${db.username}</value> 21: </property> 22: <property name="password"> 23: <value>${db.password}</value> 24: </property> 25: </bean> 26: <bean id="sessionFactory" 27: class="org.springframework.orm.hibernate.LocalSessionFactoryBean"> 28: <property name="hibernateProperties"> 29: <props> 30: <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop> 31: </props> 32: </property> 33: <property name="mappingDirectoryLocations"> 34: <list> 35: <value>classpath:/diaop/model</value> 36: </list> 37: </property> 38: <property name="dataSource"> 39: <ref bean="dataSource"/> 40: </property> 41: </bean> 42: <bean id="hibernateTemplate" 43: class="org.springframework.orm.hibernate.HibernateTemplate"> 44: <property name="sessionFactory"> 45: <ref bean="sessionFactory"/> 46: </property> 47: </bean> 48: <!-- memberDao を定義 --> 49: <bean id="memberDao" 50: class="diaop.dao.HibernateMemberDao"> 51: <!-- hibernateTemplate を template にセット --> 52: <property name="template"> 53: <ref bean="hibernateTemplate"/> 54: </property> 55: </bean>
変更したプログラムを動作させてみます。最初にDBにテーブルを作成し、1件のデータをセットします。
CREATE TABLE T_MEMBER(ID INTEGER NOT NULL PRIMARY KEY,ADDRESS VARCHAR,NAME VARCHAR) INSERT INTO T_MEMBER (ADDRESS, ID, NAME)VALUES ('中野区' , 1, 'たまねぎ')
リスト[例題1のmainメソッド]で使用したコンフィグファイルをリスト[コンフィグファイルのHibernate対応への変更部分]に変更して実行します。
MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区 MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区 MEMBER: ID=2 NAME=華子 ADDRESS=杉並区
リスト[例題1のmainメソッド]と同じ結果が出力されます。
マッピングファイル(クラス名.hbm.xml)は、ファイルの数が多くなると手で作成するのは大変な作業になります。そこで、DbHelperというユーティリティクラスを作って
を出力するようにしました。
DBHelperの制限としては、
図[DBHelperの実行例]のようにdiaop.util.DbHelperの後に出力するクラス名を列記すると
java diaop.util.DbHelper diaop.model.Member
図[DbHelper main の出力]のように出力されるで、これを適宜コピーして使用して下さい。
<!-- CREATE TABLE FOR T_MEMBER --> CREATE TABLE T_MEMBER(ID INTEGER NOT NULL PRIMARY KEY,ADDRESS VARCHAR,NAME VARCHAR) <!-- Member.hbm.xml --> <?xml version="1.0" encoding="Windows-31J"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN" "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"> <hibernate-mapping> <class name="diaop.model.Member" table="T_MEMBER"> <id name="id"> <generator class="increment"/> </id> <property name="address"/> <property name="name"/> </class> </hibernate-mapping>
DbHelperのソースプログラム(をリスト[DbHelper]に示します。
package diaop.util; import java.beans.*; import java.lang.reflect.*; import java.sql.*; import java.text.SimpleDateFormat; import java.util.*; public class DbHelper { private String clsName; // クラスパス private String objLabel; // クラスパスの最後の名前 private List propList; public DbHelper(Class cls) { clsName = cls.getName(); try { StringTokenizer tk = new StringTokenizer(clsName, "."); while (tk.hasMoreTokens()) { objLabel = tk.nextToken(); } BeanInfo info = Introspector.getBeanInfo(cls); PropertyDescriptor[] objProps = info.getPropertyDescriptors(); /** * getter, setterが揃っているものだけをDB登録用候補とする */ propList = new ArrayList(); for (int i = 0; i < objProps.length; i++) { Method getMethod = objProps[i].getReadMethod(); Method setMethod = objProps[i].getWriteMethod(); if (getMethod != null && setMethod != null) { propList.add(objProps[i]); } } } catch (Exception e) { } } public DbHelper(Object obj) { this(obj.getClass()); } public String toString(Object obj) { if (!isInstanceOfMyClass(obj)) return (null); StringBuffer buf = new StringBuffer(); buf.append(objLabel.toUpperCase() + ":"); for (int i = 0; i < propList.size(); i++) { PropertyDescriptor objProp = (PropertyDescriptor)propList.get(i); String propName = objProp.getName().toUpperCase(); Class propType = objProp.getPropertyType(); Method getMethod = objProp.getReadMethod(); if (propName.equals("CLASS")) // オブジェクト自身を除く continue; Object propValue = null; try { propValue = getMethod.invoke(obj, null); } catch (Exception e) { propValue = null; } if (propValue != null) { if (Integer.class.equals(propType) || Double.class.equals(propType) || Boolean.class.equals(propType)) { buf.append(" "); buf.append(propName); buf.append("="); buf.append(propValue.toString()); } else if (Date.class.equals(propType)) { SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd"); buf.append(" "); buf.append(propName); buf.append("='"); buf.append(fmt.format(propValue)); buf.append("'"); } else if (String.class.equals(propType) || Timestamp.class.equals(propType)) { buf.append(" "); buf.append(propName); buf.append("='"); buf.append(propValue.toString()); buf.append("'"); } } } return (buf.toString()); } public String toDbUnitFormat(Object obj) { if (!isInstanceOfMyClass(obj)) return (null); StringBuffer buf = new StringBuffer(); buf.append("\t<" + getTableName()); for (int i = 0; i < propList.size(); i++) { PropertyDescriptor objProp = (PropertyDescriptor)propList.get(i); String propName = objProp.getName(); Class propType = objProp.getPropertyType(); Method getMethod = objProp.getReadMethod(); if (propName.equals("CLASS")) // オブジェクト自身を除く continue; Object propValue = null; try { propValue = getMethod.invoke(obj, null); } catch (Exception e) { propValue = null; } if (propValue != null) { if (Integer.class.equals(propType) || Double.class.equals(propType) || Boolean.class.equals(propType) || String.class.equals(propType) || Date.class.equals(propType) || Timestamp.class.equals(propType)) { buf.append("\n\t\t"); buf.append(propName); buf.append("='"); buf.append(propValue.toString()); buf.append("'"); } } } buf.append("/>\n"); return (buf.toString()); } public String getTableName() { return ("T_" + objLabel.toUpperCase()); } public String getCreateTableStmt() { StringBuffer buf = new StringBuffer(); buf.append("CREATE TABLE " + getTableName()); buf.append( "(ID INTEGER NOT NULL PRIMARY KEY"); for (int i = 0; i < propList.size(); i++) { PropertyDescriptor objProp = (PropertyDescriptor)propList.get(i); String propName = objProp.getName().toUpperCase(); Class propType = objProp.getPropertyType(); Method getMethod = objProp.getReadMethod(); Method setMethod = objProp.getWriteMethod(); if (propName.equals("ID")) // IDは固定とする continue; if (propName.equals("CLASS")) // オブジェクト自身を除く continue; // getter, setterが揃っているものだけ使用する if (setMethod == null || getMethod == null) continue; // volatile宣言された変数はDBには、含めない if (Modifier.isVolatile(propType.getModifiers())) continue; if (Integer.class.equals(propType)) { buf.append("," + propName + " INTEGER"); } else if (Double.class.equals(propType)) { buf.append("," + propName + " DOUBLE"); } else if (String.class.equals(propType)) { buf.append("," + propName + " VARCHAR"); } else if (Boolean.class.equals(propType)) { buf.append("," + propName + " BIT"); } else if (Date.class.equals(propType)) { buf.append("," + propName + " DATE"); } else if (Timestamp.class.equals(propType)) { buf.append("," + propName + " TIMESTAMP"); } } buf.append(")"); return (buf.toString()); } public String getHibernateXml() { StringBuffer buf = new StringBuffer(); buf.append("<?xml version=\"1.0\" encoding=\"Windows-31J\"?>\n"); buf.append("<!DOCTYPE hibernate-mapping"); buf.append("\tPUBLIC \"-//Hibernate/Hibernate Mapping DTD//EN\"\n"); buf.append("\t\"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd\">\n"); buf.append("<hibernate-mapping>\n"); buf.append("\t<class \n"); buf.append("\t\tname=\"" + getClsName() + "\"\n"); buf.append("\t\ttable=\"" + getTableName() + "\">\n"); buf.append("\t\t<id name=\"id\">\n"); buf.append("\t\t\t<generator class=\"increment\"/>\n"); buf.append("\t\t</id>\n"); // カラムをセット for (int i = 0; i < propList.size(); i++) { PropertyDescriptor objProp = (PropertyDescriptor)propList.get(i); String propName = objProp.getName(); Class propType = objProp.getPropertyType(); Method getMethod = objProp.getReadMethod(); Method setMethod = objProp.getWriteMethod(); if (propName.toUpperCase().equals("ID")) // IDは固定とする continue; if (propName.toUpperCase().equals("CLASS")) // オブジェクト自身を除く continue; // getter, setterが揃っているものだけ使用する if (setMethod == null || getMethod == null) continue; // volatile宣言された変数はDBには、含めない if (Modifier.isVolatile(propType.getModifiers())) continue; if (Integer.class.equals(propType) || Double.class.equals(propType) || String.class.equals(propType) || Boolean.class.equals(propType) || Date.class.equals(propType) || Timestamp.class.equals(propType)) { buf.append("\t\t<property name=\"" + propName + "\"/>\n"); } } buf.append("\t</class>\n"); buf.append("</hibernate-mapping>\n"); return (buf.toString()); } public List getPropList() { return propList; } public String getObjLabel() { return objLabel; } public String getClsName() { return clsName; } boolean isInstanceOfMyClass(Object obj) { // AOP用にinstanceofのチェックを動的に行うように修正 try { Object clsObj = Class.forName(clsName); if (obj.getClass().isInstance(clsObj)) return (false); } catch (Exception e) { return (false); } return (true); } public static void main(String[] args) { for (int i = 0; i < args.length; i++) { try { DbHelper helper = new DbHelper(Class.forName(args[0])); System.out.println("<!-- CREATE TABLE FOR " + helper.getTableName() + " -->"); System.out.println(helper.getCreateTableStmt()); System.out.println("<!-- " + helper.getObjLabel() + ".hbm.xml -->"); System.out.println(helper.getHibernateXml()); } catch (Exception e) { System.err.println("Class not found"); } } } }
プロキシーデザインパターンの適応モデルによく挙げられているのが、リモート処理です。
RMIは、Sun のRPC(Remote Procedure Call)のJava版として開発され、プラット フォームに依存しないJavaの性質を利用し、メソッドを含むオブジェクトそのものを リモートのマシンに送信し、処理をさせるように改良されています。
通常のRMIの開発とSpring を使った開発を比較しながら、プロキシーパターンの 有効性について見てみましょう。
指定された処理(ITask)を行う計算機(Computor)をRMIを使って実装します。
クラス構成は、図[RMIサンプルのクラス構成]のようになります。
計算機がリモートで実装するインタフェースICompute(リスト[ICompute])はタスクITask(リスト[ITask])を実行(executeTask)するというきわめて簡単なものです。ただし、IComputeがRemoteを継承している点に注意してください。
1: package diaop.rmi.compute; 2: import java.rmi.Remote; 3: import java.rmi.RemoteException; 4: 5: public interface ICompute extends Remote { 6: Object executeTask(ITask k) throws RemoteException; 7: }
RMIでリモートに渡す引数は、インタフェースSerializableを実装するしなければならないので、ITaskはSerializableを継承します。
1: package diaop.rmi.compute; 2: import java.io.Serializable; 3: 4: public interface ITask extends Serializable { 5: Object execute(); 6: }
サーバ側で実際に計算を行うComputorの実装は、リスト[Computor]のようになります。
1: package diaop.rmi.server; 2: import java.rmi.*; 3: import java.rmi.server.UnicastRemoteObject; 4: import diaop.rmi.compute.ICompute; 5: import diaop.rmi.compute.ITask; 6: 7: public class Computor extends UnicastRemoteObject 8: implements ICompute { 9: public Object executeTask(ITask t) throws RemoteException { 10: return t.execute(); 11: } 12: public static void main(String[] args) { 13: if (System.getSecurityManager() == null) { 14: System.setSecurityManager(new RMISecurityManager()); 15: } 16: String name = "//localhost/Computor"; 17: try { 18: ICompute engine = new Computor(); 19: Naming.rebind(name, engine); 20: System.out.println("Computor bound"); 21: } catch (Exception e) { 22: System.err.println("Computor exception: " + 23: e.getMessage()); 24: } 25: } 26: }
Computorは、UnicastRemoteObjectのサブクラスと実装しなくてはなりません。
mainメソッドでは、
クライアント側では、2項演算子プラスを処理するタスクBinOpePlus(リスト[BinOpePlus])を定義します。
1: package diaop.rmi.client; 2: import diaop.rmi.compute.ITask; 3: 4: public class BinOpePlus implements ITask { 5: int left; 6: int right; 7: public BinOpePlus(int left, int right) { 8: this.left = left; 9: this.right = right; 10: } 11: 12: public Object execute() { 13: return (new Integer(left + right)); 14: } 15: }
クライアントのmainメソッドはリスト[ComputeBinOpe]のようになります。
1: package diaop.rmi.client; 2: import java.rmi.Naming; 3: import java.rmi.RMISecurityManager; 4: import diaop.rmi.compute.ICompute; 5: 6: public class ComputeBinOpe { 7: public static void main(String[] args) { 8: if (System.getSecurityManager() == null) { 9: System.setSecurityManager(new RMISecurityManager()); 10: } 11: try { 12: String name = "//" + args[0] + "/Computor"; 13: ICompute comp = (ICompute) Naming.lookup(name); 14: BinOpePlus task = new BinOpePlus(Integer.parseInt(args[1]), 15: Integer.parseInt(args[2])); 16: Integer result = (Integer) (comp.executeTask(task)); 17: System.out.println(result); 18: } catch (Exception e) { 19: System.err.println("ComputeBinOpe exception: " + 20: e.getMessage()); 21: e.printStackTrace(); 22: } 23: } 24: }
次にコンパイルとビルドですが、
% rmic -d . Computor
grant { permission java.net.SocketPermission "*:1024-65535", "connect,accept"; permission java.net.SocketPermission "*:80", "connect"; };
とします。
サーバ起動の前にrmiregistryを起動します(図[rmiregistryの起動]参照)。
% start rmiregistry
サーバの起動には、codebase, policyを指定します(図[RMIサーバの起動]参照)。
% java -Djava.rmi.server.codebase=file:/インタフェースのjarファイルのパス -Djava.security.policy=client.policy Computor
クライアントも同様に起動します(図[例題4(RMIクライアント)の起動]参照)。
% java -Djava.rmi.server.codebase=file:/インタフェースのjarファイルのパス -Djava.security.policy=client.policy ComputeBinOpe 2 3 5
と、2と3の2項演算プラスの実行結果5が出力されます。
面倒なことが苦手な私は、Eclipse用のRMI PlugInを使ってビルドと実行を自動的に行っています。http://www.genady.net/rmi/を参照してください。
DIを使ったRMIクライアントは、rmirestryからlookupメソッドでIComputeのインスタンを取得した部分をDIとプロキシーを使って記述します。
1: <bean id="computeService" 2: class="org.springframework.remoting.rmi.RmiProxyFactoryBean"> 3: <property name="serviceUrl"> 4: <value>//localhost/Computor</value> 5: </property> 6: <property name="serviceInterface"> 7: <value>compute.ICompute</value> 8: </property> 9: </bean>
このように定義したRMIプロキシーは、クライアントプログラムにとってはIComputeを実装したBeanとしか認識されないため、単体テストではモックオブジェクトに置き換えることが可能となります。
DIを使ったRMIプロキシーのmainメソッドをリスト[DIを使ったRMIプロキシーのmainメソッド] に示します。
1: package diaop.app; 2: import org.springframework.beans.factory.BeanFactory; 3: import org.springframework.context.support.ClassPathXmlApplicationContext; 4: import client.BinOpePlus; 5: import compute.ICompute; 6: 7: public class RmiClient { 8: public static void main(String[] args) { 9: // コンフィグファイルからコンテナを定義 10: BeanFactory factory = 11: new ClassPathXmlApplicationContext("/config-3.xml"); 12: // コンテナからcomputeServiceを取得 13: ICompute comp = (ICompute) factory.getBean("computeService"); 14: BinOpePlus task = new BinOpePlus(Integer.parseInt(args[0]), 15: Integer.parseInt(args[1])); 16: try { 17: Integer result = (Integer) (comp.executeTask(task)); 18: System.out.println(result); 19: } 20: catch (Exception e){ 21: System.err.println(e.getCause()); 22: } 23: } 24: }10-13行目のコンテナーから computeService を取得する部分がDIを使って変更した ところです。
Springの提供するRMIエクスポータは、rmicを使かわないでサーバ側のサービスをクライアントに提供する機能です。
Spring RMIエクスポータを使った場合のクラス図を図[Spring RMIエクスポータのクラス構成] に示します。 もはや Remote, UnicastRemoteObject に依存していない点に注意してください。
先の例でIComputeと区別するために、IComputeService インタフェース(リスト[IComputeService]) のように定義します。
1: package diaop.rmi.spring; 2: import diaop.rmi.compute.ITask; 3: 4: public interface IComputeService { 5: Object executeTask(ITask k) throws BinOpeException; 6: }サーバ側でのサービスの実装は、リスト[ComputeServiceImpl] のようになります。
1: package diaop.rmi.spring; 2: import diaop.rmi.compute.ITask; 3: 4: public class ComputeServiceImpl 5: implements IComputeService { 6: public Object executeTask(ITask t) throws BinOpeException { 7: return t.execute(); 8: } 9: }
Spring RMIエクスポータのコンフィグファイルは、リスト[Spring RMIエクスポータのコンフィグファイル] と定義します。
1: <beans> 2: <bean id="computerService" 3: class="diaop.rim.ComputeServiceImpl"/> 4: <bean 5: class="org.springframework.remoting.rmi.RmiServiceExporter"> 6: <property name="service"> 7: <ref bean="computerService"/> 8: </property> 9: <property name="serviceName"> 10: <value>Computor</value> 11: </property> 12: <property name="serviceInterface"> 13: <value>diaop.rim.IComputeService</value> 14: </property> 15: </bean> 16: </beans>
Spring RMIエクスポータのmainメソッドは極めて簡単です(リスト[SpringRmiServer])。
1: public static void main(String[] args) { 2: // コンフィグファイルからコンテナを定義 3: BeanFactory factory = 4: new ClassPathXmlApplicationContext("/config-5.xml"); 5: } 6:Spring RMIエクスポータの起動時には、spring-remoting.jar とポリシーファイル 指定が必要です。
% java -Djava.security.policy=client.policy -Djava.rmi.server.codebase=file:spring-remoting.jarのパス
AOPはとても便利で使いやすい反面、多用すると実際のソフトウェアがどのように構成され、どのように振る舞うかを把握しづらくなるという危険性を併せ持っています。
AOPを使用する場合の基本的なスタンスとはどのような ものかを考えてみましょう。
SpringのAOPは、プロキシーを使ってメソッドへの呼び出しを横取りするInterceptorとコンテナーのどの部分にInterceptorを適応するかを指定するAdvisorから構成されています。
AOPを適応するメソッドがインタフェースで定義されている場合 にはProxyを使いますが、クラスの場合にはCGLibを使ってサブクラスを 生成しプロキシーとします。
AOP では「AOPで埋め込まれる処理」を advice と呼んでいます。 Spring でサポートされている adivceの種類を表[adivceの種類] に示します。
adivceの種類 | インタフェース | 適応箇所 |
---|---|---|
Around | org.aopalliance.intercept.MethodInterceptor | メソッド呼び出しを横取りする |
Before | org.springframework.aop.BeforeAdvice | メソッドが呼び出される前 |
After | org.springframework.aop.AfterReturningAdvice | リターンの前 |
Throws | org.springframework.aop.ThrowsAdvice | 例外が発生した時 |
各メソッドの入口と出口にデバッグ用のログを入れたソースを見たことはないでしょうか。同じようなことを、「なぜ」毎回記述しなければならないのか疑問に感じたことはありませんか。
これに答えてくれるのが、AOP を使ったログ出力です。
各メソッドの呼び出し前とリターン直前にログを出力する EnterMethodLogAdvice と LeaveMethodLogAdvice を作成して みましょう。
Spring AOP のメソッド呼び出し前の Advice(Before Advice)は、 インタフェース MethodBeforeAdvice を実装しなくてはなりません。
EnterMethodLogAdviceでは、メソッド名と引数を出力します。(4)
public interface MethodBeforeAdvice{ public void before(Method method, Object[] args, Object target) throws Throwable ; } public class EnterMethodLogAdvice implements MethodBeforeAdvice { public void before(Method method, Object[] args, Object target) throws Throwable { System.out.print("enter " + method.getName() + " args=("); if (args != null) { for (int i = 0; i < args.length; i++) { if (i != 0) System.out.print(", "); System.out.print(args[i]); } System.out.println(")"); } } }
同様に、メソッドリターン直前の Advice(After Advice)は、 インタフェース AfterReturningAdvice を実装しなくてはなりません。
LeaveMethodLogAdviceのソースを以下に示します。EnterMethodLogAdviceと同様にメソッド名とリターン値を出力します。
public interface AfterReturningAdvice{ public void afterReturning( Object returnValue, Method method, Object[] args, Object target) throws Throwable ; } public class LeaveMethodLogAdvice implements AfterReturningAdvice { public void afterReturning( Object returnValue, Method method, Object[] args, Object target) throws Throwable { System.out.println("leave " + method.getName() + " return=" + (returnValue != null ? returnValue : "null")); } }
Springの基本的なAOPの定義は、
を行いますが、今回のようにサービス内のすべてのメソッドに同一のAdviceを適応する場合にはこれでは不便です。代わりにSpringの提供するBeanNameAutoProxyCreatorというAdvisor (AOPを適応する範囲とAdviceを組み合わせたもの)を使用します。BeanNameAutoProxyCreatorには、
を定義します。
ログ出力の Spring コンフィグファイルは、 リスト[ログ出力の Spring コンフィグファイル] のようになります。
<!-- メソッドログアドバイス --> <bean id="enterMethodLogAdvice" class="diaop.advice.EnterMethodLogAdvice"/> <bean id="leaveMethodLogAdvice" class="diaop.advice.LeaveMethodLogAdvice"/> <!-- メソッドログ・プロキシー・クリエータ --> <bean id="methodLogProxyCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="beanNames"> <list> <value>*Dao</value> </list> </property> <property name="interceptorNames"> <list> <value>enterMethodLogAdvice</value> <value>leaveMethodLogAdvice</value> </list> </property> </bean>
ログ出力AOPを組み入れたコンフィグファイルを実行してみると、図[例題7(ログ出力 AOP)の実行結果]のように出力されます。期待したとおりmemberDaoのメソッド呼び出し前、リターン直前にログが出力されます。
enter findMember args=(1) leave findMember return=diaop.model.Member@187814 enter printMember args=(diaop.model.Member@187814) leave printMember return=MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区 MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区 enter insertMember args=(diaop.model.Member@1f8c6df) leave insertMember return=null enter findAllMembers args=(leave findAllMembers return=[Ldiaop.model.Member;@123b25c enter printMember args=(diaop.model.Member@187814) leave printMember return=MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区 MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区 enter printMember args=(diaop.model.Member@1f8c6df) leave printMember return=MEMBER: ID=2 NAME=華子 ADDRESS=杉並区 MEMBER: ID=2 NAME=華子 ADDRESS=杉並区
各メソッド毎にチェックプリントを入れていたのと比べると 非常に簡単で、プログラムも読みやすくなります。
SpringのAOPはコンフィグからコンテナーを生成するときに組み込まれてるため、プログラム内で生成されたインスタンスにはAOPが適応されません。
そのため Spring では、
特定のインスタンを生成するファクトリを定義するには、BeanFactoryAwareインタフェースを実装する必要があります。例として、MemberクラスのインスタンスのファクトリMemberFactory (リスト[MemberFactory])を使って実装の方法を示します。
public interface BeanFactoryAware { void setBeanFactory(BeanFactory beanFactory) throws BeansException; } public class MemberFactory implements BeanFactoryAware { private BeanFactory factory; public Member getInstance() { return ((Member)factory.getBean("member")); } public void setBeanFactory(BeanFactory factory) throws BeansException { this.factory = factory; } }
BeanFactoryAwareのsetBeanFactoryメソッドでコンフィグファイルのBeanFactoryが属性factoryに自動的にセットされます。プログラム内でMemberのインスタンスを生成したいクラスは、DIを使ってMemberFactoryを自分の属性にセットし、getInstanceメソッドを使って新しいインスタンスを生成します。
コンフィグファイルへの変更は、リスト[ログ出力の Spring コンフィグファイル] の member の singleton を false とし、memberFactory を 追加します。節[APIベースのAOP]と合わせるためにコンフィグファイルを リスト[MemberFactoryのコンフィグ] のように定義します。
1: <?xml version="1.0" encoding="Windows-31J"?> 2: <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 3: "http://www.springframework.org/dtd/spring-beans.dtd"> 4: <beans> 5: <!-- メンバを定義 --> 6: <bean id="member" class="diaop.model.Member" 7: singleton="false"/> 8: <!-- MemberFactory を定義 --> 9: <bean id="memberFactory" class="diaop.advice.MemberFactory"/> 10: <!-- メソッドログアドバイス --> 11: <bean id="enterMethodLogAdvice" 12: class="diaop.advice.EnterMethodLogAdvice"/> 13: <bean id="leaveMethodLogAdvice" 14: class="diaop.advice.LeaveMethodLogAdvice"/> 15: <!-- アドバイザーを定義 --> 16: <bean id="enterMethodLogAdvisor" 17: class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> 18: <property name="pattern"> 19: <value>.*set.*</value> 20: </property> 21: <property name="advice"> 22: <ref bean="enterMethodLogAdvice"/> 23: </property> 24: </bean> 25: <bean id="leaveMethodLogAdvisor" 26: class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> 27: <property name="pattern"> 28: <value>.*set.*</value> 29: </property> 30: <property name="advice"> 31: <ref bean="leaveMethodLogAdvice"/> 32: </property> 33: </bean> 34: <!-- プロキシー・クリエータ --> 35: <bean id="proxyCreator" 36: class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> 37: <property name="beanNames"> 38: <list> 39: <value>member</value> 40: </list> 41: </property> 42: <property name="interceptorNames"> 43: <list> 44: <value>enterMethodLogAdvisor</value> 45: <value>leaveMethodLogAdvisor</value> 46: </list> 47: </property> 48: </bean> 49: </beans>
サンプルプログラムは MemberFactory から取得したインスタンスに 値をセットし、DbHelper を使って出力するものです。
1: package diaop.app; 2: import org.springframework.beans.factory.BeanFactory; 3: import org.springframework.context.support.ClassPathXmlApplicationContext; 4: import diaop.advice.MemberFactory; 5: import diaop.model.Member; 6: import diaop.util.DbHelper; 7: 8: public class Ex_2 { 9: public static void main(String[] args) { 10: // コンフィグファイルからコンテナを定義 11: BeanFactory factory = 12: new ClassPathXmlApplicationContext("/config-8.xml"); 13: // コンテナからmemberFactoryを取得 14: MemberFactory memberFactory = (MemberFactory)factory.getBean("memberFactory"); 15: 16: Member shotaro = memberFactory.getInstance(); 17: shotaro.setId(new Integer(2)); 18: shotaro.setName("正太郎"); 19: shotaro.setAddress("杉並区"); 20: DbHelper helper = new DbHelper(Member.class); 21: System.out.println(helper.toString(shotaro)); 22: } 23: } 24:
サンプルプログラムの実行結果は、図[例題8(MemberFactory)の実行結果]のようになります。
memberFactoryから生成されたメンバー shotaro へのメソッド呼び出し、 setId, setName, setAddress で EnterMethodLogAdvice, LeaveMethodLogAdvice が 実行されています。
enter setId args=(2) leave setId return=null enter setName args=(正太郎) leave setName return=null enter setAddress args=(杉並区) leave setAddress return=null MEMBER: ADDRESS='杉並区' ID=2 NAME='正太郎'
APIベースのAOPが提供されているということは、Springのコンテナーを使用しないアプリケーションでAOPを適応できることを意味します。試しに、リスト[MemberFactory]をAPIを使って書いてみます。
1: public static void main(String[] args) { 2: // プロキシーファクトリを生成する 3: ProxyFactory factory = new ProxyFactory(new Member()); 4: factory.setProxyTargetClass(true); 5: factory.addAdvisor(new RegexpMethodPointcutAdvisor(".*set.*", new EnterMethodLogAdvice())); 6: factory.addAdvisor(new RegexpMethodPointcutAdvisor(".*set.*", new LeaveMethodLogAdvice())); 7: // ファクトリからMemberインスタンスを取り出す 8: Member shotaro = (Member)factory.getProxy(); 9: shotaro.setId(new Integer(2)); 10: shotaro.setName("正太郎"); 11: shotaro.setAddress("杉並区"); 12: DbHelper helper = new DbHelper(Member.class); 13: System.out.println(helper.toString(shotaro)); 14: } 15:
出力結果は、図[例題8(MemberFactory)の実行結果]と同じです。
POJO(Plain Old Java Object)をベースしたSpringのDIを使うとそれぞれの結合が疎になるため、
等のテストを一貫した環境で実施できます。
DB関連のテストをする場合、毎回テスト仕様に合ったデータをDBにセットする必要があります。
Phillippe Girolami 氏によって開発された DbUnitは、
FaltXmlDataSetの扱うXMLファイルは、以下の形式でデータベースのテーブルの値を保持します。
<dataset> <テーブル名 カラム名="カラムの値" ... カラム数分繰り返す /> ... レコード分繰り返す </dataset>
MemberDaoのMemberを例にすると
<?xml version="1.0" encoding="Windows-31J"?> <dataset> <T_MEMBER address='中野区' id='1' name='たまねぎ'/> </dataset>
となります。
Springで提供されているDB用の単体テストケースAbstractTransactionalSpringContextTestsを使用すると同じTestCaseからのサブクラスのDbUnitを使うことができません。
そこで、DbUnit の XML ファイルを Dao でも扱える ように DbUnitHelper を作成しました。DbUnitHelper は、
1: package diaop.util; 2: import java.io.*; 3: import java.util.*; 4: import org.apache.commons.digester.Digester; 5: 6: public class DbUnitHelper { 7: private ILoadAndSave dao; 8: private Digester digester; 9: 10: public DbUnitHelper(ILoadAndSave dao) { 11: this.dao = dao; 12: setupDigester(); 13: } 14: public void dump(Writer writer) { 15: try { 16: Iterator itr = dao.getHelperMap().values().iterator(); 17: writer.write("<?xml version=\"1.0\" encoding=\"Windows-31J\"?>\n"); 18: writer.write("<dataset>\n") ; 19: // 各テーブルに対して 20: while (itr.hasNext()) { 21: DbHelper helper = (DbHelper) itr.next(); 22: List list = dao.loadAll(Class.forName(helper.getClsName())); 23: if (list != null) { 24: for (int i = 0; i < list.size(); i++) { 25: writer.write(helper.toDbUnitFormat(list.get(i))); 26: } 27: } 28: } 29: writer.write("</dataset>\n"); 30: writer.close(); 31: } 32: catch (Exception e){} 33: } 34: public void dump(String path) { 35: try { 36: OutputStreamWriter writer = 37: new OutputStreamWriter(new FileOutputStream(path)); 38: dump(writer); 39: } 40: catch (Exception e) { 41: } 42: } 43: public void restore(String path) { 44: try { 45: BufferedInputStream stream = 46: new BufferedInputStream(new FileInputStream(path)); 47: Iterator itr = dao.getHelperMap().values().iterator(); 48: 49: // すべてのテーブルを空にする 50: while (itr.hasNext()) { 51: DbHelper helper = (DbHelper) itr.next(); 52: dao.delete(Class.forName(helper.getClsName()), ""); 53: } 54: 55: List dataset = (List)digester.parse(stream); 56: if (dataset != null) { 57: for (int i = 0; i < dataset.size(); i++) { 58: dao.save(dataset.get(i)); 59: } 60: } 61: } 62: catch (Exception e) {} 63: } 64: private Digester setupDigester() { 65: digester = new Digester(); 66: // <dataset>タグの処理 67: digester.addObjectCreate("dataset", "java.util.ArrayList"); 68: digester.addSetProperties("dataset"); 69: 70: Iterator itr = dao.getHelperMap().values().iterator(); 71: while (itr.hasNext()) { 72: DbHelper helper = (DbHelper) itr.next(); 73: digester.addObjectCreate("dataset/" + helper.getTableName(), 74: helper.getClsName(), "type"); 75: digester.addSetNext("dataset/" + helper.getTableName(), "add", 76: helper.getClsName()); 77: digester.addSetProperties("dataset/" + helper.getTableName()); 78: } 79: return (digester); 80: } 81: }
DbUnitHelperを使ってHibernateMemberDaoにdump/restore機能を追加したDbUnitMemberDaoを作成します(リスト[DbUnitMemberDao]参照)。dump/restore機能は、インタフェースIDbUnitAwareのdump/restoreを実装します。
1: package diaop.dao; 2: 3: import java.io.Serializable; 4: import java.util.*; 5: 6: import diaop.model.*; 7: import diaop.util.*; 8: 9: public class DbUnitMemberDao extends HibernateMemberDao 10: implements ILoadAndSave, IDbUnitAware { 11: private DbHelper[] dbHelpers = { 12: new DbHelper(Member.class) 13: }; 14: private Map helperMap; 15: 16: public DbUnitMemberDao() { 17: super(); 18: helperMap = new HashMap(); 19: for (int i = 0; i < dbHelpers.length; i++) { 20: helperMap.put(dbHelpers[i].getClsName(), dbHelpers[i]); 21: } 22: } 23: public void delete(Class cls, String where) { 24: String hql = "from " + cls.getName() + " " + where; 25: template.delete(hql); 26: } 27: public Map getHelperMap() { 28: return helperMap; 29: } 30: public List loadAll(Class cls) { 31: return (template.loadAll(cls)); 32: } 33: public Serializable save(Object obj) { 34: return (template.save(obj)); 35: } 36: public void flush() { 37: template.flush(); 38: } 39: public void dump(String path) { 40: DbUnitHelper dbUnit = new DbUnitHelper(this); 41: dbUnit.dump(path); 42: } 43: public void restore(String path) { 44: DbUnitHelper dbUnit = new DbUnitHelper(this); 45: dbUnit.restore(path); 46: } 47: }
AbstractTransactionalSpringContextTestsは、Springのトランザクション処理を使ってテスト終了後にDBを自動的にテスト前の状態に戻してくれる便利なTestCaseクラスです。
AbstractTransactionalSpringContextTests でトランザクション処理を使用するには コンフィグファイルに PlatformTransactionManager を実装した bean を "transactionManager" という名前で定義しておく必要があります。
単体テスト用のコンフィグファイル変更点をリスト[単体テスト用のコンフィグファイル変更点]に示します。
1: <!-- TransactionManager --> 2: <bean id="transactionManager" 3: class="org.springframework.orm.hibernate.HibernateTransactionManager"> 4: <property name="sessionFactory"> 5: <ref bean="sessionFactory"/> 6: </property> 7: </bean> 8: <!-- memberDao を定義 --> 9: <bean id="memberDao" 10: class="diaop.dao.DbUnitMemberDao"> 11: <!-- hibernateTemplate を template にセット --> 12: <property name="template"> 13: <ref bean="hibernateTemplate"/> 14: </property> 15: </bean>
restoreを使うとtestXXXXXメソッド毎にDBの設定を行うことができます。
MemberをDBから検索するテストケースを restore を使って 実現すると以下の様になる。
1: package unittest; 2: import org.springframework.test.AbstractTransactionalSpringContextTests; 3: import diaop.dao.IMember; 4: import diaop.model.Member; 5: import diaop.util.IDbUnitAware; 6: 7: public class DbTestCase1 8: extends AbstractTransactionalSpringContextTests { 9: private IMember dao; 10: 11: public DbTestCase1() { 12: super(); 13: // HibernateMemberDao はprotected template を持つので true 14: this.setPopulateProtectedVariables(true); 15: } 16: public String[] getConfigLocations() { 17: return new String[] {"/config-9.xml"}; 18: } 19: public void onSetUpInTransaction() throws Exception { 20: super.onSetUpInTransaction(); 21: dao = (IMember)getContext(getConfigLocations()[0]).getBean("memberDao"); 22: } 23: public void onTearDownInTransaction() { 24: super.onTearDownInTransaction(); 25: } 26: public void testFindMemberByID() { 27: ((IDbUnitAware)dao).restore("bin/dump.xml"); 28: 29: Member member = dao.findMember(new Integer(1)); 30: assertEquals("Hiroshi TAKEMOTO", member.getName()); 31: assertEquals("Nakano-ku", member.getAddress()); 32: } 33: }
AbstractTransactionalSpringContextTestsを使用する場合、getConfigLocations, onSetUpInTransactionメソッドをTestCaseで定義する必要があります。
例では、
restoreでDBにセットしているdump.xmlの内容をリスト[dump.xml]に示します。
1: <?xml version="1.0" encoding="Windows-31J"?> 2: <dataset> 3: <T_MEMBER 4: address='Nakano-ku' 5: id='1' 6: name='Hiroshi TAKEMOTO'/> 7: </dataset>
リスト[restore を使ったMember検索テスト] を実行した後は、図[単体テスト後のT_MEMBERの内容] のようにT_MEMBERの内容は元に戻っています。
AbstractTransactionalSpringContextTests を使うために以下のjarファイルをクラス パスに追加してください。
resotre, dumpを使うために以下のjarファイルをクラスパスに追加してください。
antでjunitを使用する場合には、junit.jarを$ANT_HOME/libにコピーしてください。
HSQLDBのmanagerでT_MEMBERの内容をSELECTした時の内容です。ソースのbuild.xmlを使った場合には、
% ant manager
で起動しますので、
でDBの内容が確認できます。
DIによってModelの設計に集中し、テスト駆動の開発環境によって 安心してリファクタリングできることがよいソフトウェア開発 への一歩につながると思います。
DIをどのように活用すればよいかを教えてくれる一番良い教材が Springコンポーネントです。 今回の例題を元に、すこしずつ使いながらDIの可能性を体感して いただけると幸いです。
Hibernateのファイルは、http://www.hibernate.org/からダウンロードできます。ここでは広く使われている2.1版のhibernate-2.1.8.zipをダウンロードしました。ZIPファイルには、日本語マニュアル、hibernateを使用するときに必要なjarファイルが含まれています。Hibernateを使うために以下のjarファイルをクラスパスに追加してください。
その他にjdbcドライバーが必要です。今回はHSQLDBを使用しますので、hsqldb.jarを追加します。
記事で使用したソースは、ここからダウロードしてください。
解答したファイルは、Eclipse のプロジェクトファイルですので、 インポートするだけです。
build.xml も付いていますので Eclipse を使っていなくてもサンプル プログラムを実行する事ができます。 図[例題1のビルド]のようにすると例題1が実行できます。 (8)
% ant Ex_1例題の番号との対応を表[例題番号とbuildターゲット名]に示します。
番号 | 内容 | ターゲット名 | コンフィグファイル | 別コンソールで起動 |
---|---|---|---|---|
例題1 | MemoryMemberDao | Ex_1 | config-1.xml | × |
例題2 | BeforeLogProxy | Ex_2 | config-2.xml | × |
例題3 | HibernateMemberDao | Ex_3 | config-6.xml | × |
例題4 | 通常のRMI | Ex_4_ClientとEx_4_Server | 不要 | ○ |
例題5 | RMIプロキシー | Ex_5_Client | config-3.xml | ○ |
例題6 | Spring RMIエクスポータ | Ex_6_ClientとEx_6_Server | config-4.xmlとconfig-5.xml | ○ |
例題7 | ログ出力AOP | Ex_7 | config-7.xml | × |
例題8 | MemberFactory | Ex_8 | config-8.xml | × |
例題9 | APIを使ったAOP | Ex_9 | 不要 | × |
例題10 | DbUnitMemberDaoを使った単体テスト | Ex_10 | config-9.xml | × |
HSQLDBマネージャ | HSQLDBマネージャの起動 | manager | 不要 | ○ |
HSQLDBサーバ | HSQLDBサーバの起動 | hsqldb | 不要 | ○ |
rmiregistry | rmiregistryの起動 | rmiregistry | 不要 | ○ |
[1] | Graig Walls and Ryan Greidenbach. Spring in Action. Manning, |
[2] | 長谷川裕一、伊藤清人、岩永寿来、大野渉. Java・J2EE・オープンソース_Spring入門 : より良いWebアプリケーションの設計と実装. 技術評論社, 2005 |
[3] | 河村嘉之、首藤智大、竹内祐介、吉尾真祐. 実践 Spring Framework : J2EE 開発を変える DI コンテナのすべて. 日経BP社, 2005 |
[4] | Martin Fowler. Inversion of Control Containers and the Dependency Injection pattern. 2004 |
[5] | Rod Johnson, Juergen Hoeller, Alef Arendsen, Colin Sampaleanu Rob Harrop, Thomas Risberg, Darren Davison, Dmitriy Kopylenko Mark Pollack, Thierry Templier. Spring - Java/J2EE Application Framework. |
[6] | Brad J. Cox, Andrew J. Novobilski. Object-Oriented Programming. Addison Wesley Publishing Company, 1986 |
#db.url=jdbc:hsqldb:hsql://localhost
のコメント(#)を外し、db.url=jdbc:hsqldb:data/test
をコメントにしてご使用下さい。