Powered by SmartDoc

DIとAOPを使ってみよう

竹本 浩
take@pwv.co.jp
http://www.pwv.co.jp
Sprigを使って、DIを実際に動かしながら、これまでのプログラミングとの違いや活用方法を説明します。

目次

Last modified: Mon Sep 05 18:27:56 JST 2005

1 DI

1.1 DIの由来

DI(Dependecy Injection)は、日本語では「依存性の注入」と訳されていますが、 PicoContainer, SpringではIoC(Inverse of Control)日本語では「制御の逆転」 と呼ばれていました。

IoCとは、アプリケーションが部品であるコンポーネントを生成し組み立てる 代わりに、コンテナーがコンポーネントを生成し組み立てた後、アプリケーション に渡すので「コンポーネントを生成し組み立てる」という制御がアプリケーション からコンテナーに移行したためにこのように名付けられました。

しかし、「制御の逆転」と言われても意味が伝わりにくいため、 Matin Fowler氏(2004)[4]で、DI(Dependecy Injection)と呼ばれるようになりました。

1.2 DIとは何か

DIがどのようなものなのかを、通常のアプリケーションでの部品の組み立て方とDIを使った場合を例に図を使って説明します。

1.2.1 通常のアプリケーション

DIを使わない通常のアプリケーションで、部品A、部品Bを生成する場合を図[通常のアプリケーション]に示します。

図 1.2.1.1 通常のアプリケーション
  1. アプリケーションが部品Aを生成する
  2. 部品Aが部品Bを生成する
  3. アプリケーションがインタフェースAを使って部品Aにアクセスをする

アプリケーションAのソースコードには、明示的に部品Aのコンストラクタが記述され、 同様に部品Aのソースコードには部品Bのコンストラクタを明記しなくてなりません。

1.2.2 DIを使ったアプリケーション

DIを使ってコンフィグファイルから部品A,部品Bを生成する場合を図[DIを使ったアプリケーション]に示します。

図 1.2.2.1 DIを使ったアプリケーション
  1. アプリケーションがコンテナを生成する
  2. コンテナが部品B、部品Aを生成し、部品Bを部品Aにセットする
  3. アプリケーションは、コンテナから組み立てられた部品Aを取得する
  4. アプリケーションがインタフェースAを使って部品Aにアクセスする

アプリケーションAのソースコードにはコンテナーのコンストラクタとコンフィグファイル を指定しますが、もはや部品Aのコンストラクタは指定しません。 同様に部品Aも部品Bを生成する必要は無くなります。

1.2.3 DIのタイプ

Matin Fowler氏(2004)[4]ではDIのタイプとして、

が紹介されています。

コンストラクタ注入タイプとは、コンストラクタの引数に注入する属性を渡す方式です。 渡す属性が増えれば、それに対応したコンストラクタが必要になります。

Setter注入タイプとは、属性のSetterメソッドを使って注入する方式です。注入する 属性は、SetterメソッドをPublicで公開するだけです。

インタフェース注入タイプは、注入するためのインタフェースを定義し、そのインタフェース を通じて属性を注入する方式です。初期のDIフレームワークであるAvalonで採用されている 方式です。

Springでは、コンストラクタ注入タイプとSetter注入タイプをサポートしています。

1.3 DIの恩恵

DIを使うことによって享受することができる恩恵を以下に示します。

1.4 DIを使ってみる

Springを使って簡単なサンプルプログラム(例題1)でDIを試してみましょう。

1.4.1 例題1の構成

例題1では、複数のメンバーを管理し、登録、検索、プリントする機能を提供するMemoryMemberDAOを作成します。

図[DIを使ったアプリケーション]の部品Aに相当するのがDAOで、部品Bに相当するのが、 メンバーのMAPです。

1.4.2 Memberクラス

最初にメンバーのクラスMember.javaを作成します。

Memberクラスは、メンバーを一意に識別するidと名前(name)及び住所(address)を属性に持ち、それらのgetter/setterから構成されたクラスです(リスト[Member]を参照)。

リスト 1.4.2.1 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: }

1.4.3 メンバーDAOインタフェース

メンバーを管理するためにインタフェースとしてIMemberに登録(insertMember)、検索(findMember, findAllMember)、プリント(printMember)機能を定義します(リスト[IMember]を参照)。

リスト 1.4.3.1 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: }

1.4.4 MemoryMemberDAO

メモリ上でメンバを管理するDAOとして、MemoryMemberDaoをリスト[MemoryMemberDao]のように定義します。

6行目にメンバ情報を入れるMapとして、属性membersを定義していますが、ソースにはsetMembersでmembersを設定する部分がありません。このmembersがDIによって注入される属性です。

リスト 1.4.4.1 MemoryMemberDao
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: }

1.4.5 Spring定義ファイルの説明

SpringではXML形式のコンフィグファイルからDIを埋め込みます。コンフィグファイルの構造をリスト[コンフィグファイル-1.xml]を例に説明します(詳しくはSpringのリファレンスマニュアルを参照してください)。

リスト 1.4.5.1 コンフィグファイル-1.xml
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属性を省略することもできます。

リスト 1.4.5.2 entry内のbeanタグ定義
<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の型を示します。

表 1.4.5.1 Springがサポートしているコレクション
XMLの定義 Javaの型
listタグ java.util.Listと配列
setタグ java.util.Set
mapタグ java.util.Map
propsタグ java.util.Properties
listタグを使った例をリスト[listタグの例]に示します。 (残りは必要なときにリファレンスマニュアル 3.3.3.3. The collection elements を参照してください)
リスト 1.4.5.3 listタグの例
<list>
    <!-- オブジェクト定義 -->
    <bean class="diaop.model.Member"/>    
    <bean class="diaop.model.Order"/>    
    <bean class="diaop.model.OrderItem"/>    
    <bean class="diaop.model.Product"/>        
</list>
				
				

1.4.6 例題1と動作確認

例題1のmainをリスト[例題1のmainメソッド]に示します。

リスト 1.4.6.1 例題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の実行結果]に示します。

図 1.4.6.1 サンプルプログラム-1の実行結果
MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区
MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区
MEMBER: ID=2 NAME=華子 ADDRESS=杉並区
			

実行に必要なjarファイルは、

です。

1.5 シングルトンとプロトタイプ

Springコンテナで定義されたBeanは、デフォルトではシングルトン(コンテナ内で1個のインスタンスしか持たない)として生成されます。

コンテナからBeanを取り出す度に新しいインスタンスを生成する場合には、 リスト[プロトタイプの例] のようにsingleton="false"とします。コンテナからprotoMemberとして 取り出したMemberインスタンスには、addressとして「中野区」がセット されているので、必要な属性をセットしたプロトタイプデザインパターン と同じ役割をします。

リスト 1.5.1 プロトタイプの例
<bean id="protoMember" class="diaop.model.Member"
    singleton="false"/>
    <property name="address">
        <value>中野区</value>
    </property>
</bean>
								
			

また、シングルトンをデザインパターンに忠実に実装するとMemberクラスは、

リスト 1.5.2 シングルトン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のシングルトンの機能を使った方がクラスの実装も簡単になり、再利用も可能となります。

1.6 インタフェースを使った疎結合

1.6.1 部品の交換

同じインタフェースに対する異なった実装の例として、ユーザ認証を例に見てみましょう。Springに対応しているAcegi Security SystemではインタフェースAuthenticationManagerの種類は表[AuthenticationManagerの種類]のようなっています。

表 1.6.1.1 AuthenticationManagerの種類
認証方式 用途 規模
インメモリDao認証 開発とテスト
Dao認証(認証用に別途DBを持たない) 開発と運用
Dao認証(認証用に別途DBを使用) 運用
パスワードを暗号化したDao認証 運用
LDAPを使った認証 運用

このように多様な認証要求にコンフィグファイルの変更のみで対応できるところがDIのすばらしいところだと感じます。この他にもセキュリティレベルでログの出力形式を変えたいときにも便利です。

1.6.2 ソフトウェアIC

20年前にCox氏が[6]で提唱したソフトウェアICという考え方が、DIによって一気に普及する予感がします。

javaのクラスを入れ替えることは、同じ規格のICを消費電力、処理能力、価格 等を検討して使い分けるのとまったく同じ感覚です。DIはまさに 「コロンブスの卵」です。

1.7 他の機能との融合

1.7.1 融合の方式

Springの構成をリファレンスマニュアル[5]から引用します(図[Spring の構成])。

図 1.7.1.1 Spring の構成

Spring Coreがベースとなっており、各コンポーネントが独立しているため、必要なものだけを選択して使用することができます。各コンポーネントがどのような処理をするかを簡単に説明します。

Spring構成をその融合方式で見てみると

のタイプに分類できます。

1.7.2 コンテナーに対する共通処理

Springの拡張性と柔軟性はコンポーネントに対する共通の処理を自由に組み込むことができることに由来しています。共通処理に於いてプロキシーを生成することでプロキシーを使った融合、AOPを使った融合を実現しているのです。

メソッドが呼ばれる前にメソッド名を出力する BeforeLogProxy を 使って Spring がどのようにプロキシーを生成し、AOPを実現して いるのか覗いてみましょう。

コンテナー内のコンポーネントBeanが生成される直前と属性が セットされた直後に呼び出される共通メソッドが BeanPostProcessor インタフェースに定義されています。

リスト 1.7.2.1 BeanPostProcessorインタフェース定義
public interface BeanPostProcessor {
    public Object postProcessAfterInitialization(Object bean, String beanName);	
    public Object postProcessBeforeInitialization(Object bean, String beanName);					
}
				

また、BeforeLogProxyはプロキシーなので、InvocationHandlerインタフェース の invoke メソッドも実装する必要があります。 BeforeLogProxy のソースをリスト[BeforeLogProxy]に示します。

リスト 1.7.2.2 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 をセットしているだけです。

リスト 1.7.2.3 BeforeLogProxyのコンフィグ
	<!-- ログプロキシー -->
	<bean id="logProxy"
		class="diaop.proxy.BeforeLogProxy">
		<property name="targetName">
			<value>memberDao</value>
		</property>		
	</bean>									
				

実行結果を図[例題2(BeforeLogProxy)の実行結果] に示します。各メソッド呼び出しの前に「Enter: メソッド名」が出力 されているのが分かります。

図 1.7.2.1 例題2(BeforeLogProxy)の実行結果
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=杉並区
				

2 永続化とO/Rマッピング

Springでの永続化の実装は、JDBC, Hibernate等の既存機能をDIに組み入れる場合の良い例題です。

Spring では様々なO/Rマッピングを

によってトランザクション処理やビジネス層の例外処理が共通に使用できるようになって います。

Spring で定義されているデータアクセス例外を表[Spring DAO例外]に示します。

表 2.1 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 特定不可能な例外が発生した場合

2.1 Memberクラスの永続化

最初に節[MemoryMemberDAO]で作成したMember DAOをSpringの提供するHibernateTemplateを使ったDAOに移行してみます。

2.1.1 マッピングファイルの作成

HibernateではDBの値を保持するPOJO(Plain Old Java Object)と同じディレクトリに

POJOクラス名.hbm.xml

の名称のマッピングファイルを作成します。

永続化で使用するテーブルは、以下の方針で作成しました。

Memberのテーブルはリスト[MemberテーブルのCREATE TABLE文]のようになります。
リスト 2.1.1.1 MemberテーブルのCREATE TABLE文
CREATE TABLE T_MEMBER(ID INTEGER NOT NULL PRIMARY KEY,ADDRESS VARCHAR,NAME VARCHAR)					
				

Member クラスに対応した Member.hbm.xml は、以下のようになります。

リスト 2.1.1.2 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)

  1. テーブルのカラム名と属性名が異なる場合には、<property name="foo" column="bar"/>のようにcolumn属性を追加します。

2.1.2 DAOの作成

Hibernateを使ったメンバーDAOは、HibernateTemplateを使って非常に簡単に作成できます。HibernateMemberDaoのソースをに示します。

リスト 2.1.2.1 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: }

2.1.3 Springコンフィグファイルの変更

SpringコンフィグファイルをHibernateTemplateを使用できるように変更します。

最初にデータベースの定義を記述したプロパティファイル jdbc.properties を リスト[jdbc.properties]の様に定義します。 (2)

リスト 2.1.3.1 jdbc.properties
db.url=jdbc:hsqldb:hsql://localhost
db.driver=org.hsqldb.jdbcDriver
db.username=sa
db.password=

コンフィグファイルの変更はリスト[コンフィグファイルのHibernate対応への変更部分]の部分です。

リスト 2.1.3.2 コンフィグファイルのHibernate対応への変更部分
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>
  1. ここでは、HSQLDBをサーバタイプで使用する設定にしてあります。

2.1.4 例題3(HibernateMemberDao)の実行

変更したプログラムを動作させてみます。最初に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対応への変更部分]に変更して実行します。

図 2.1.4.1 例題3(HibernateMemberDao)の実行結果
MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区
MEMBER: ID=1 NAME=たまねぎ ADDRESS=中野区
MEMBER: ID=2 NAME=華子 ADDRESS=杉並区
			

リスト[例題1のmainメソッド]と同じ結果が出力されます。

2.2 DBヘルパー

マッピングファイル(クラス名.hbm.xml)は、ファイルの数が多くなると手で作成するのは大変な作業になります。そこで、DbHelperというユーティリティクラスを作って

を出力するようにしました。

DBHelperの制限としては、

が挙げられます。

図[DBHelperの実行例]のようにdiaop.util.DbHelperの後に出力するクラス名を列記すると

図 2.2.1 DBHelperの実行例
java diaop.util.DbHelper diaop.model.Member		
		

図[DbHelper main の出力]のように出力されるで、これを適宜コピーして使用して下さい。

図 2.2.2 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]に示します。

リスト 2.2.1 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");
            }
        }
    }
}

3 プロキシーを使った融合

3.1 RMIとの融合

プロキシーデザインパターンの適応モデルによく挙げられているのが、リモート処理です。

RMIは、Sun のRPC(Remote Procedure Call)のJava版として開発され、プラット フォームに依存しないJavaの性質を利用し、メソッドを含むオブジェクトそのものを リモートのマシンに送信し、処理をさせるように改良されています。

通常のRMIの開発とSpring を使った開発を比較しながら、プロキシーパターンの 有効性について見てみましょう。

3.1.1 通常のRMIの開発

指定された処理(ITask)を行う計算機(Computor)をRMIを使って実装します。

クラス構成は、図[RMIサンプルのクラス構成]のようになります。

図 3.1.1.1 RMIサンプルのクラス構成

計算機がリモートで実装するインタフェースICompute(リスト[ICompute])はタスクITask(リスト[ITask])を実行(executeTask)するというきわめて簡単なものです。ただし、IComputeがRemoteを継承している点に注意してください。

リスト 3.1.1.1 ICompute
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を継承します。

リスト 3.1.1.2 ITask
1: package diaop.rmi.compute;
2: import java.io.Serializable;
3: 
4: public interface ITask extends Serializable {
5:     Object execute();
6: }

サーバ側で実際に計算を行うComputorの実装は、リスト[Computor]のようになります。

リスト 3.1.1.3 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])を定義します。

リスト 3.1.1.4 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]のようになります。

リスト 3.1.1.5 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: }

次にコンパイルとビルドですが、

  1. ICompute, Itaskをコンパイルし、jarファイルを作成し、クラスパスに追加する
  2. rmicでComputorのスタブを生成する(図[rmicの起動]参照)
    図 3.1.1.2 rmicの起動
    % rmic -d . Computor						
    					
    
  3. クライアントをコンパイルする
  4. セキュリティポリシーファイルを作成する
    grant {
        permission java.net.SocketPermission "*:1024-65535",
            "connect,accept";
        permission java.net.SocketPermission "*:80", "connect";
    };						
    					
    

とします。

サーバ起動の前にrmiregistryを起動します(図[rmiregistryの起動]参照)。

図 3.1.1.3 rmiregistryの起動
% start rmiregistry				
			

サーバの起動には、codebase, policyを指定します(図[RMIサーバの起動]参照)。

図 3.1.1.4 RMIサーバの起動
% java -Djava.rmi.server.codebase=file:/インタフェースのjarファイルのパス
       -Djava.security.policy=client.policy Computor			
			

クライアントも同様に起動します(図[例題4(RMIクライアント)の起動]参照)。

図 3.1.1.5 例題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

面倒なことが苦手な私は、Eclipse用のRMI PlugInを使ってビルドと実行を自動的に行っています。http://www.genady.net/rmi/を参照してください。

3.1.2 DIを使ったRMIプロキシー

DIを使ったRMIクライアントは、rmirestryからlookupメソッドでIComputeのインスタンを取得した部分をDIとプロキシーを使って記述します。

リスト 3.1.2.1 RMIプロキシーの定義
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メソッド] に示します。

リスト 3.1.2.2 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を使って変更した ところです。

3.1.3 Spring RMIエクスポータ

Springの提供するRMIエクスポータは、rmicを使かわないでサーバ側のサービスをクライアントに提供する機能です。

Spring RMIエクスポータを使った場合のクラス図を図[Spring RMIエクスポータのクラス構成] に示します。 もはや Remote, UnicastRemoteObject に依存していない点に注意してください。

図 3.1.3.1 Spring RMIエクスポータのクラス構成

先の例でIComputeと区別するために、IComputeService インタフェース(リスト[IComputeService]) のように定義します。

リスト 3.1.3.1 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] のようになります。
リスト 3.1.3.2 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エクスポータのコンフィグファイル] と定義します。

リスト 3.1.3.3 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>
このコンフィグファイルを読み込むするだけで、POJOで実装したサービスを リモートのサービスとして公開できるのです。

Spring RMIエクスポータのmainメソッドは極めて簡単です(リスト[SpringRmiServer])。

リスト 3.1.3.4 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のパス				
				

(3)

  1. クライアントは、IComputeがIComputeServiceに変わった部分を除いてリスト[DIを使ったRMIプロキシーのmainメソッド]と同じなので省略します。

4 AOPを使った融合

4.1 AOPの用途

AOPはとても便利で使いやすい反面、多用すると実際のソフトウェアがどのように構成され、どのように振る舞うかを把握しづらくなるという危険性を併せ持っています。

AOPを使用する場合の基本的なスタンスとはどのような ものかを考えてみましょう。

既存の枠組みでは不可能な場合やアルゴリズムの一貫性やプログラム のメンテナンス性を損なうような場合にAOPを適応するのが望ましいと 考えられます。

4.2 プロキシーを使ったメソッドベースのAOP

SpringのAOPは、プロキシーを使ってメソッドへの呼び出しを横取りするInterceptorとコンテナーのどの部分にInterceptorを適応するかを指定するAdvisorから構成されています。

AOPを適応するメソッドがインタフェースで定義されている場合 にはProxyを使いますが、クラスの場合にはCGLibを使ってサブクラスを 生成しプロキシーとします。

AOP では「AOPで埋め込まれる処理」を advice と呼んでいます。 Spring でサポートされている adivceの種類を表[adivceの種類] に示します。

表 4.2.1 adivceの種類
adivceの種類 インタフェース 適応箇所
Around org.aopalliance.intercept.MethodInterceptor メソッド呼び出しを横取りする
Before org.springframework.aop.BeforeAdvice メソッドが呼び出される前
After org.springframework.aop.AfterReturningAdvice リターンの前
Throws org.springframework.aop.ThrowsAdvice 例外が発生した時

4.3 ログ出力

各メソッドの入口と出口にデバッグ用のログを入れたソースを見たことはないでしょうか。同じようなことを、「なぜ」毎回記述しなければならないのか疑問に感じたことはありませんか。

これに答えてくれるのが、AOP を使ったログ出力です。

4.3.1 adviceを定義する

各メソッドの呼び出し前とリターン直前にログを出力する EnterMethodLogAdvice と LeaveMethodLogAdvice を作成して みましょう。

Spring AOP のメソッド呼び出し前の Advice(Before Advice)は、 インタフェース MethodBeforeAdvice を実装しなくてはなりません。

EnterMethodLogAdviceでは、メソッド名と引数を出力します。(4)

リスト 4.3.1.1 EnterMethodLogAdvice
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と同様にメソッド名とリターン値を出力します。

リスト 4.3.1.2 LeaveMethodLogAdvice
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"));
    }
}
  1. 本来ならLogを使ってログに出力するところですが、Springから出力される他のログと区別しにくいので、標準出力に出力しました。

4.3.2 AOPの定義

Springの基本的なAOPの定義は、

を行いますが、今回のようにサービス内のすべてのメソッドに同一のAdviceを適応する場合にはこれでは不便です。代わりにSpringの提供するBeanNameAutoProxyCreatorというAdvisor (AOPを適応する範囲とAdviceを組み合わせたもの)を使用します。BeanNameAutoProxyCreatorには、

を定義します。

ログ出力の Spring コンフィグファイルは、 リスト[ログ出力の Spring コンフィグファイル] のようになります。

リスト 4.3.2.1 ログ出力の 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> 

4.3.3 ログ出力 AOP を動かしてみる

ログ出力AOPを組み入れたコンフィグファイルを実行してみると、図[例題7(ログ出力 AOP)の実行結果]のように出力されます。期待したとおりmemberDaoのメソッド呼び出し前、リターン直前にログが出力されます。

図 4.3.3.1 例題7(ログ出力 AOP)の実行結果
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=杉並区
			

各メソッド毎にチェックプリントを入れていたのと比べると 非常に簡単で、プログラムも読みやすくなります。

4.4 コンテナ外でのインスタンスの生成

SpringのAOPはコンフィグからコンテナーを生成するときに組み込まれてるため、プログラム内で生成されたインスタンスにはAOPが適応されません。

そのため Spring では、

の2つの手段を提供しています。

4.4.1 ファクトリの定義

特定のインスタンを生成するファクトリを定義するには、BeanFactoryAwareインタフェースを実装する必要があります。例として、MemberクラスのインスタンスのファクトリMemberFactory (リスト[MemberFactory])を使って実装の方法を示します。

リスト 4.4.1.1 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のコンフィグ] のように定義します。

リスト 4.4.1.2 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 を使って出力するものです。

リスト 4.4.1.3 MemberFactoryのサンプルプログラム
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 が 実行されています。

図 4.4.1.1 例題8(MemberFactory)の実行結果
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='正太郎'
			

4.4.2 APIベースのAOP

APIベースのAOPが提供されているということは、Springのコンテナーを使用しないアプリケーションでAOPを適応できることを意味します。試しに、リスト[MemberFactory]をAPIを使って書いてみます。

リスト 4.4.2.1 APIを使ったAOPサンプル
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)の実行結果]と同じです。

  1. メソッド名にはクラスパスが付いているため、".*set.*"のようにsetの前に".*"を追加する必要があります。

5 テスト駆動の開発

POJO(Plain Old Java Object)をベースしたSpringのDIを使うとそれぞれの結合が疎になるため、

等のテストを一貫した環境で実施できます。

5.1 DBテストデータ

DB関連のテストをする場合、毎回テスト仕様に合ったデータをDBにセットする必要があります。

Phillippe Girolami 氏によって開発された DbUnitは、

を提供しています。

5.1.1 Dbunitのテストデータ

FaltXmlDataSetの扱うXMLファイルは、以下の形式でデータベースのテーブルの値を保持します。

<dataset>
	<テーブル名
		カラム名="カラムの値"
		... カラム数分繰り返す
		/>
	... レコード分繰り返す
</dataset>
				
			

MemberDaoのMemberを例にすると

<?xml version="1.0" encoding="Windows-31J"?>
<dataset>
	<T_MEMBER
		address='中野区'
		id='1'
		name='たまねぎ'/>
</dataset>
				
			

となります。

5.1.2 DbUnitHelper

Springで提供されているDB用の単体テストケースAbstractTransactionalSpringContextTestsを使用すると同じTestCaseからのサブクラスのDbUnitを使うことができません。

そこで、DbUnit の XML ファイルを Dao でも扱える ように DbUnitHelper を作成しました。DbUnitHelper は、

を提供します(リスト[DbUnitHelper]参照)。 (6)
リスト 5.1.2.1 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: }

  1. DbUnitHelperでのDigesterの利用はFlatXmlDataSet形式の特徴をうまく使って、XMLファイルからJavaオブジェクトへの変換をわずか15行のjavaプログラムで実現しています。

5.1.3 Daoへのdump/restore機能の追加

DbUnitHelperを使ってHibernateMemberDaoにdump/restore機能を追加したDbUnitMemberDaoを作成します(リスト[DbUnitMemberDao]参照)。dump/restore機能は、インタフェースIDbUnitAwareのdump/restoreを実装します。

リスト 5.1.3.1 DbUnitMemberDao
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: }

5.2 DAOの単体テスト

AbstractTransactionalSpringContextTestsは、Springのトランザクション処理を使ってテスト終了後にDBを自動的にテスト前の状態に戻してくれる便利なTestCaseクラスです。

AbstractTransactionalSpringContextTests でトランザクション処理を使用するには コンフィグファイルに PlatformTransactionManager を実装した bean を "transactionManager" という名前で定義しておく必要があります。

単体テスト用のコンフィグファイル変更点をリスト[単体テスト用のコンフィグファイル変更点]に示します。

リスト 5.2.1 単体テスト用のコンフィグファイル変更点
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>

5.2.1 restore を使ったテストケース

restoreを使うとtestXXXXXメソッド毎にDBの設定を行うことができます。

MemberをDBから検索するテストケースを restore を使って 実現すると以下の様になる。

リスト 5.2.1.1 restore を使ったMember検索テスト
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]に示します。

リスト 5.2.1.2 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の内容は元に戻っています。

図 5.2.1.1 単体テスト後のT_MEMBERの内容
(7)

AbstractTransactionalSpringContextTests を使うために以下のjarファイルをクラス パスに追加してください。

resotre, dumpを使うために以下のjarファイルをクラスパスに追加してください。

antでjunitを使用する場合には、junit.jarを$ANT_HOME/libにコピーしてください。

  1. HSQLDBのmanagerでT_MEMBERの内容をSELECTした時の内容です。ソースのbuild.xmlを使った場合には、

    % ant manager
    					
    

    で起動しますので、

    • Type: HSQL Database Engine Standalone
    • Driver: org.hsqldb.jdbcDriver
    • URL: jdbc:hsqldb:data/test
    • User: sa
    • Password:

    でDBの内容が確認できます。

6 おわりに

DIによってModelの設計に集中し、テスト駆動の開発環境によって 安心してリファクタリングできることがよいソフトウェア開発 への一歩につながると思います。

DIをどのように活用すればよいかを教えてくれる一番良い教材が Springコンポーネントです。 今回の例題を元に、すこしずつ使いながらDIの可能性を体感して いただけると幸いです。

Hibernateの導入

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)

図 18 例題1のビルド
% ant Ex_1			
		
例題の番号との対応を表[例題番号とbuildターゲット名]に示します。
表 5 例題番号と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
  1. jdbc.propertiesは本文とは異なりスタンドアローンになっています。サーバモードで使用する場合には、#db.url=jdbc:hsqldb:hsql://localhostのコメント(#)を外し、db.url=jdbc:hsqldb:data/testをコメントにしてご使用下さい。