Last modified: Sun Sep 04 17:29:37 JST 2005 since 2005/06/03.
よいモデルを作成することがプログラムの醍醐味であるのに GUIを含むアプリケーションやWebアプリケーションではユーザインタフェースの実装に 作業のほとんどの時間を費やさなければならない。
ここでは、Springフレームワークを使ってモデルとビューやコントローラーの結合を疎にしたプログラミング作りについて検証してみる。
Spring は、コンフィグファイルからコンテンツを動的に生成するツールである。
Springには、次のような特徴がある。
Springは、開発者がモデルの作成に集中できるようインタフェースと実装の結合を疎にし、コンフィグファイルの変更のみによってMockオブジェクトを使った単体テストから、実機への配置までスムーズに対応することができる。
ここでは、以下の環境でSpringのアプリケーションを作成する。
Spring のダウンロード方法は、開発環境の構築と活用のSpringを参照されたい。
Eclipseでのプロジェクトの作成方法については、MVCwithEclipse も合わせて参照されたい。
以下の手順でSpring Webアプリケーション用のプロジェクトを作成する。
libに
をインポートする
これで、Springを使ったプロジェクト作成の準備が完了する。
Eclipseのパッケージ・エクスプローラーの表示は以下のようになる。
プログラミングのお決まりと言えばHello Worldである。 まずは、Hello WorldをSpringを使って出力してみる。
Springでは、インタフェースを使って実装を分離するのが定石であるので、 それにならって、IHelloService というインタフェースを定義する。
内容は、単にsayHelloメソッドを実装してるだけである。
public interface IHelloService { public void sayHello(); }
次に、IHelloServiceを実装する部分のHelloServiceImplを定義する。
ここで注意しなければならないのは、helloMessage を属性として定義し、 それのsetterを定義している点にある。Springはこのsetterを使ってコンフィグ ファイルからHello Worldメッセージをセットしている。
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メソッドを呼び出している。
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)最後にコンフィグファイルを作成する。
<?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!
最後に、コンフィグファイルのメッセージを変更して実行してみる。
<?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>
期待通りコンソールには、
こんにちは、みなさん!
と出力された。
「Hello World」の完全ソースを以下からダウンロードされたい。
単にXMLのコンフィグファイルからBeanを生成するならば、他にもツールはある。Springの最大に強みは、beanの生成過程で呼び出されるBeanPostProcessorを駆使した処理にある。
Spring内でBeanPostProcessorを使っている例としては、
がある。これらによってSpringの機能が大きく拡張されている。
BeanPostProcessorの例として、コンフィグファイルに定義されたbeanの数を出力する beanDefinitionCounter を作成してみる。
public class BeanDefinitionCounter implements BeanFactoryPostProcessor { public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException { System.out.println("Bean definition count = " + factory.getBeanDefinitionCount()); } }
コンフィグファイルにもbeanCounterを追加する。
<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!
「BeanPostProcessor」の完全ソースを以下からダウンロードされたい。
アスペクト指向プログラミング(以下AOPと記す)を説明すると
異なるサービスに共通の処理を適応すること
となる。
AOP でよく例に挙げられるのが、ログの出力である。 ここでは Spring の AOP を使ってその機能を検証してみる。
各メソッドの入口と出口にデバッグ用のログを入れたことはないだろうか。同じようなことを、「なぜ」毎回定義しなければならないのか疑問に感じたことはないだろうか。
これに答えてくれるのが、AOP を使ったログ出力である。 AOP 独自用語の説明よりもまずは動かしてみよう。
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の実装は至って簡単である。
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のソースを以下に示す。
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 のソースを示す。
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); } }
Springの基本的なAOPの定義は、
を行うが、今回のようにサービス内のすべてのメソッドに同一のAdviceを適応する場合にはこれでは不便である。そこで、Springの提供するBeanNameAutoProxyCreatorというAdvisor (AOPを適応する範囲とAdviceを組み合わせたもの)を使用する。BeanNameAutoProxyCreatorには、
を定義する。
ログ出力の 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>
hello worldと同様にログ出力AOPを組み入れたコンフィグファイルを実行してみると、
Bean definition count = 5 441 INFO [main] service.HelloServiceImpl - enter sayHello Hello World! 441 INFO [main] service.HelloServiceImpl - leave sayHello
のように期待したとおり、HelloServiceImplの呼び出し前、リターン直前にログが出力されている。
「ログ出力」の完全ソースを以下からダウンロードされたい。
Springでは永続性をサポートするために、テンプレートとDaoデザイン・パターンを使った永続性を提供している。
また、O/Rマッピングツール Hibernate 用のテンプレートも提供しており、永続性を 実装するのが、容易になった。
しかし、Hibernate の設定も初心者には難しいため、Spring の JdbcTemplate と DbUtil を融合した EDbutilTemplate と EDbHelper を作成し Spring での永続性の実現方法を検証してみた。
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生成が容易になることを利用している。
EDbHelperは指定されたオブジェクトに対する
の機能を提供している。
Member オブジェクトを使って、EDbHelperの機能を確認してみる。 Member のクラス宣言で、implements IEBase としている点に注意されない。
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
EDbutilTemplateを導入するために、必要なjarファイルは
最後のhsqldb.jarは、使用するデータベースによって変更する必要がある。
次にデータベースの定義を記述したプロパティファイル jdbc.properties を 以下の様に定義する。 (8)
db.url=jdbc:hsqldb:hsql://localhost db.driver=org.hsqldb.jdbcDriver db.username=sa db.password=
EDbutilTemplateを使用するためのSpringコンフィグファイルを以下に示す。
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が利用可能となる。
順序は逆になったが、SpringコンフィグファイルでセットしたMember Daoについて、その宣言と使い方を示す。
Member Dao のインタフェース IMember は、以下のようになる。
public interface IMember { void insertMember(Member member); Member findMember(Integer id); Member[] findAllMembers(); String printMember(Member member); }
IMemberの実装MemberDaoは、DbutilTemplateオブジェクトtemplateを単に呼び出しているだけのきわめて簡単になっている。
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; } }
最後にテスト用のアプリケーションを定義する。
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)); }
ようやく動かす準備が整ったので、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
「EDbutilTemplateの使用法」の完全ソースを以下からダウンロードされたい。
買い物かごを例にもう少し複雑なデータを扱ってみる。
買い物かごには、
がある。
Memberを除くOrder, OrderItem, Productのモデルを定義以下のように定義する。
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を自動生成した。
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を自動生成した。
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のクラス関連図を以下に示す。
EDbutilTemplateで扱うことができるデータ型は、
のみであるため、ユーザが定義したクラスを扱う場合には、インタフェースIEPropertiesを実装した属性DAOを定義する必要がある。
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 で ある。
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 を示す。
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()
と検索条件をセットしている。
先のMemberDaoのテストで使ったSpringコンフィグファイルを以下のように修正する
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>
が主な変更点である。
テスト用のプログラムは、
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)); }
のようになる。
を順に処理し、挿入後と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を定義するだけで無理なく対応できることが分かる。
「複雑なデータ構造を扱う」の完全ソースを以下からダウンロードされたい。
「EDbutilTemplate」の完全ソースを以下からダウンロードされたい。
Hibernateは最近よく使われているO/Rマッピングツールであり、簡単にDBにアクセスすることができる。しかし、その設定ファイルの修得に時間が掛かると思い、二の足を踏んでいた。事例をみると、POJOからDBをセットするといいうよりも、DBの設定からPOJO, Hibernateの設定ファイルを自動生成するケースが多いので、データの永続性を実現する意味では、EDbutilTemplateを使う方が楽である。EDbutilTemplateの欠点は、
がある。そこで、EDbutilTemplateからHibernateTemplateを使ったDAOへの移行方法について検証してみる。
買い物かごの例題を1000回繰り返し、すべての注文を取り出す例で計測したところ、
種別 | 挿入(ミリ秒) | 読込み(ミリ秒) |
---|---|---|
EDbutilTemplate | 42871 | 20940 |
HibernateTemplate | 10816 | 4046 |
挿入で4倍、読込みで5倍もHibernateが速いという結果になった。
Hibernateのファイルは、http://www.hibernate.org/から最新のバージョンではなく、広く使われている2.1版のhibernate-2.1.8.zipをダウンロードした。ZIPファイルには、日本語マニュアル、hibernateを使用するときに必要なjarファイルが含まれている。Hibernateを使うために以下のjarファイルをEclipseのlibにインポートし、ビルドパスに追加する。
最初に節[Member Dao を使った永続性の実現]で作成したMember DAOをSpringの提供するHibernateTemplateを使ったDAOに移行してみる。
HibernateではDBの値を保持するPOJOと同じディレクトリに
POJOクラス名.hbm.xml
のDB対応設定ファイルを作成する必要がある。Memberクラスに対応したMember.hbm.xmlは、以下のようになる。
1: <?xml version="1.0" encoding="Windows-31J"?> 2: <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN" 3: "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"> 4: <hibernate-mapping> 5: <class 6: name="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"を指定する必要がないためである。
EDbutilTemplateとHibernateTemplateは処理が非常に類似しているため、Daoの変更は非常に簡単である。
例)template.loadObject(Member.class, id));
からtemplate.load(Member.class, id));
に修正する
helper.toString(member)
MebmerHibernateDaoのソースを以下に示す。
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; } }
最後にSpringコンフィグファイルをHibernateTemplateを使用するように変更する。
mappingDirectoryLocationsでhbm.xmlの置かれているディレクトリを指定する
classpath:/pwv/spring/model
のようにクラス名を/で区切って指定する
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>
変更したプログラムを動作させてみる。リスト[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'
上記のように正常に動作した。
hbm.xmlファイルは、ファイルの数が多くなると手で作成するのは大変な作業になる。そこで、EDbhelperにテーブル定義とhbm.xmlを出力するmainメソッドを追加した。
java pwv.spring.edbutil.EDbHelper pwv.spring.model.Member pwv.spring.model.Product
と出力するクラス名を列記すると
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:
と出力されるので、これを適宜コピーして使用されたい。
節[複雑なデータ構造を扱う]で使ったような複雑なデータをHibernateで使用するためには、hbm.xmlファイルを少し修正する必要がある。以下に、
についての設定方法を示す。
他のオブジェクトを参照する場合には、POJOオブジェクトには、
private 参照オブジェクトクラス xxxxx; private Integer xxxxxRef;
の2個の属性をセットし、それらのgetter/setterを定義する。
これをHibernateのhbm.xmlで記述する場合、
<many-to-one name="xxxxx" column="xxxxxRef" cascade="save-update" class="参照オブジェクトクラスパス"/>
同じクラスのオブジェクトのリストを保持している場合には、
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>
と定義する。
Order, OrderItem, 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.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>
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>
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:
CartServiceのHibernate版Daoは、MemberDaoの時と同じ要領で作成する。
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; } }
CartServiceHibernateDaoでもEDHelperを使用してprintXXXXメソッドの書式出力を行っている。単体テストに於いてDBからロードしてきたオブジェクトのチェックをするときに、「EDbHelperを使った書式出力」機能が有効になる。処理もIEPropertiesのprintPropertiesメソッドと類似しているので、EDbutilTemplateからの移行においては流用が可能である。
DBを使ったプログラムのテストは、テストに使用するデータを作成し、 テスト毎にテーブルの値をセットしなくてはならない。
Phillippe Girolami 氏によって開発された DbUnitは、
DbUnitは、http://dbunit.sourceforge.net/からダウンロードされたい。 また、Phillippe Girolami 氏による解説記事が、 DbUnitとAnthillによるテスト環境の制御 にあるので適宜参照されたい。
DbUnitを導入するために、必要なjarファイルは
である。
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>
となる。
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から検索テストケースは、以下のようになる。
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()); } }
「DbUnitを使ったテストケース」の完全ソースを以下からダウンロードされたい。
DbUnitはテスト作業を楽にしてくれるツールではあるが、
このような問題を解決するために、Springではトランザクション処理を使ってテスト終了後に自動的にテスト前の状態に戻してくれるAbstractTransactionalSpringContextTestsを提供し、開発者のDBの管理を容易にしている。
DbUnitもAbstractTransactionalSpringContextTestsもともにTestCaseのサブクラスであるため、AbstractTransactionalSpringContextTestsを使うとDbUnitの機能を使うことができない。
そこで、DbUnit の XML ファイルを DbutilTemplateでも使える ように DbUnitHelper を作成した。DbUnitHelper は、
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)
restoreを使うとtestXXXXXメソッド毎にDBの設定を行うことができる。
先のMemberをDBから検索テストケースを restore を使って 実現すると以下の様になる。
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にインポートし、ビルドパスに追加する。
「AbstractTransactionalSpringContextTestsを使ったテストケース」の完全ソースを以下からダウンロードされたい。
EasyMockは、インタフェースからMockオブジェクトを生成するツールである。
EasyMock を使うことによってインタフェースの実装が完了する前に、 Junit を使ったテストケースを作成し、結合テストを行うことができる。
このことは、仕様変更に伴うテストケースの修正を行う上でも 有効な手法であると考えられる。
EasyMock は、http://www.easymock.org/Downloads.htmlからダウンロードされたい。
EasyMockを導入するために、必要なjarファイルは
である。
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()); }
「EasyMockを使ったテストケース」の完全ソースを以下からダウンロードされたい。
EasyMockは結合テストからインタフェースと実装を分離したことに於いて画期的なツールであると言える。
しかしながら、EasyMock を使ったテストケースと実際のテストケースで 別々のソースになってしまうのが難点である。
Spring の IoC を使ってコンフィグファイルによって EasyMock を使う 場合と、使わない場合を切り替えることにする。
SpringによってEasyMockのMockControlを切り替えるインタフェースとして、IObjectControlを以下のように定義する。
public interface IObjectControl { // MockControl を返す MockControl getControl(Class cls); // Mockオブジェクトを返す Object getObject(); }
IObjectControlを実装し、EasyMockのMockControlを返し、getObjectでモックオブジェクトを返すクラス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(); } }
IObjectControlを実装し、何もしないダミーのMockControlを返し、モックではなく、本物のオブジェクトを返すクラス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) {} ... 同様にすべてのメソッドを作成 }
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を除くすべてのメソッドをダミーで上書きしているため、メソッドの実行には支障はない。
これで、ようやくEasyMockと本物のテストでソースを一本化できるようになった。
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>
とするだけである。
「Mockの切り替え」の完全ソースを以下からダウンロードされたい。
Springの提供するMVC機能について検証してみる。
HTTP リクエストが Spring でどのように処理されるかを [1]の Figure8.1 を参考に説明する。
ユーザが作成するのは、Controller だけである。Spring では用途に合わせて いくつかの Controller を提供している。
Controller の種別 | クラス | 用途 |
---|---|---|
Simple | Controller Interface AbstractController |
単にページを切り替えるような場合 |
Throwaway | Throwawaycontroller | リクエストをコマンドとして処理する場合 |
Mulit-Action | MultiActionContorller | 類似したロジックを処理する場合 |
Command | BasicCommandController AbstractCommandController |
Controllerが1個以上のパラメータを受け取る場合 |
Form | AbstractFormController SimpleFormContorller |
フォームを扱う場合 |
Wizard | AbstractWizardFormController | アンケートのように複数のページを扱う場合 |
節[Member Dao を使った永続性の実現]を使ってメンバーIDからメンバー の情報(氏名、住所)を検索する MemberController を作成してみる。
処理の振り分けをするために action パラメータを使用し、 メンバーの検索には、memberId パラメータにメンバーIDをセット する。
HTTPリクエストのパラメータをオブジェクトの属性にセットするためにMemberController用のコマンドオブジェクトDisplayMemberCommandを実装する。
実装といっても単にコマンドパラメータの型と名称を private にセットし、getter/setter を自動生成するだけである。
public class DisplayMemberCommand { String action; Integer memberId; // 自動生成された getter/setterは省略 }
Springではコマンドの検証メカニズムをValidatorインタフェースによって提供している。
Validator は次のインタフェースを実装する必要がある。
public interface Validator { boolean supports(Class cls); void validate(Object command, Errors errors); }
検出されたエラーは、
DisplayMemberCommand の Validator (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."); } }
最後に、Controllerの実装であるが、きわめて単純に実装した。引数を使用するので、AbstractCommandControllerのサブクラスとした。
IMember を属性 dao に保持し、command から memberId を取得して検索し、 それを ModelAndView にセットして返すだけである。
ModelAndView の定義は次の通りである。
public ModelAndView(String viewName, String modelName, Object modelObject)
MemberConntrollerを以下に示す。
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); } } }
「MemberControllerの作成」の完全ソースを以下からダウンロードされたい。
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);
MemberControllerのテストケースは、以下のようになる。
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(); } } }
「MemberControllerの単体テスト」の完全ソースを以下からダウンロードされたい。
SpringのMVCを使ってWebアプリケーションを作成する前に、Javaアプリケーションを作成してみる。(16)
Controllerの返すModelAndViewには、
がセットされている。
このビューの論理名とJFrameのビューを結びつければ、java アプリケーション版のMVCが実現できる。
HTTP要求とその戻り値であるModelAndView を関連づける メッセージディスパッチャーのインタフェース IMessageDispatcher 以下の様に定義する。
public interface IMessageDispatcher { void doSubmit(String url, String action); void doSubmit(String url, Map args); void addObserver(Observer o); }
MockHttpServletRequestを使ったアプリケーション用のメッセージディスパッチャーを以下に示す。
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)
ビューは、以下のメソッドを実装しなければならない。
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イベントの処理を記述する。
JFrame, JDialogに対するdispatcher, nameのsetterを実装したAbstractDispatchFrame, AbstractDispatchDialogを以下のように定義した。
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); }
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メソッドを定義すればよい。
検索条件を入力する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; }
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のフィールドにセットしているだけである。
エラーメッセージを表示する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(); }
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")
としているだけである。
Springのアプリケーションは、いずれも同じ簡単な構造となる。
public class MVCTestApp { public static void main(String[] args) { BeanFactory factory = new ClassPathXmlApplicationContext("/mvcTest.xml"); JFrame main = (JFrame)factory.getBean("mainFrame"); main.show(); } }
コンフィグファイルは、dispatcherを定義し、各Frameでそれをセットするだけのきわめて簡単な構造となる。
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>
アプリケーションを実行すると、
が表示され、「操作」メニューから「検索」を選択すると、
検索条件設定画面が表示される。
ここで、Member ID に 0 をセットし、検索ボタンを押すと、
が表示される。Member IDにaと数字以外を入力すると、
が表示される。
「アプリケーションへの拡張」の完全ソースを以下からダウンロードされたい。
[1] | Graig Walls and Ryan Greidenbach. Spring in Action. Manning, |
[2] | 長谷川裕一、伊藤清人、岩永寿来、大野渉. Java・J2EE・オープンソース Spring入門 : より良いWebアプリケーションの設計と実装. 技術評論社, |
[3] | 河村嘉之、首藤智大、竹内祐介、吉尾真祐. 実践 Spring Framework : J2EE 開発を変える DI コンテナのすべて. , |