Powered by SmartDoc

モデル中心プログラミング

竹本 浩
http://www.pwv.co.jp
Springをベースにモデルを中心としたプログラミングについて説明する。

目次

Last modified: Sun Sep 04 17:29:37 JST 2005 since 2005/06/03.

なぜモデル中心プログラミングなのか

よいモデルを作成することがプログラムの醍醐味であるのに GUIを含むアプリケーションやWebアプリケーションではユーザインタフェースの実装に 作業のほとんどの時間を費やさなければならない。

ここでは、Springフレームワークを使ってモデルとビューやコントローラーの結合を疎にしたプログラミング作りについて検証してみる。

Spring が提供してくれるもの

Spring は、コンフィグファイルからコンテンツを動的に生成するツールである。

Springには、次のような特徴がある。

Springは、開発者がモデルの作成に集中できるようインタフェースと実装の結合を疎にし、コンフィグファイルの変更のみによってMockオブジェクトを使った単体テストから、実機への配置までスムーズに対応することができる。

1 Springの導入

ここでは、以下の環境でSpringのアプリケーションを作成する。

Spring のダウンロード方法は、開発環境の構築と活用のSpringを参照されたい。

Eclipseでのプロジェクトの作成方法については、MVCwithEclipse も合わせて参照されたい。

以下の手順でSpring Webアプリケーション用のプロジェクトを作成する。

  1. 新規プロジェクトで"Model1st"をTomcatプロジェクト形式で作成する。
  2. libに

    をインポートする

これで、Springを使ったプロジェクト作成の準備が完了する。

Eclipseのパッケージ・エクスプローラーの表示は以下のようになる。

(1)

  1. docsは、このページ用のディレクトリであり開発には不要である。

2 Springを試してみる

2.1 Hello World

プログラミングのお決まりと言えばHello Worldである。 まずは、Hello WorldをSpringを使って出力してみる。

Springでは、インタフェースを使って実装を分離するのが定石であるので、 それにならって、IHelloService というインタフェースを定義する。

内容は、単にsayHelloメソッドを実装してるだけである。

リスト 2.1.1 IHelloService
public interface IHelloService {
	public void sayHello();
}

次に、IHelloServiceを実装する部分のHelloServiceImplを定義する。

ここで注意しなければならないのは、helloMessage を属性として定義し、 それのsetterを定義している点にある。Springはこのsetterを使ってコンフィグ ファイルからHello Worldメッセージをセットしている。

リスト 2.1.2 HelloServiceImpl
public class HelloServiceImpl implements IHelloService {
	private String	helloMessage;
	
	public void sayHello() {
		System.out.println(helloMessage);
	}
	public void setHelloMessage(String string) {
		helloMessage = string;
	}
}

それでは、mainメソッドを定義する。XmlBeanFactoryで生成された、bean factoryを使って"helloService" beanを取得し、sayHelloメソッドを呼び出している。

リスト 2.1.3 HelloApp
	public static void main(String[] args) {
		BeanFactory factory = 
			new XmlBeanFactory(new FileSystemResource("hello.xml"));
		
		IHelloService helloService = (IHelloService)factory.getBean("helloService");
		helloService.sayHello();
	}

Resourceの指定は、ファイルシステムのパスで指定する場合には、

		new FileSystemResource("hello.xml")	
		

を使用し、クラスパスで指定する場合には、/で始まるクラスパスの相対位置で指定する。

		new ClassPathResource("/hello.xml")	
		

jarファイルにまとめた場合には、ClassPathResourceを使用する。(2)最後にコンフィグファイルを作成する。

リスト 2.1.4 hello.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
	<bean id="helloService" 
		class="pwv.spring.service.HelloServiceImpl">
		<property name="helloMessage">
			<value>Hello World!</value>
		</property>
	</bean>
</beans>

コンソールに以下のメッセージが出力される。(3)

Hello World!		
		

最後に、コンフィグファイルのメッセージを変更して実行してみる。

リスト 2.1.5 メッセージを変更したhello.xml
<?xml version="1.0" encoding="Windows-31J"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
	<bean id="helloService" 
		class="pwv.spring.service.HelloServiceImpl">
		<property name="helloMessage">
			<value>こんにちは、みなさん!</value>
		</property>
	</bean>
</beans>

期待通りコンソールには、

こんにちは、みなさん!			
		

と出力された。

  1. 以前のバージョンでは、XmlBeanFactoryの引数にFileInputStreamを指定してが、1.2.1では、Resourceを指定するように仕様が変更されている。
  2. log4jの設定によっては、loggerのデバッグ情報等が出力される。

2.1.1 「Hello World」の完全ソース

「Hello World」の完全ソースを以下からダウンロードされたい。

2.2 Springの最大の武器 BeanPostProcessor

単にXMLのコンフィグファイルからBeanを生成するならば、他にもツールはある。Springの最大に強みは、beanの生成過程で呼び出されるBeanPostProcessorを駆使した処理にある。

Spring内でBeanPostProcessorを使っている例としては、

がある。これらによってSpringの機能が大きく拡張されている。

BeanPostProcessorの例として、コンフィグファイルに定義されたbeanの数を出力する beanDefinitionCounter を作成してみる。

リスト 2.2.1 BeanDefinitionCounter
public class BeanDefinitionCounter implements BeanFactoryPostProcessor {
	public void postProcessBeanFactory(ConfigurableListableBeanFactory factory)
		throws BeansException {
		System.out.println("Bean definition count = " +
			factory.getBeanDefinitionCount());
	}
}

コンフィグファイルにもbeanCounterを追加する。

リスト 2.2.2 beanCounterを追加したhello.xml
	<bean id="beanCounter"
		class="pwv.spring.util.BeanDefinitionCounter">		
	</bean>

BeanFactoryは、自動的にBeanPostProcessorを検出しないので、ApplicationContextに切り替える。

		BeanFactory factory = 
			new ClassPathXmlApplicationContext("/hello.xml");			
		

コンソールには、期待通りbeanの定義数が出力された。

Bean definition count = 2
Hello World!			
		

2.2.1 「BeanPostProcessor」の完全ソース

「BeanPostProcessor」の完全ソースを以下からダウンロードされたい。

3 AOP を試してみる

アスペクト指向プログラミング(以下AOPと記す)を説明すると

異なるサービスに共通の処理を適応すること

となる。

AOP でよく例に挙げられるのが、ログの出力である。 ここでは Spring の AOP を使ってその機能を検証してみる。

3.1 ログ出力

各メソッドの入口と出口にデバッグ用のログを入れたことはないだろうか。同じようなことを、「なぜ」毎回定義しなければならないのか疑問に感じたことはないだろうか。

これに答えてくれるのが、AOP を使ったログ出力である。 AOP 独自用語の説明よりもまずは動かしてみよう。

3.1.1 adviceを定義する

AOPでは「サービスに共通の処理」をadviceと呼んでいる。

そこで、各メソッドの呼び出し前とリターン直前にログを出力する EnterMethodLogAdvice と LeaveMethodLogAdvice を作成する。

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

public interface MethodBeforeAdvice{
	public void before(Method method, Object[] args, Object target)
		throws Throwable ;
}
			

EnterMethodLogAdviceの実装は至って簡単である。

リスト 3.1.1.1 EnterMethodLogAdvice
public class EnterMethodLogAdvice extends AbstractLogBase
implements MethodBeforeAdvice {		
	public void before(Method method, Object[] args, Object target)
		throws Throwable {
		log = LogFactory.getLog(target.getClass());
		info("enter " + method.getName());
	}
}

同様に、メソッドリターン直前のAdvice(After Advice)は、インタフェースAfterReturningAdviceを実装しなくてはならない。

public interface AfterReturningAdvice{
	public void afterReturning(
		Object returnValue, Method method, 
		Object[] args, Object target)
		throws Throwable ;
}
			

LeaveMethodLogAdviceのソースを以下に示す。

リスト 3.1.1.2 LeaveMethodLogAdvice
public class LeaveMethodLogAdvice
	extends AbstractLogBase
	implements AfterReturningAdvice {
	public void afterReturning(
		Object returnValue, Method method, 
		Object[] args, Object target)
		throws Throwable {
			log = LogFactory.getLog(target.getClass());
			info("leave " + method.getName());
	}

}

ApacheのLog4jを使った時、実行速度を上げるため、以下のようにベースとなるクラスにisDebugEnabledをチェックするメソッドを作成することが多い。

しかしながら、ルートクラスが異なるサービスが混在する場合にはそれぞれのルートクラスに 同様のdebugメソッドを定義しなくてならない。

public void debug(String msg) {
	if (log.isDebugEnabled()) {
		log.debug(msg);
	}
}				
			

しかし、AOPのアドバイスを使うとログを出力するAdviceのルートクラスにのみ実装すれば良くなる。

以下に、AbstractLogBase のソースを示す。

リスト 3.1.1.3 AbstractLogBase
abstract public class AbstractLogBase {
	protected Log	log;
		
	public void debug(String msg) {
		if (log.isDebugEnabled())	log.debug(msg);
	}
	public void info(String msg) {
		if (log.isInfoEnabled()	log.info(msg);
	}
	public void warn(String msg) {
		if (log.isWarnEnabled())	log.warn(msg);
	}	
	public void error(String msg) {
		if (log.isErrorEnabled())	log.error(msg);
	}
	public void fatal(String msg) {
		if (log.isFatalEnabled())	log.fatal(msg);
	}	
}

3.1.2 AOPの定義

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

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

を定義する。

ログ出力の Spring コンフィグファイルは、以下のようになる。

リスト 3.1.2.1 ログ出力の Spring コンフィグファイル
	<!-- メソッドログアドバイス -->
	<bean id="enterMethodLogAdvice"
		class="pwv.spring.advice.EnterMethodLogAdvice"/>
	<bean id="leaveMethodLogAdvice"
		class="pwv.spring.advice.LeaveMethodLogAdvice"/>
	<!-- メソッドログ・プロキシー・クリエータ -->
	<bean id="methodLogProxyCreator"
		class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
		<property name="beanNames">
			<list>
				<value>*Service</value>
			</list>			
		</property>
		<property name="interceptorNames">
			<list>
				<value>enterMethodLogAdvice</value>
				<value>leaveMethodLogAdvice</value>
			</list>			
		</property>
	</bean>

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

hello worldと同様にログ出力AOPを組み入れたコンフィグファイルを実行してみると、

Bean definition count = 5
441   INFO  [main] service.HelloServiceImpl - enter sayHello
Hello World!
441   INFO  [main] service.HelloServiceImpl - leave sayHello				
			

のように期待したとおり、HelloServiceImplの呼び出し前、リターン直前にログが出力されている。

3.1.4 「ログ出力」の完全ソース

「ログ出力」の完全ソースを以下からダウンロードされたい。

4 永続性を試してみる

Springでは永続性をサポートするために、テンプレートとDaoデザイン・パターンを使った永続性を提供している。

また、O/Rマッピングツール Hibernate 用のテンプレートも提供しており、永続性を 実装するのが、容易になった。

しかし、Hibernate の設定も初心者には難しいため、Spring の JdbcTemplate と DbUtil を融合した EDbutilTemplate と EDbHelper を作成し Spring での永続性の実現方法を検証してみた。

4.1 EDbutilTemplateとは何か

EDbutilTemplateは、DbUtilを拡張したEDbUtilをSpringのjdbcTemplateのサブクラスとして実装したものであり、個々のDaoクラスを実装することなく、EDbutilTemplateを使ってオブジェクトの挿入、更新、検索ができる。(4)

EDbutilTemplateの特徴として、

が挙げられる。

EDbUtilTemplateが扱う POJO オブジェクトは、IEBase インタフェースを実装 しなくてはならない。 (6)

このように言うと大変な用に見えるが、各オブジェクトがidという整数型を保持し、以下のgetter, setterを提供するだけでよい。

	public interface IEBase {
		public Integer getId();
		public void setId(Integer id);
	}			
		

このidがテーブルの主キーとなり、EDbUtilTemplateは主キーのカラム名をIDに統治することによって、Dao関係のSQL生成が容易になることを利用している。

  1. EDbUtilではDaoモデルを使わずPOJOオブジェクトに属性のload, insert用の機能を追加していたが、EDbutilTemplateではPOJOオブジェクトとDao機能を分離した。
  2. 現在のところ、HSqlDBに特化している
  3. インターフェースであるため、クラス階層の制約はない。

4.2 EDbHelperとは何か

EDbHelperは指定されたオブジェクトに対する

の機能を提供している。

Member オブジェクトを使って、EDbHelperの機能を確認してみる。 Member のクラス宣言で、implements IEBase としている点に注意されない。

リスト 4.2.1 Member
public class Member implements IEBase {
	private Integer	id;
	private String		name;
	private String		address;
	// 自動生成された getter/setter
	public Integer getId() {
		return id;
	}
	public String getAddress() {
		return address;
	}
	public String getName() {
		return name;
	}
	public void setId(Integer id) {
		this.id = id;
	}
	public void setAddress(String string) {
		address = string;
	}
	public void setName(String string) {
		name = string;
	}
}

それでは、Memberに対するテーブル作成用のSQLは、以下の様になる。

CREATE TABLE T_MEMBER(ID INTEGER NOT NULL PRIMARY KEY,ADDRESS VARCHAR,NAME VARCHAR)				
		

挿入用のSQL文は、以下の通りである。

INSERT INTO T_MEMBER (ADDRESS, ID, NAME)VALUES (? , ?, ?)				
		

jdbcTemplateのupdateに渡す、オブジェクトの配列には、

[Nakano-ku, null, Hiroshi TAKEMOTO]				
		

型の配列には、

[Types.VARCHAR, Types.INTEGER, Types.VARCHAR]				
		

がセットされている。挿入前にオブジェクトのIDは、常にnullでなくてはならない。(7)オブジェクトの文字列への変換は、チェックに有効である。

以下の形式で出力され、テーブルの各カラムにどのような値がセット されているか分かる。

MEMBER: ADDRESS=Nakano-ku ID=27 NAME=Hiroshi TAKEMOTO				
		
  1. IDは、挿入時にセットされる。

4.3 EDbutilTemplateの使用法

4.3.1 EDbutilTemplateの導入

EDbutilTemplateを導入するために、必要なjarファイルは

最後のhsqldb.jarは、使用するデータベースによって変更する必要がある。

次にデータベースの定義を記述したプロパティファイル jdbc.properties を 以下の様に定義する。 (8)

リスト 4.3.1.1 jdbc.properties
db.url=jdbc:hsqldb:hsql://localhost
db.driver=org.hsqldb.jdbcDriver
db.username=sa
db.password=
  1. ここでは、HSQLDBをサーバタイプで使用する設定にしてある。

4.3.2 Springコンフィグファイルの追加項目

EDbutilTemplateを使用するためのSpringコンフィグファイルを以下に示す。

リスト 4.3.2.1 edbutilMinimum
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: 	<!-- EDbUtil テンプレート -->
27: 	<bean id="edbutilTemplate" 
28: 		class="pwv.spring.edbutil.EDbutilTemplate">
29: 		<property name="mappingObjects">
30: 			<list>
31: 				<!-- Member オブジェクト定義 -->
32: 				<bean class="pwv.spring.model.Member"/>	
33: 			</list>
34: 		</property>
35: 		<property name="dataSource">
36: 			<ref bean="dataSource"/>
37: 		</property>
38: 	</bean>	
39: 	<!-- Member Dao -->
40: 	<bean id="memberDao" 
41: 		class="pwv.spring.dao.MemberDao">
42: 		<property name="template">
43: 			<ref bean="edbutilTemplate"/>
44: 		</property>
45: 	</bean>

以上でMember Daoが利用可能となる。

4.3.3 Member Dao を使った永続性の実現

順序は逆になったが、SpringコンフィグファイルでセットしたMember Daoについて、その宣言と使い方を示す。

Member Dao のインタフェース IMember は、以下のようになる。

リスト 4.3.3.1 IMember
public interface IMember {
	void 		insertMember(Member member);
	Member 		findMember(Integer id);
	Member[]	findAllMembers();
	String		printMember(Member member);	
}

IMemberの実装MemberDaoは、DbutilTemplateオブジェクトtemplateを単に呼び出しているだけのきわめて簡単になっている。

リスト 4.3.3.2 MemberDao
public class MemberDao implements IMember {
	private EDbutilTemplate	template;

	public Member[] findAllMembers() {
		List list = template.selectObjects(Member.class, "");
		return (list != null 
			? (Member[])list.toArray(new Member[list.size()])
			: null);
	}
	public Member findMember(Integer id) {
		return ((Member)template.loadObject(Member.class, id));
	}
	public void insertMember(Member member) {
		template.insertObject(member);
	}
	public String printMember(Member member) {
		return (template.printObject(member));
	}
	public EDbutilTemplate getTemplate() {
		return template;
	}
	public void setTemplate(EDbutilTemplate template) {
		this.template = template;
	}
}

4.3.4 テストアプリケーション

最後にテスト用のアプリケーションを定義する。

リスト 4.3.4.1 DbTest1
	public static void main(String[] args) {
		BeanFactory factory = 
			new ClassPathXmlApplicationContext("/dbtest.xml");
		IMember memberDao = (IMember)factory.getBean("memberDao");
		Member takemoto = new Member();
		takemoto.setName("Hiroshi TAKEMOTO");
		takemoto.setAddress("Nakano-ku");
		memberDao.insertMember(takemoto);
		System.out.println("takemoto:" + memberDao.printMember(takemoto));		
		// DBからロードする
		Member loaded = memberDao.findMember(takemoto.getId());
		System.out.println("loaded:" + memberDao.printMember(loaded));
	}
  1. Springコンフィグファイルdbtest.xmlからBeanFactoryを生成し、Member takemotoを生成する
  2. memberDaoを使ってDBに挿入し、挿入後のtakemotoをチェックプリントする
  3. takemotoのIdでmemberDaoからロードしたオブジェクトをチェックプリントし、takemotoと内容が一致する事を確認している。

4.3.5 動作確認

ようやく動かす準備が整ったので、HSQLDB 1.7.1のdemoにあるrunServer.batを使ってDBサーバを起動する。(9)

> runServer.bat

以下に実行結果を示す。

Bean definition count = 10
takemoto:MEMBER: ADDRESS=Nakano-ku ID=0 NAME=Hiroshi TAKEMOTO
loaded:MEMBER: ADDRESS=Nakano-ku ID=0 NAME=Hiroshi TAKEMOTO				
			
  1. HSQLDBをserverモードで起動する事によって、デバッグ中にDB内容を確認する事ができる。

4.3.6 「EDbutilTemplateの使用法」の完全ソース

「EDbutilTemplateの使用法」の完全ソースを以下からダウンロードされたい。

4.4 複雑なデータ構造を扱う

買い物かごを例にもう少し複雑なデータを扱ってみる。

買い物かごには、

がある。

4.4.1 モデルの定義

Memberを除くOrder, OrderItem, Productのモデルを定義以下のように定義する。

リスト 4.4.1.1 Order
public class Order implements IEBase {
	private Integer	id;
	private Double		totalPrice = new Double(0);
	private Member		member;	
	private Integer	memberRef;
	private List		items = new ArrayList();
	private Timestamp	ordered;

	// Order 固有のメソッド
	public Order(Member member) {
		setMember(member);
	}
	public Order() {
		this(null);
	}	
	private void calclateTotalPrice() {
		double	total = 0.0;
		for (int i = 0; i < items.size(); i++) {
			OrderItem item = (OrderItem)items.get(i);
			total += item.getQuantity().doubleValue() * item.getUnitPrice().doubleValue();		
		}
		totalPrice = new Double(total);
	}	
	public void addItem(OrderItem item) {
		items.add(item);
		calclateTotalPrice();
	}	
	public void removeItem(OrderItem item) {
		items.remove(item);	
		calclateTotalPrice();
	}
	public void setMember(Member member) {
		this.member = member;
		if (member != null) {
			memberRef = member.getId();
		}
	}
	public void setItems(List list) {
		items = list;
		calclateTotalPrice();
	}
	// 自動生成した setter, getterは省略
}

Orderには、項目(OrderItem)の追加、削除メソッドaddItem, removeItemと支払い総額(totalPrice)を計算するメソッド、Memberの設定を独自に定義し、残りはsetter/getterを自動生成した。

リスト 4.4.1.2 OrderItem
public class OrderItem implements IEBase {
	private Integer	id;
	private Integer	orderRef;
	private Integer	productRef;
	private Product	product;
	private Integer	quantity;
	
	// OrderItem 固有のメソッド
	public OrderItem(Product product, int quantity) {
		setProduct(product);		
		this.quantity = new Integer(quantity);
	}	
	public OrderItem() {
		this(null, 0);
	}	
	public String getProductName() {
		return (product != null ? product.getName() : "");
	}	
	public Double getUnitPrice() {
		return (product != null ? product.getUnitPrice() : new Double(0));	
	}
	public void setProduct(Product product) {
		this.product = product;
		if (product != null) {
			productRef = product.getId();
		}
	}
	// 自動生成した setter, getterは省略
}

OrderItemには、商品(Product)の名称、単価を取り出す部分を独自に作成し、残りはsetter/getterを自動生成した。

リスト 4.4.1.3 Product
public class Product implements IEBase {
	private Integer	id;
	private String		name;
	private Double		unitPrice;
	
	public Product(String name, double price) {
		this.name = name;
		this.unitPrice = new Double(price);
	}
	
	public Product() {
		this(null, 0.0);
	}
	// 自動生成した setter, getterは省略
}

Member, Order, OrderItem, Productのクラス関連図を以下に示す。

4.4.2 属性DAOインタフェース(IEProperties)

EDbutilTemplateで扱うことができるデータ型は、

のみであるため、ユーザが定義したクラスを扱う場合には、インタフェースIEPropertiesを実装した属性DAOを定義する必要がある。

リスト 4.4.2.1 IEProperties
public interface IEProperties {
	public String	getSupportClsName();
	public void loadProperties(EDbutilTemplate template, Object obj);
	public void insertProperties(EDbutilTemplate template, Object obj);
	public String printProperties(EDbutilTemplate template, Object obj);
}

最初に、注文項目(OrderItem)と商品(Product)の1:1の関係を扱う場合で説明する。

OrderItemProperties が OrderItem の属性を処理する属性 Dao で ある。

リスト 4.4.2.2 OrderItemProperties
public class OrderItemProperties implements IEProperties {
	public String getSupportClsName() {
		return OrderItem.class.getName();
	}
	public void loadProperties(EDbutilTemplate template, Object obj) {
		OrderItem item = (OrderItem)obj;
		if (item.getProductRef() != null && item.getProduct() == null) {
			item.setProduct((Product)template.loadObject(Product.class, item.getProductRef()));
		}
	}
	public void insertProperties(EDbutilTemplate template, Object obj) {
		OrderItem item = (OrderItem)obj;
		if (item.getProductRef() == null && item.getProduct() != null) {
			if (item.getProduct().getId() == null) {
				template.insertObject(item.getProduct());
			}
			item.setProductRef(item.getProduct().getId());
		}
	}
	public String printProperties(EDbutilTemplate template, Object obj) {
		OrderItem item = (OrderItem)obj;
		StringBuffer buf = new StringBuffer();
		buf.append(" { ");
		if (item.getProduct() != null) {
			buf.append(template.printObject(item.getProduct()));
		}
		buf.append(" } ");
		return buf.toString();
	}
}

loadPropertiesメソッドでは、商品のIDであるProductRefを持つがproductが未ロードの場合に、商品をDBからロードし、セットしている。

逆に挿入の時には、Product がセットされており、商品のIDが null (DBに未挿入のレコードを示す)の場合に、DBに挿入し、そのIDを productRefにセットしている。

次に注文項目の一覧を持つ Order の属性 Dao である OrderProperties を示す。

リスト 4.4.2.3 OrderProperties
public class OrderProperties implements IEProperties {
	public String getSupportClsName() {
		return Order.class.getName();
	}
	public void loadProperties(EDbutilTemplate template, Object obj) {
		Order order = (Order)obj;
		if (order.getMemberRef() != null && order.getMember() == null) {
			order.setMember((Member)template.loadObject(Member.class, order.getMemberRef()));
		}
		order.setItems(template.selectObjects(OrderItem.class, "WHERE orderRef=" + order.getId()));
	}
	public void insertProperties(EDbutilTemplate template, Object obj) {
		Order order = (Order)obj;
		if (order.getMemberRef() == null && order.getMember() != null) {
			if (order.getMember().getId() == null) {
				template.insertObject(order.getMember());
			}
			order.setMemberRef(order.getMember().getId());
		}
		List items = order.getItems();
		if (items != null) {
			for (int i = 0; i < items.size(); i++) {
				OrderItem item = (OrderItem)items.get(i);
				if (item.getId() == null) {
					item.setOrderRef(order.getId());
					template.insertObject(item);
				}
			}
		}
	}
	public String printProperties(EDbutilTemplate template, Object obj) {
		Order order = (Order)obj;
		StringBuffer buf = new StringBuffer();
		buf.append(" { ");
		if (order.getMember() != null) {
			buf.append(template.printObject(order.getMember()));
		}
		buf.append(", [ ");
		List items = order.getItems();
		if (items != null) {
			for (int i = 0; i < items.size(); i++) {
				buf.append(template.printObject(items.get(i)));
			}
		}	
		buf.append(" ] ");
		buf.append(" } ");
		return buf.toString();
	}
}

注文項目リストののロードは、EDbutilTemplateのselectObjectsを使って

"WHERE orderRef=" + order.getId()

と検索条件をセットしている。

4.4.3 定義ファイルの変更

先のMemberDaoのテストで使ったSpringコンフィグファイルを以下のように修正する

リスト 4.4.3.1 dbtest.xml
1: 	<!-- EDbUtil テンプレート -->
2: 	<bean id="edbutilTemplate" 
3: 		class="pwv.spring.edbutil.EDbutilTemplate">
4: 		<property name="mappingObjects">
5: 			<list>
6: 				<!-- オブジェクト定義 -->
7: 				<bean class="pwv.spring.model.Member"/>	
8: 				<bean class="pwv.spring.model.Order"/>	
9: 				<bean class="pwv.spring.model.OrderItem"/>	
10: 				<bean class="pwv.spring.model.Product"/>		
11: 			</list>
12: 		</property>
13: 		<property name="supportProperties">
14: 			<list>
15: 				<!-- Properties Dao 定義 -->
16: 				<bean class="pwv.spring.dao.OrderProperties"/>		
17: 				<bean class="pwv.spring.dao.OrderItemProperties"/>			
18: 			</list>
19: 		</property>
20: 		<property name="dataSource">
21: 			<ref bean="dataSource"/>
22: 		</property>
23: 	</bean>	
24: 	<!-- CartService Dao -->
25: 	<bean id="cartServiceDao" 
26: 		class="pwv.spring.dao.CartServiceDao">
27: 		<property name="template">
28: 			<ref bean="edbutilTemplate"/>
29: 		</property>
30: 	</bean>

が主な変更点である。

4.4.4 複雑なデータ構造の実行

テスト用のプログラムは、

リスト 4.4.4.1 DbTest2
	public static void main(String[] args) {
		BeanFactory factory = 
			new ClassPathXmlApplicationContext("/dbtest.xml");
		CartServiceDao dao = (CartServiceDao)factory.getBean("cartServiceDao");
		Member	takemoto = new Member("Hiroshi TAKEMOTO", "Nakano-ku");
		Product	orange = new Product("みかん", 10);
		Product apple = new Product("リンゴ", 15);
		OrderItem	item1 = new OrderItem(orange, 2);
		OrderItem	item2 = new OrderItem(apple, 3);
		Order	order = new Order(takemoto);
		order.addItem(item1);
		order.addItem(item2);
		order.setOrdered(new Timestamp(Calendar.getInstance().getTime().getTime()));
		
		dao.insertOrder(order);
		System.out.println("order:" + dao.printOrder(order));
		
		// DBからロードする
		Order loaded = dao.findOrder(order.getId());
		System.out.println("loaded:" + dao.printOrder(loaded));

	}

のようになる。

  1. Member takemotoを作成する
  2. Product orange, appleを作成する
  3. 注文項目item1, item2にorange 2個、apple 3個を作成する
  4. orderを作成し、item1, item2を追加する
  5. cartService Daoを使ってDBに挿入する

を順に処理し、挿入後とDBからロードした結果を出力している。出力結果は、以下の通りである。

Bean definition count = 14
order:ORDER: ID=0 MEMBERREF=0 ORDERED='2005-06-16' TOTALPRICE=65.0 { MEMBER: ADDRESS='Nakano-ku' 
ID=0 NAME='Hiroshi TAKEMOTO', [ ORDERITEM: ID=0 ORDERREF=0 PRODUCTREF=0 QUANTITY=2 { PRODUCT: 
ID=0 NAME='みかん' UNITPRICE=10.0 } ORDERITEM: ID=2 ORDERREF=0 PRODUCTREF=1 QUANTITY=3 
{ PRODUCT: ID=1 NAME='リンゴ' UNITPRICE=15.0 }  ]  } 
loaded:ORDER: ID=0 MEMBERREF=0 ORDERED='2005-06-16' TOTALPRICE=65.0 { MEMBER: ADDRESS='Nakano-ku' 
ID=0 NAME='Hiroshi TAKEMOTO', [ ORDERITEM: ID=0 ORDERREF=0 PRODUCTREF=0 QUANTITY=2 { PRODUCT: 
ID=0 NAME='みかん' UNITPRICE=10.0 } ORDERITEM: ID=2 ORDERREF=0 PRODUCTREF=1 QUANTITY=3 
{ PRODUCT: ID=1 NAME='リンゴ' UNITPRICE=15.0 }  ]  } 			
			

少し、見にくいので、orderを手で字下げしたものを以下に示す。

order:ORDER: ID=0 MEMBERREF=0 ORDERED='2005-06-16' TOTALPRICE=65.0 
{ 
	MEMBER: ADDRESS='Nakano-ku' ID=0 NAME='Hiroshi TAKEMOTO', 
	[ 
		ORDERITEM: ID=0 ORDERREF=0 PRODUCTREF=0 QUANTITY=2 
		{ PRODUCT: ID=0 NAME='みかん' UNITPRICE=10.0 } 
		ORDERITEM: ID=2 ORDERREF=0 PRODUCTREF=1 QUANTITY=3 
		{ PRODUCT: ID=1 NAME='リンゴ' UNITPRICE=15.0 }  
	]  
} 			
			

比較的複雑なデータ構造でもIEPropertiesを定義するだけで無理なく対応できることが分かる。

4.4.5 「複雑なデータ構造を扱う」の完全ソース

「複雑なデータ構造を扱う」の完全ソースを以下からダウンロードされたい。

4.5 EDbutilTemplateの全ソース

「EDbutilTemplate」の完全ソースを以下からダウンロードされたい。

5 Hibernateとの連携

Hibernateは最近よく使われているO/Rマッピングツールであり、簡単にDBにアクセスすることができる。しかし、その設定ファイルの修得に時間が掛かると思い、二の足を踏んでいた。事例をみると、POJOからDBをセットするといいうよりも、DBの設定からPOJO, Hibernateの設定ファイルを自動生成するケースが多いので、データの永続性を実現する意味では、EDbutilTemplateを使う方が楽である。EDbutilTemplateの欠点は、

がある。そこで、EDbutilTemplateからHibernateTemplateを使ったDAOへの移行方法について検証してみる。

  1. 買い物かごの例題を1000回繰り返し、すべての注文を取り出す例で計測したところ、

    種別 挿入(ミリ秒) 読込み(ミリ秒)
    EDbutilTemplate 42871 20940
    HibernateTemplate 10816 4046

    挿入で4倍、読込みで5倍もHibernateが速いという結果になった。

  2. テーブル名は、T_"クラス名"、カラム名はPOJOの属性名でありgetter/setterを持つ必要がある。

5.1 Hibernateの導入

Hibernateのファイルは、http://www.hibernate.org/から最新のバージョンではなく、広く使われている2.1版のhibernate-2.1.8.zipをダウンロードした。ZIPファイルには、日本語マニュアル、hibernateを使用するときに必要なjarファイルが含まれている。Hibernateを使うために以下のjarファイルをEclipseのlibにインポートし、ビルドパスに追加する。

5.2 MemberDAOのHibernate対応

最初に節[Member Dao を使った永続性の実現]で作成したMember DAOをSpringの提供するHibernateTemplateを使ったDAOに移行してみる。

5.2.1 hbm.xmlの作成

HibernateではDBの値を保持するPOJOと同じディレクトリに

POJOクラス名.hbm.xml

のDB対応設定ファイルを作成する必要がある。Memberクラスに対応したMember.hbm.xmlは、以下のようになる。

リスト 5.2.1.1 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="pwv.spring.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項に各属性名がセットされているだけである。これは、EDbHelperを使ってテーブルを作成しているため、属性名とテーブルのカラム名が一致しているため、"Column"を指定する必要がないためである。

5.2.2 DAOの作成

EDbutilTemplateとHibernateTemplateは処理が非常に類似しているため、Daoの変更は非常に簡単である。

MebmerHibernateDaoのソースを以下に示す。

リスト 5.2.2.1 MemberHibernateDao
package pwv.spring.dao;

import java.util.List;

import org.springframework.orm.hibernate.HibernateTemplate;

import pwv.spring.edbutil.EDbHelper;
import pwv.spring.model.Member;

public class MemberHibernateDao implements IMember {
	private HibernateTemplate	template;
	private EDbHelper			helper = new EDbHelper(Member.class);

	public Member[] findAllMembers() {
		List list = template.loadAll(Member.class);
		return (list != null 
			? (Member[])list.toArray(new Member[list.size()])
			: null);
	}
	public Member findMember(Integer id) {
		return ((Member)template.load(Member.class, id));
	}
	public void insertMember(Member member) {
		template.save(member);
	}
	public String printMember(Member member) {
		return (helper.toString(member));
	}
	public HibernateTemplate getTemplate() {
		return template;
	}
	public void setTemplate(HibernateTemplate template) {
		this.template = template;
	}

}

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

最後にSpringコンフィグファイルをHibernateTemplateを使用するように変更する。

リスト 5.2.3.1 コンフィグファイルのHibernate対応への変更部分
1: 	<bean id="sessionFactory" 
2: 		class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
3: 		<property name="hibernateProperties">
4: 			<props>
5: 			<prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
6: 			</props>
7: 		</property>
8: 		<property name="mappingDirectoryLocations">
9: 			<list>
10: 				<value>classpath:/pwv/spring/model</value>
11: 			</list>
12: 		</property>
13: 		<property name="dataSource">
14: 			<ref bean="dataSource"/>
15: 		</property>
16: 	</bean>	
17: 	<bean id="hibernateTemplate"
18: 		class="org.springframework.orm.hibernate.HibernateTemplate">
19: 		<property name="sessionFactory">
20: 			<ref bean="sessionFactory"/>
21: 		</property>
22: 	</bean>	
23: 	<!-- Member Dao -->
24: 	<bean id="memberDao" 
25: 		class="pwv.spring.dao.MemberHibernateDao">
26: 		<property name="template">
27: 			<ref bean="hibernateTemplate"/>
28: 		</property>
29: 	</bean>	

5.2.4 実行

変更したプログラムを動作させてみる。リスト[DbTest1]ClassPathXmlApplicationContext("/dbtest.xml")のdbtest.xmlをmemberHibernate.xmlに変えるだけである。ここが、Springのすばらしいところである。

takemoto:MEMBER: ADDRESS='Nakano-ku' ID=9 NAME='Hiroshi TAKEMOTO'
loaded:MEMBER: ADDRESS='Nakano-ku' ID=9 NAME='Hiroshi TAKEMOTO'				
			

上記のように正常に動作した。

5.3 EDbHelperのHibernate hbm.xml出力機能の追加

hbm.xmlファイルは、ファイルの数が多くなると手で作成するのは大変な作業になる。そこで、EDbhelperにテーブル定義とhbm.xmlを出力するmainメソッドを追加した。

java pwv.spring.edbutil.EDbHelper pwv.spring.model.Member pwv.spring.model.Product		
		

と出力するクラス名を列記すると

リスト 5.3.1 EDbHelper main の出力
1: <!-- CREATE TABLE FOR T_MEMBER -->
2: CREATE TABLE T_MEMBER(ID INTEGER NOT NULL PRIMARY KEY,ADDRESS VARCHAR,NAME VARCHAR)
3: <!-- Member.hbm.xml -->
4: <?xml version="1.0" encoding="Windows-31J"?>
5: <!DOCTYPE hibernate-mapping	PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
6: 	"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
7: <hibernate-mapping>
8: 	<class 
9: 		name="pwv.spring.model.Member"
10: 		table="T_MEMBER">
11: 		<id name="id">
12: 			<generator class="increment"/>
13: 		</id>
14: 		<property name="address"/>
15: 		<property name="name"/>
16: 	</class>
17: </hibernate-mapping>
18: 
19: <!-- CREATE TABLE FOR T_MEMBER -->
20: CREATE TABLE T_MEMBER(ID INTEGER NOT NULL PRIMARY KEY,ADDRESS VARCHAR,NAME VARCHAR)
21: <!-- Member.hbm.xml -->
22: <?xml version="1.0" encoding="Windows-31J"?>
23: <!DOCTYPE hibernate-mapping	PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
24: 	"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
25: <hibernate-mapping>
26: 	<class 
27: 		name="pwv.spring.model.Member"
28: 		table="T_MEMBER">
29: 		<id name="id">
30: 			<generator class="increment"/>
31: 		</id>
32: 		<property name="address"/>
33: 		<property name="name"/>
34: 	</class>
35: </hibernate-mapping>
36: 

と出力されるので、これを適宜コピーして使用されたい。

5.4 複雑なデータのhbm.xmlの記述方法

節[複雑なデータ構造を扱う]で使ったような複雑なデータをHibernateで使用するためには、hbm.xmlファイルを少し修正する必要がある。以下に、

についての設定方法を示す。

5.4.1 他のオブジェクトの参照

他のオブジェクトを参照する場合には、POJOオブジェクトには、

	private 参照オブジェクトクラス xxxxx;
	private Integer                xxxxxRef;
			

の2個の属性をセットし、それらのgetter/setterを定義する。

これをHibernateのhbm.xmlで記述する場合、

  1. EDbHelperに生成されたhbm.xmlファイルのproperty項のxxxxxRefを削除する
  2. many-to-one項を以下のように定義する
    	<many-to-one
    		name="xxxxx"
    		column="xxxxxRef"
    		cascade="save-update"
    		class="参照オブジェクトクラスパス"/>	
    						
    					
    

5.4.2 リスト参照

同じクラスのオブジェクトのリストを保持している場合には、

	private List  xxxxList;
			

の属性をセットし、リストのgetter/setterを定義する。また、リストのオブジェクトには、

	private Integer yyyyRef; // リストを保持するオブジェクトのID
			

の属性をセットする。このリストをHibernateのhbm.xmlで記述する場合、

	<bag name="xxxxList" 
		cascade="save-update"			
		table="リストに含まれるオブジェクトのテーブル名">
		<key column="yyyyRef" foreign-key="ID"/>
		<one-to-many class="リストに含まれるオブジェクトのクラスパス"/>			
	</bag>
										
			

と定義する。

5.5 CartServiceDaoのHibernate版

5.5.1 hbm.xmlの記述

Order, OrderItem, Productのhbm.xmlファイルを以下に示す。

リスト 5.5.1.1 Order.hbm.xml
1: <?xml version="1.0" encoding="Windows-31J"?>
2: <!DOCTYPE hibernate-mapping
3: 	PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
4: 	"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
5: 
6: <hibernate-mapping>
7: 	<class 
8: 		name="pwv.spring.model.Order"
9: 		table="T_ORDER">
10: 		<id name="id">
11: 			<generator class="increment"/>
12: 		</id>
13: 		<property name="totalPrice"/>
14: 		<property name="ordered"/>
15: 		<many-to-one
16: 			name="member"
17: 			column="memberRef"
18: 			cascade="save-update"
19: 			class="pwv.spring.model.Member"/>	
20: 		<bag name="items" 
21: 			cascade="save-update"			
22: 			table="T_ORDERITEM">
23: 			<key column="orderRef" foreign-key="ID"/>
24: 			<one-to-many class="pwv.spring.model.OrderItem"/>			
25: 		</bag>
26: 	</class>	
27: </hibernate-mapping>
リスト 5.5.1.2 OrderItem.hbm.xml
1: <?xml version="1.0" encoding="Windows-31J"?>
2: <!DOCTYPE hibernate-mapping
3: 	PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
4: 	"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
5: 
6: <hibernate-mapping>
7: 	<class 
8: 		name="pwv.spring.model.OrderItem"
9: 		table="T_ORDERITEM">
10: 		<id name="id">
11: 			<generator class="increment"/>
12: 		</id>
13: 		<property name="orderRef"/>
14: 		<property name="quantity"/>
15: 		<many-to-one
16: 			name="product"
17: 			column="productRef"
18: 			cascade="save-update"
19: 			class="pwv.spring.model.Product"/>				
20: 	</class>	
21: </hibernate-mapping>
リスト 5.5.1.3 Product.hbm.xml
1: <?xml version="1.0" encoding="Windows-31J"?>
2: <!DOCTYPE hibernate-mapping
3: 	PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
4: 	"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
5: 
6: <hibernate-mapping>
7: 	<class 
8: 		name="pwv.spring.model.Product"
9: 		table="T_PRODUCT">
10: 		<id name="id">
11: 			<generator class="increment"/>
12: 		</id>
13: 		<property name="name"/>
14: 		<property name="unitPrice"/>		
15: 	</class>	
16: </hibernate-mapping>
17: 	

5.5.2 DAOの作成

CartServiceのHibernate版Daoは、MemberDaoの時と同じ要領で作成する。

リスト 5.5.2.1 CartServiceHibernateDao
package pwv.spring.dao;

import java.util.List;

import org.springframework.orm.hibernate.HibernateTemplate;

import pwv.spring.edbutil.EDbHelper;
import pwv.spring.model.Member;
import pwv.spring.model.Order;
import pwv.spring.model.OrderItem;
import pwv.spring.model.Product;

public class CartServiceHibernateDao 
implements IMember, IOrder, IOrderItem, IProduct 
{
	private HibernateTemplate	template;
	private EDbHelper			memberHelper = new EDbHelper(Member.class);	
	private EDbHelper			orderHelper = new EDbHelper(Order.class);	
	private EDbHelper			itemHelper = new EDbHelper(OrderItem.class);	
	private EDbHelper			productHelper = new EDbHelper(Product.class);	

	public Member[] findAllMembers() {
		List list = template.loadAll(Member.class);
		return (list != null 
			? (Member[])list.toArray(new Member[list.size()])
			: null);
	}
	public Member findMember(Integer id) {
		return ((Member)template.load(Member.class, id));
	}
	public void insertMember(Member member) {
		template.saveOrUpdate(member);
	}
	public String printMember(Member member) {
		return (memberHelper.toString(member));
	}
	public Order[] findAllOrders() {
		List list = template.loadAll(Member.class);
		return (list != null 
			? (Order[])list.toArray(new Order[list.size()])
			: null);
	}
	public Order findOrder(Integer id) {
		return ((Order)template.load(Order.class, id));
	}
	public void insertOrder(Order order) {
		template.saveOrUpdate(order);
	}
	public String printOrder(Order order) {
		StringBuffer buf = new StringBuffer();
		buf.append(orderHelper.toString(order));
		buf.append(" { ");
		if (order.getMember() != null) {
			buf.append(printMember(order.getMember()));
		}
		buf.append(", [ ");
		List items = order.getItems();
		if (items != null) {
			for (int i = 0; i < items.size(); i++) {
				buf.append(printOrderItem((OrderItem)items.get(i)));
			}
		}	
		buf.append(" ] ");
		buf.append(" } ");
		return buf.toString();
	}
	public Product[] findAllProducts() {
		List list = template.loadAll(Product.class);
		return (list != null 
			? (Product[])list.toArray(new Product[list.size()])
			: null);
	}
	public Product findProduct(Integer id) {
		return ((Product)template.load(Product.class, id));
	}
	public void insertProduct(Product product) {
		template.saveOrUpdate(product);
	}
	public String printProduct(Product product) {
		return (productHelper.toString(product));
	}
	public OrderItem[] findAllOrderItems() {
		List list = template.loadAll(OrderItem.class);
		return (list != null 
			? (OrderItem[])list.toArray(new OrderItem[list.size()])
			: null);
	}
	public OrderItem findOrderItem(Integer id) {
		return ((OrderItem)template.load(OrderItem.class, id));
	}
	public void insertOrderItem(OrderItem orderItem) {
		template.saveOrUpdate(orderItem);
	}
	public String printOrderItem(OrderItem orderItem) {
		StringBuffer buf = new StringBuffer();
		buf.append(itemHelper.toString(orderItem));
		buf.append(" { ");
		if (orderItem.getProduct() != null) {
			buf.append(printProduct(orderItem.getProduct()));
		}
		buf.append(" } ");
		return buf.toString();
	}
	public HibernateTemplate getTemplate() {
		return template;
	}
	public void setTemplate(HibernateTemplate template) {
		this.template = template;
	}

}

5.5.3 EDbHelperを使った書式出力

CartServiceHibernateDaoでもEDHelperを使用してprintXXXXメソッドの書式出力を行っている。単体テストに於いてDBからロードしてきたオブジェクトのチェックをするときに、「EDbHelperを使った書式出力」機能が有効になる。処理もIEPropertiesのprintPropertiesメソッドと類似しているので、EDbutilTemplateからの移行においては流用が可能である。

6 DB関連のテスト

6.1 DbUnit

DBを使ったプログラムのテストは、テストに使用するデータを作成し、 テスト毎にテーブルの値をセットしなくてはならない。

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

を提供している。

DbUnitは、http://dbunit.sourceforge.net/からダウンロードされたい。 また、Phillippe Girolami 氏による解説記事が、 DbUnitとAnthillによるテスト環境の制御 にあるので適宜参照されたい。

DbUnitを導入するために、必要なjarファイルは

である。

6.1.1 Dbunitのテストデータ

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

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

先の、MemberDaoで保存したMemberを例にすると

<?xml version="1.0" encoding="Windows-31J"?>
<dataset>
	<T_MEMBER
		address='Nakano-ku'
		id='0'
		name='Hiroshi TAKEMOTO'/>
</dataset>
				
			

となる。

6.1.2 DbUnitを使ったテストケース

DbUnitを使ったテストケースでは、getConnection, getDataSetを必ず定義しなくてはならない。

EDbTemplateを使ったgetConnectionは次のようになる。

	protected IDatabaseConnection getConnection() throws Exception {
		return new DatabaseConnection(
			template.getDataSource().getConnection());
	}
			

getDataSetでテストに使用するXMLファイルからFlatXmlDataSetを生成する。

	protected IDataSet getDataSet() throws Exception {
		return new FlatXmlDataSet(new FileInputStream("dump.xml"));
	}								
			

MemberをDBから検索テストケースは、以下のようになる。

リスト 6.1.2.1 DbUnitを使ったMember検索テスト
public class DbutilTemplateTestCase1 extends DatabaseTestCase {
	private BeanFactory 		factory;
	private EDbutilTemplate	template;
	private IMember			dao;
	
	protected void setUp() throws Exception {
		factory = new ClassPathXmlApplicationContext("/dbtest.xml");
		template = (EDbutilTemplate)factory.getBean("edbutilTemplate");
		dao = (IMember)factory.getBean("cartServiceDao");
		super.setUp();
	}
	protected void tearDown() throws Exception {
		super.tearDown();
	}
	protected IDatabaseConnection getConnection() throws Exception {
		return new DatabaseConnection(
			template.getDataSource().getConnection());
	}
	protected IDataSet getDataSet() throws Exception {
		return new FlatXmlDataSet(new FileInputStream("dump.xml"));
	}
	
	public void testFindMemberByID() {
		Member	member = dao.findMember(new Integer(0));
		assertEquals("Hiroshi TAKEMOTO", member.getName());
		assertEquals("Nakano-ku", member.getAddress());		
	}

}

6.1.3 「DbUnitを使ったテストケース」の完全ソース

「DbUnitを使ったテストケース」の完全ソースを以下からダウンロードされたい。

6.2 AbstractTransactionalSpringContextTestsを使ったテストケース

DbUnitはテスト作業を楽にしてくれるツールではあるが、

このような問題を解決するために、Springではトランザクション処理を使ってテスト終了後に自動的にテスト前の状態に戻してくれるAbstractTransactionalSpringContextTestsを提供し、開発者のDBの管理を容易にしている。

6.2.1 DbUnitHelper

DbUnitもAbstractTransactionalSpringContextTestsもともにTestCaseのサブクラスであるため、AbstractTransactionalSpringContextTestsを使うとDbUnitの機能を使うことができない。

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

を提供する。 (12)

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

6.2.2 DbutilTemplateのdump/restore機能追加

DbUnitHelperを使ってDbutilTemplateへのdump, resotreメソッドの追加は、

	public void dump(String path) {
		DbUnitHelper dbUnit = new DbUnitHelper(this);
		dbUnit.dump(path);
	}	
	public void restore(String path) {
		DbUnitHelper dbUnit = new DbUnitHelper(this);
		dbUnit.restore(path);
		setupIds();	
	}				
			

と非常に簡単である。(13)

  1. DbutilTemplateでは、restore時にEDbHelperのIDの初期値をDBのIDの最大値から再設定しているため、IDの再現性もカバーしている。

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

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

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

リスト 6.2.3.1 restore を使ったMember検索テスト
public class DbutilTemplateTestCase2
	extends AbstractTransactionalSpringContextTests {	
	private EDbutilTemplate	template;
	private IMember			dao;
	
	protected String[] getConfigLocations() {
		return new String[] {"/unitTest1.xml"};
	}
	protected void onSetUpInTransaction() throws Exception {
		template = (EDbutilTemplate)getContext("/unitTest1.xml").getBean("template");
		dao = (IMember)getContext("/unitTest1.xml").getBean("dao");
		super.onSetUpInTransaction();
	}
	public void testFindMemberByID() {
		template.restore("dump.xml");
		Member	member = dao.findMember(new Integer(0));
		assertEquals("Hiroshi TAKEMOTO", member.getName());
		assertEquals("Nakano-ku", member.getAddress());		
	}
}

AbstractTransactionalSpringContextTestsを使用する場合、getConfigLocations, onSetUpInTransactionメソッドをTestCaseで定義する必要がある。例では、

を行っている。

AbstractTransactionalSpringContextTests を使うために以下のjarファイルをEclipseのlibにインポートし、 ビルドパスに追加する。

resotre, dumpを使うために以下のjarファイルをEclipseのlibにインポートし、ビルドパスに追加する。

6.2.4 「AbstractTransactionalSpringContextTestsを使ったテストケース」の完全ソース

「AbstractTransactionalSpringContextTestsを使ったテストケース」の完全ソースを以下からダウンロードされたい。

6.3 EasyMock を使ったテストケース

EasyMockは、インタフェースからMockオブジェクトを生成するツールである。

EasyMock を使うことによってインタフェースの実装が完了する前に、 Junit を使ったテストケースを作成し、結合テストを行うことができる。

このことは、仕様変更に伴うテストケースの修正を行う上でも 有効な手法であると考えられる。

6.3.1 EasyMock の導入

EasyMock は、http://www.easymock.org/Downloads.htmlからダウンロードされたい。

EasyMockを導入するために、必要なjarファイルは

である。

6.3.2 Member 検索テストケースを EasyMock を使って記述

Member検索テストケースをEasyMockを使って記述すると以下のようになる。

リスト 6.3.2.1 Member 検索テストケースを EasyMock を使った場合
	protected void onSetUpInTransaction() throws Exception {
		control = MockControl.createControl(IMember.class);
		dao = (IMember)control.getMock();
	}	
	protected void onTearDownInTransaction() {
		control.verify();
	}	
	public void testFindMemberByID() {
		Member user = new Member("Hiroshi TAKEMOTO", "Nakano-ku");
		user.setId(new Integer(0));
		control.expectAndReturn(dao.findMember(new Integer(0)), user);
		control.replay();
				
		Member	member = dao.findMember(new Integer(0));
		assertEquals("Hiroshi TAKEMOTO", member.getName());
		assertEquals("Nakano-ku", member.getAddress());		
	}
  1. テストの前にMockControl.createControlでMockControlを作成する
  2. 各testXXXメソッドでcontrol.expectAndReturnでインタフェースIMemberで定義したfindMemberを実行した時の期待値をセットした後、control.replayを呼ぶ(14)
  3. テストの終わりにcontrol.verifyを呼び、controlで定義した通りの処理(回数と値)が同じかどうかをチェックする
  1. 戻り値がないメソッドを呼び出す場合にはsetVoidCallableを使い、例外が発生する場合には、expectAndThrowを使用する。

6.3.3 「EasyMock を使ったテストケース」の完全ソース

「EasyMockを使ったテストケース」の完全ソースを以下からダウンロードされたい。

6.4 Mockの切り替え

EasyMockは結合テストからインタフェースと実装を分離したことに於いて画期的なツールであると言える。

しかしながら、EasyMock を使ったテストケースと実際のテストケースで 別々のソースになってしまうのが難点である。

Spring の IoC を使ってコンフィグファイルによって EasyMock を使う 場合と、使わない場合を切り替えることにする。

6.4.1 IObjectControlインタフェース

SpringによってEasyMockのMockControlを切り替えるインタフェースとして、IObjectControlを以下のように定義する。

リスト 6.4.1.1 IObjectControl
public interface IObjectControl {
	// MockControl を返す
	MockControl	getControl(Class cls);
	// Mockオブジェクトを返す
	Object		getObject();
}

6.4.2 MockObjectControl

IObjectControlを実装し、EasyMockのMockControlを返し、getObjectでモックオブジェクトを返すクラスMockObjectControlを以下の作成した。

リスト 6.4.2.1 MockObjectControl
public class MockObjectControl
implements IObjectControl {
	private MockControl	control;
	public MockObjectControl(Object obj) {
	}
	public MockControl getControl(Class cls) {
		control = MockControl.createControl(cls);
		return control;
	}
	public Object getObject() {
		return control.getMock();
	}
}

6.4.3 RealObjectControl

IObjectControlを実装し、何もしないダミーのMockControlを返し、モックではなく、本物のオブジェクトを返すクラスRealObjectControlを以下のように作成した。

リスト 6.4.3.1 RealObjectControl
public class RealObjectControl extends MockControl 
implements IObjectControl {
	private MockControl	control;
	private Object			obj;

	public MockControl getControl(Class cls) {
		return this;
	}
	public Object getObject() {
		return obj;
	}

	// dummy methods
	public void replay() {}
	public void verify() {}
	public void setVoidCallable() {}
	public void setThrowable(java.lang.Throwable throwable) {}
	public void setReturnValue(boolean value) {}
	... 同様にすべてのメソッドを作成
}

(15)

  1. EasyMockはサブクラスを作らせないような構造になっているため、EclipseのReflectionを使ってコンストラクタを調べ、

    public RealObjectControl(Object obj) {
    	// ダミーのcontrolを作成させるために、ダミーのインタフェース(Interfaceなら何でもいいのでIEBase)
    	// ダミーのIProxyFactory, ダミーのIBehaviorFactoryを渡す
    	super(IEBase.class, new JavaProxyFactory(), (IBehaviorFactory)new DummyBehaviorFactory());
    	this.obj = obj;
    }
    
    public RealObjectControl(
    	Class arg0,
    	IProxyFactory arg1,
    	IBehaviorFactory arg2) {
    	super(arg0, arg1, arg2);
    }
    class DummyBehaviorFactory	
    implements IBehaviorFactory {
    	public IBehavior createBehavior() {
    		return null;
    	}
    }					
    				
    

    と無理矢理インスタンが生成できるようにした。しかしながら、resetを除くすべてのメソッドをダミーで上書きしているため、メソッドの実行には支障はない。

6.4.4 IObjectControlを使ったテストケース

これで、ようやくEasyMockと本物のテストでソースを一本化できるようになった。

リスト 6.4.4.1 IObjectControlを使ったテストケース
public class DbutilTemplateTestCase4
	extends AbstractTransactionalSpringContextTests {	
	private EDbutilTemplate	template;
	private IMember			dao;
	private MockControl		control;
	
	protected String[] getConfigLocations() {
		return new String[] {"/unitTest1.xml"};
	}

	protected void onSetUpInTransaction() throws Exception {
		template = (EDbutilTemplate)getContext("/unitTest1.xml").getBean("template");
		IObjectControl objectControl = (IObjectControl)getContext("/unitTest1.xml").getBean("objectConrol");
		control = objectControl.getControl(IMember.class);
		dao = (IMember)objectControl.getObject();
		super.onSetUpInTransaction();
	}
	
	protected void onTearDownInTransaction() {
		control.verify();
		super.onTearDownInTransaction();
	}	

	public void testFindMemberByID() {
		template.restore("dump.xml");
		Member user = new Member("Hiroshi TAKEMOTO", "Nakano-ku");
		user.setId(new Integer(0));
		control.expectAndReturn(dao.findMember(new Integer(0)), user);
		control.replay();
				
		Member	member = dao.findMember(new Integer(0));
		assertEquals("Hiroshi TAKEMOTO", member.getName());
		assertEquals("Nakano-ku", member.getAddress());		
	}
}

EasyMockを使うときのSpringのコンフィグファイルは、次のようになる。

	<!-- Mock ObjectControl -->
	<bean id="objectConrol"
		class="pwv.spring.mock.MockObjectControl">
		<constructor-arg>
			<ref bean="dao"/>
		</constructor-arg>
	</bean>				

			

本当のDaoオブジェクトを使うときには、

	<!-- Mock ObjectControl -->
	<bean id="objectConrol"
		class="pwv.spring.mock.RealObjectControl">
		<constructor-arg>
			<ref bean="dao"/>
		</constructor-arg>
	</bean>				

			

とするだけである。

6.4.5 「Mockの切り替え」の完全ソース

「Mockの切り替え」の完全ソースを以下からダウンロードされたい。

7 MVC を試してみる

Springの提供するMVC機能について検証してみる。

HTTP リクエストが Spring でどのように処理されるかを [1]の Figure8.1 を参考に説明する。

  1. クライアントの要求をDispatchServlet (フロントコントローラ)が受け取る
  2. DispatchServletは、どのコントローラに処理を振り分けるかをHandlerMappingに問い合わせ、Controllerを得る
  3. Controllerはビジネスロジックを実装してるサービスに処理を委譲する
  4. Controllerは、ModelAndViewオブジェクトをDisplatchServletに返す
  5. ModelAndViewのviewの論理名からviewオブジェクトを得るためにViewResolerに要求する
  6. ViewResolerから返されたviewオブジェクトに処理を振り分け、viewオブジェクトがレンダリング処理を行う

ユーザが作成するのは、Controller だけである。Spring では用途に合わせて いくつかの Controller を提供している。

Controller の種別 クラス 用途
Simple

Controller Interface

AbstractController

単にページを切り替えるような場合
Throwaway Throwawaycontroller リクエストをコマンドとして処理する場合
Mulit-Action MultiActionContorller 類似したロジックを処理する場合
Command

BasicCommandController

AbstractCommandController

Controllerが1個以上のパラメータを受け取る場合
Form

AbstractFormController

SimpleFormContorller

フォームを扱う場合
Wizard AbstractWizardFormController アンケートのように複数のページを扱う場合

7.1 MemberController の作成

7.1.1 MemberController の概要

節[Member Dao を使った永続性の実現]を使ってメンバーIDからメンバー の情報(氏名、住所)を検索する MemberController を作成してみる。

処理の振り分けをするために action パラメータを使用し、 メンバーの検索には、memberId パラメータにメンバーIDをセット する。

7.1.2 Command の実装

HTTPリクエストのパラメータをオブジェクトの属性にセットするためにMemberController用のコマンドオブジェクトDisplayMemberCommandを実装する。

実装といっても単にコマンドパラメータの型と名称を private にセットし、getter/setter を自動生成するだけである。

リスト 7.1.2.1 DisplayMemberCommand
public class DisplayMemberCommand {
	String	action;
	Integer memberId;
	// 自動生成された getter/setterは省略
}

7.1.3 Validator の実装

Springではコマンドの検証メカニズムをValidatorインタフェースによって提供している。

Validator は次のインタフェースを実装する必要がある。

public interface Validator {
	boolean supports(Class cls);
	void validate(Object command, Errors errors);
}					
				

検出されたエラーは、

を使って view に返される。

DisplayMemberCommand の Validator (DisplayCommandValidator)は 次の様に実装した。

リスト 7.1.3.1 DisplayCommandValidator
public class DisplayCommandValidator implements Validator {
	public boolean supports(Class cls) {
		return cls.equals(DisplayMemberCommand.class);
	}
	public void validate(Object command, Errors errors) {
		DisplayMemberCommand displayCommand = (DisplayMemberCommand)command;
		
		ValidationUtils.rejectIfEmpty(errors, "action", "required.action", "Action required.");
		ValidationUtils.rejectIfEmpty(errors, "memberId", "required.memberId", "MemberId required.");
	}
}

7.1.4 MemberController の実装

最後に、Controllerの実装であるが、きわめて単純に実装した。引数を使用するので、AbstractCommandControllerのサブクラスとした。

IMember を属性 dao に保持し、command から memberId を取得して検索し、 それを ModelAndView にセットして返すだけである。

ModelAndView の定義は次の通りである。

public ModelAndView(String viewName, String modelName, Object modelObject)				
				

MemberConntrollerを以下に示す。

リスト 7.1.4.1 MemberController
public class MemberController extends AbstractCommandController {
	private IMember	dao;
	
	// コンストラクタでコマンドオブジェクトクラスを設定
	public MemberController() {
		setCommandClass(DisplayMemberCommand.class);
	}
	public void setDao(IMember member) {
		dao = member;
	}
	protected ModelAndView handle(
		HttpServletRequest request,
		HttpServletResponse response,
		Object command,
		BindException errors)
		throws Exception {
		DisplayMemberCommand displayCommand = (DisplayMemberCommand)command;
		if (displayCommand.getAction().equals("start")) {
			return new ModelAndView("find", "find", null);
		}
		else if (errors.hasErrors()) {
			return new ModelAndView("error", "error", errors.getModel());
		}
		else if (displayCommand.getAction().equals("find")){
			return new ModelAndView("member", "member", dao.findMember(displayCommand.getMemberId()));
		}
		else {
			return (null);
		}
	}
}

7.1.5 「MemberController の作成」の完全ソース

「MemberControllerの作成」の完全ソースを以下からダウンロードされたい。

7.2 MockHttpServletRequest を使った Conroller の単体テスト

7.2.1 MockHttpServletRequest の使い方

MockHttpServletRequest を使うと Controller を tomcat の webapps に配置しないで 単体テストを行うことができる。

MockHttpServletRequest は、以下の手順で使用する。

	// MockHttpServletRequest を生成
	MockHttpServletRequest	request = new MockHttpServletRequest("POST", "コントローラのURI");
	// パラメータをセット
	request.addParameter("パラメータ名", "パラメータの値");
	// 目的のコントローラを beanFactory から取得
	Controller	controller = beanFactory.get(""コントローラのURI"");
	// HTTP要求を処理させ、コントローラの戻り値をModelAndView を取得
	ModelAndView modelAndView = controller.handleRequest(request, null);
				

7.2.2 MemberController の単体テスト

MemberControllerのテストケースは、以下のようになる。

リスト 7.2.2.1 MockHttpTestCase
public class MockHttpTestCase
	extends AbstractTransactionalSpringContextTests {	
	private EDbutilTemplate	template;
	private IMember			dao;
	private Controller			controller;
	
	protected String[] getConfigLocations() {
		return new String[] {"/unitTest1.xml"};
	}
	protected void onSetUpInTransaction() throws Exception {
		template = (EDbutilTemplate)getContext("/unitTest1.xml").getBean("template");
		dao = (IMember)getContext("/unitTest1.xml").getBean("dao");
		controller = (Controller)getContext("/unitTest1.xml").getBean("/member.htm");
		super.onSetUpInTransaction();
	}
	protected void onTearDownInTransaction() {
		super.onTearDownInTransaction();
	}	
	public void testFindMemberByID() {
		template.restore("dump.xml");

		MockHttpServletRequest	request = new MockHttpServletRequest("POST", "/member.htm");
		request.addParameter("memberId", "0");
		try {
			ModelAndView modelAndView = controller.handleRequest(request, null);

			Member	member = (Member)modelAndView.getModel().get("member");
			assertEquals("Hiroshi TAKEMOTO", member.getName());
			assertEquals("Nakano-ku", member.getAddress());		
		}
		catch (Exception e) {
			fail();
		}		
	}
}

7.2.3 「MemberController の単体テスト」の完全ソース

「MemberControllerの単体テスト」の完全ソースを以下からダウンロードされたい。

7.3 アプリケーションへの拡張

SpringのMVCを使ってWebアプリケーションを作成する前に、Javaアプリケーションを作成してみる。(16)

  1. 多くの雑誌や書籍には、Springのアプリケーションへの応用は説明されていないが、この例でその有効性を実感できる。

7.3.1 モデルとViewの結合

Controllerの返すModelAndViewには、

がセットされている。

このビューの論理名とJFrameのビューを結びつければ、java アプリケーション版のMVCが実現できる。

HTTP要求とその戻り値であるModelAndView を関連づける メッセージディスパッチャーのインタフェース IMessageDispatcher 以下の様に定義する。

リスト 7.3.1.1 IMessageDispatcher
public interface IMessageDispatcher {
	void	doSubmit(String url, String action);
	void	doSubmit(String url, Map args);
	void 	addObserver(Observer o);
}

7.3.2 IMessageDispatcher の実装

MockHttpServletRequestを使ったアプリケーション用のメッセージディスパッチャーを以下に示す。

リスト 7.3.2.1 MockMessageDispatcher
public class MockMessageDispatcher extends Observable 
implements IMessageDispatcher {
	private Controller	contorller;
	private Map		listener = new HashMap();
	
	public synchronized void addObserver(Observer o) {
		if (o instanceof Component) {
			Component com = (Component)o;
			listener.put(com.getName(), o);
		}
	}

	public void doSubmit(String url, Map args) {
		MockHttpServletRequest	request = new MockHttpServletRequest("POST", url);
		Iterator itr = args.keySet().iterator();
		while (itr.hasNext()) {
			Object key = (Object)itr.next();
			request.addParameter((String)key, (String)args.get(key));
		}
		try {
			ModelAndView modelAndView = contorller.handleRequest(request, null);
			String	view = modelAndView != null ? modelAndView.getViewName() : null;
			Observer target = (Observer)listener.get(view);
			super.addObserver(target);
			this.setChanged();
			notifyObservers(modelAndView);
			super.deleteObserver(target);
		}
		catch (Exception e) {
		}
	}
	public void doSubmit(String url, String action) {
		HashMap	args = new HashMap();
		args.put("action", action);
		doSubmit(url, args);
	}
	public void setContorller(Controller controller) {
		contorller = controller;
	}	
}

MockMessageDispatcherは、Observableのサブクラスとしたが、すべてのObserverにupdateイベント送っては困るので、addObserverでObserverであるビューをlinstenerに登録し、doSubmitの戻り値であるビューの論理名でメッセージを振り分けるビューを取り出し、updateイベントを送るようにした。(17)

  1. ビューであるobserverにはComponentのインスタンスとしたので、swingのすべての要素がイベントのリスナーとして登録できる。

7.3.3 ビューの IMessageDispatcher 対応

ビューは、以下のメソッドを実装しなければならない。

	protected IMessageDispatcher	dispatcher;

	public void setName(String name) {
		super.setName(name);
		if (dispatcher != null) {
			dispatcher.addObserver(this);
		}
	}
	public void setDispatcher(IMessageDispatcher dispatcher) {
		this.dispatcher = dispatcher;
		dispatcher.addObserver(this);
	}
	public void update(Observable o, Object arg) {
	}				
			

name, dispatcherのsetterではビューのdispatcherへの登録を行い、updateメソッドには、updateイベントの処理を記述する。

7.3.4 Frame、Dialog の実装

JFrame, JDialogに対するdispatcher, nameのsetterを実装したAbstractDispatchFrame, AbstractDispatchDialogを以下のように定義した。

リスト 7.3.4.1 AbstractDispatchFrame
public abstract class AbstractDispatchFrame extends JFrame 
implements Observer {
	protected IMessageDispatcher	dispatcher;

	public void setName(String name) {
		super.setName(name);
		if (dispatcher != null) {
			dispatcher.addObserver(this);
		}
	}
	public void setDispatcher(IMessageDispatcher dispatcher) {
		this.dispatcher = dispatcher;
		dispatcher.addObserver(this);
	}
	
	abstract public void update(Observable o, Object arg);	
}
リスト 7.3.4.2 AbstractDispatchDialog
public abstract class AbstractDispatchDialog extends JDialog 
implements Observer {
	protected IMessageDispatcher	dispatcher;

	public void setName(String name) {
		super.setName(name);
		if (dispatcher != null) {
			dispatcher.addObserver(this);
		}
	}
	public void setDispatcher(IMessageDispatcher dispatcher) {
		this.dispatcher = dispatcher;
		dispatcher.addObserver(this);
	}
	abstract public void update(Observable o, Object arg);	
}

ユーザは、これらのサブクラスを定義し、インタフェースObserverのupdateメソッドを定義すればよい。

7.3.5 FindFrame の実装

検索条件を入力するFindFrameをEclipseのGUIビルダーVisual Editorを使って以下のように作成した。

最後に、サブクラスをJFrameからAbstractDispatchFrameに変更し、updateメソッド、検索ボタンのコールバックを以下のように定義する。

	public void update(Observable o, Object arg) {
		this.setModal(true);
		show();
	}
	private javax.swing.JButton getJFindButton() {
		if(jFindButton == null) {
			jFindButton = new javax.swing.JButton();
			jFindButton.setText("検索");
			jFindButton.addActionListener(new java.awt.event.ActionListener() { 
				public void actionPerformed(java.awt.event.ActionEvent e) {    
					HashMap args = new HashMap();
					args.put("action", "find");
					args.put("memberId", getJMemberIdField().getText());
					dispatcher.doSubmit("/member.htm", args);
					setVisible(false);
				}
			});
		}
		return jFindButton;
	}				
			

7.3.6 MemberFrame の実装

FindFrameと同様にMemberFrameもVisual Editorで作成し、updateメソッドを以下の様に定義する。

	public void update(Observable o, Object arg) {
		ModelAndView mv = (ModelAndView)arg;
		if (mv != null) {
			Member member = mv.getModel() != null 
							? (Member)mv.getModel().get("member")
							: null;
			if (member != null) {
				getJNameField().setText(member.getName());
				getJAddressField().setText(member.getAddress());
			}
		}
		show();
	}				
			

単に、ModelAndViewからmemberモデルを取り出し、そのname, addressをMemberFrameのフィールドにセットしているだけである。

7.3.7 ErrorFrame の実装

エラーメッセージを表示するErrorFrameは、メッセージが長いので、";"で改行するようにした。

	public void update(Observable o, Object arg) {
		ModelAndView mv = (ModelAndView)arg;
		if (mv != null) {
			HashMap error = mv.getModel() != null 
							? (HashMap)mv.getModel().get("error")
							: null;
			if (error != null) {
				BindException bindEx = (BindException)error.get(BindException.class.getName() + ".command");
				String	msg = bindEx.getMessage();
				getJMessageArea().setText(msg.replaceAll(";", "\n"));
			}
		}
		pack();
		show();
	}
			

7.3.8 MainFrame の実装

MainFrameもVisual Editorで作成した。メインウィンドウとメニューバーの簡単なもので、検索メニューのコールバックは、以下のようになる。

	private javax.swing.JMenuItem getJFindItem() {
		if(jFindItem == null) {
			jFindItem = new javax.swing.JMenuItem();
			jFindItem.setText("メンバ検索");
			jFindItem.addActionListener(new java.awt.event.ActionListener() { 
				public void actionPerformed(java.awt.event.ActionEvent e) {    
					dispatcher.doSubmit("/member.htm", "start");
				}
			});
		}
		return jFindItem;
	}				
			

単にdoSubmit("/member.htm", "start")としているだけである。

7.3.9 アプリケーションの実装

Springのアプリケーションは、いずれも同じ簡単な構造となる。

リスト 7.3.9.1 MVCTestApp
public class MVCTestApp {
	public static void main(String[] args) {
		BeanFactory factory = 
			new ClassPathXmlApplicationContext("/mvcTest.xml");
		JFrame	main = (JFrame)factory.getBean("mainFrame");
		main.show();
	}
}

7.3.10 コンフィグファイル

コンフィグファイルは、dispatcherを定義し、各Frameでそれをセットするだけのきわめて簡単な構造となる。

リスト 7.3.10.1 mvcTest.xml
1: 	<!-- Message dispatcher -->
2: 	<bean name="dispatcher"
3: 		class="pwv.spring.dispatcher.MockMessageDispatcher">
4: 		<property name="contorller">
5: 			<ref bean="/member.htm"/>
6: 		</property>
7: 	</bean>
8: 	<!-- Frame -->
9: 	<bean name="mainFrame"
10: 		class="pwv.spring.view.MainFrame">
11: 		<property name="dispatcher">
12: 			<ref bean="dispatcher"/>
13: 		</property>
14: 	</bean>
15: 	<bean name="findFrame"
16: 		class="pwv.spring.view.FindFrame">
17: 		<property name="dispatcher">
18: 			<ref bean="dispatcher"/>
19: 		</property>
20: 		<property name="name">
21: 			<value>find</value>
22: 		</property>
23: 	</bean>
24: 	<bean name="memberFrame"
25: 		class="pwv.spring.view.MemberFrame">
26: 		<property name="dispatcher">
27: 			<ref bean="dispatcher"/>
28: 		</property>
29: 		<property name="name">
30: 			<value>member</value>
31: 		</property>
32: 	</bean>
33: 	<bean name="errorFrame"
34: 		class="pwv.spring.view.ErrorFrame">
35: 		<property name="dispatcher">
36: 			<ref bean="dispatcher"/>
37: 		</property>
38: 		<property name="name">
39: 			<value>error</value>
40: 		</property>
41: 	</bean>		

7.3.11 アプリケーションの実行

アプリケーションを実行すると、

図 7.3.11.1 メインウィンドウ

が表示され、「操作」メニューから「検索」を選択すると、

図 7.3.11.2 検索条件設定画面

検索条件設定画面が表示される。

ここで、Member ID に 0 をセットし、検索ボタンを押すと、

図 7.3.11.3 メンバー情報画面

が表示される。Member IDにaと数字以外を入力すると、

図 7.3.11.4 エラー画面

が表示される。

7.3.12 「アプリケーションへの拡張」の完全ソース

「アプリケーションへの拡張」の完全ソースを以下からダウンロードされたい。

参考文献

[1]Graig Walls and Ryan Greidenbach. Spring in Action. Manning,
[2]長谷川裕一、伊藤清人、岩永寿来、大野渉. Java・J2EE・オープンソース Spring入門 : より良いWebアプリケーションの設計と実装. 技術評論社,
[3]河村嘉之、首藤智大、竹内祐介、吉尾真祐. 実践 Spring Framework : J2EE 開発を変える DI コンテナのすべて. ,