Struts1.3+Springframework2.5でGAE/Jのデータ・ストアを使ってみた
前回のログでは、Struts1.3の簡単なサンプルを作って、Google Apps Engine/Java(GAE/J)上でデータ・ストアを使ってみた。
それを少し発展させて、同じ仕様で、Struts1.3とSpringframework2.5を連携させたサンプルをGAE/Jに乗せてみた。
初期画面は以下。ここで、Personエンティティーのプロパティーを入力する。
登録をすると以下のように、Personエンティティーがリスト表示される。このように、サンプルの「見た目」は、前回と全く変わらない。
MVC2モデルに準拠するアプリを作る際に、StrutsとSpringを連携させるか、SpringMVCでやるか、は微妙なところ。Strutsがこの分野のキラーアプリだった時期が長いので、相当の資産(レガシーシステムという意味と、ノウハウという意味)があるのは間違いなく、Strutsでフローを作って、Springでインジェクト(ウィービング)する、というパターンをとることも多いだろうと思う。
StrutsとSpringframeworkを連携させるには、自分が初めてSpringを触った頃は、
- ActionSupportを継承する
- DelegatingActionProxyを使う
の2通りしかなかったが、今は以下の2つを加えて4種類ある(参考;ITプロ「第10回 Spring&Struts連携のベスト・プラクティスはこれだ!」)。
- DelegatingRequestProcessorを使う
- AutowiringRequestProcessorを使う
今回のサンプルは、AutowiringRequestProcessorを使って実装する。
サンプルが簡単すぎるので、GAE/Jのデータ・ストアへのDAOをActionクラスにインジェクトする、という「妙な構造」をとる。thread safeの問題もあってよくないが、GAE/J+データストアでの稼動テストということ簡単に済ませる。
以下の作業は、GAE/JのプラグインのささったEclipse3.4(Ganymede)で行った。
プロジェクトの作成
前回の作成した「Struts1.3からGAE/Jのデータストアを使う」サンプルのプロジェクトをコピーする。
struts-blank.warをもとにしているので、war/WEB-INF/libにspring-framework-2.5.6-with-dependencies.zipに含まれるjar(以下)をコピーした。
- aopalliance.jar
- aspectjrt.jar
- aspectjweaver.jar
- cglib-nodep-2.1_3.jar
- spring-2.5.6.jar
- spring-aspects.jar
- spring-webmvc-struts.jar
Strutsと連携するためには、最後のspring-webmvc-struts.jarが必要となるので注意。
必要に応じて、App Engine Settingsでヴァージョンなど変更する。
エンティティーの作成
前回作成したPersonエンティティーからインターフェイスを抽出し、それをimplementsする格好にしてみたが、ローカルサーバーでenhancementのエラーがでてしまった。GAE/Jのエラーログやデプロイされるクラスなどは、Documents and Settings/ユーザー/Local Settings/Temp下に入る。
少しいじってみた結果、以下のスーパークラスからextendsする格好だとうまくいった。
Entity.java
package test.entities; import com.google.appengine.api.datastore.Key; /** * Entityに必要最低限を定義。 * extendsするクラスで拡張することを前提。 * */ public abstract class Entity { abstract public Key getKey(); abstract public void setKey(Key key); abstract public Long getNumber(); abstract public void setNumber(Long number); abstract public int getIntNumber(); }
これに応じて、Personエンティティーを以下のように修正(注記;ここでのポイントはこの設計事態の良否ではなく、これでもGAE/Jは大丈夫という意味)。
Person.java
package test.entities; import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.IdentityType; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; import com.google.appengine.api.datastore.Key; /** * Person固有のクラス * */ @PersistenceCapable(identityType=IdentityType.APPLICATION) public class Person extends Entity{ @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Key key; @Persistent private Long number; @Persistent private String name; @Persistent private String address; @Persistent private float salary; /** * クラスPersonのオブジェクトを構築します。 * */ public Person() { } public Person(String name, String address, float salary) { this.name = name; this.address = address; this.salary = salary; } /** * Setter/Getter */ public Key getKey() { return key; } public void setKey(Key key) { this.key = key; } public Long getNumber() { return number; } public void setNumber(Long number) { this.number = number; } public int getIntNumber() { return number.intValue(); } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public float getSalary() { return salary; } public void setSalary(float salary) { this.salary = salary; } /** * プロパティーが同じならtrue */ public boolean equals(Person person){ if(this.name.equals(person.getName()) && this.address.equals(person.getAddress()) && this.salary == person.getSalary()) return true; return false; } /** * プロパティーを文字列に連結して返す */ public String toString(){ return this.key.toString() + this.name + this.address+ Float.toString(this.salary); } }
Personエンティティーの属するエンティティー・グループを規定するRootTO.javaは、前回のサンプルのときのままとした。
PersonDaoクラスの変更
このクラスを、AutowiringでStrutsのアクションクラスにインジェクションする。
体裁を整えるために、前回のサンプルからインターフェイスを抜き出し、それをPersonDao、この具象クラスをPersonDaoImplとした。
PersonDao.java
package test; import java.util.List; import test.entities.Person; public interface PersonDao { /* * Personのリストを返却する。 */ public abstract List<Person> getAll(); /* * Personを登録、更新する。 */ public abstract List<Person> setEntity(Person entity); /** * Personを削除する。 */ public abstract List<Person> deleteEntity(Person entity); /** * Personを一括削除する。 */ public abstract List<Person> deleteAll(); }
PersonDaoImpl.java;(注記)メンバー変数helloMessageを追加したのは、(対Actionクラスだけではなくて)このbeanへのインジェクションをテストするためで、機能的な意味はない。
package test; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.jdo.PersistenceManager; import javax.jdo.Query; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import test.entities.Person; import test.entities.RootTO; /** * PersonのData Access Object * */ public class PersonDaoImpl implements PersonDao{ private static final Log log = LogFactory.getLog(PersonDaoImpl.class); private Map<String,String> helloMessages; /** * コンストラクタ */ public PersonDaoImpl() { } @SuppressWarnings("unchecked") private List<Person> loadAll(){ log.debug("test: " + helloMessages.get("msg1")); List<Person> people= new ArrayList<Person>(); PersistenceManager pm = PMF.get().getPersistenceManager(); /** * Personエンティティーに対するクエリの実行 */ Query query = pm.newQuery(Person.class); query.setOrdering("number asc"); try{ List<Person> list = (List<Person>)query.execute(); for(Person entity : list){ people.add(entity); } }finally{ query.closeAll(); pm.close(); } return people; } /* * Personのリストを返却する。 */ public List<Person> getAll() { return this.loadAll(); } /* * Personを登録、更新する。 */ public List<Person> setEntity(Person entity) { if(entity.getNumber().intValue() != -1){ this.updateEntity(entity); }else{ this.registerEntity(entity); } return this.loadAll(); } /* * Personを登録する。 */ private void registerEntity(Person entity) { PersistenceManager pm = PMF.get().getPersistenceManager(); long count = 0; try{ pm.currentTransaction().begin(); RootTO root = pm.getObjectById(RootTO.class, "key"); count = root.getCount()+1; root.setCount(count); pm.currentTransaction().commit(); }finally{ if(pm.currentTransaction().isActive()){ // ロールバック pm.currentTransaction().rollback(); } pm.close(); } if(count > 0){ Person storePer = new Person(entity.getName(), entity.getAddress(), entity.getSalary()); KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "key"); kb.addChild(Person.class.getSimpleName(), Person.class.getSimpleName()+ new Long(count).toString()); Key key = kb.getKey(); storePer.setKey(key); storePer.setNumber(count); pm = PMF.get().getPersistenceManager(); try{ pm.makePersistent(storePer); }finally{ pm.close(); } } return; } /* * Personを更新する。 */ private void updateEntity(Person entity) { PersistenceManager pm = PMF.get().getPersistenceManager(); try{ // キーの生成 KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "key"); kb.addChild(Person.class.getSimpleName(), Person.class.getSimpleName()+entity.getNumber().toString()); Key key = kb.getKey(); // オブジェクトの取得 Person storePer = pm.getObjectById(Person.class, key); storePer.setName(entity.getName()); storePer.setAddress(entity.getAddress()); storePer.setSalary(entity.getSalary()); }finally{ pm.close(); } return; } /* (非 Javadoc) * @see test.EntitiyDao#deleteEntity(test.entities.Person) */ public List<Person> deleteEntity(Person entity) { PersistenceManager pm = PMF.get().getPersistenceManager(); try{ // キーの生成 KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "key"); kb.addChild(Person.class.getSimpleName(), Person.class.getSimpleName()+entity.getNumber().toString()); Key key = kb.getKey(); // オブジェクトの取得 Person storePer = pm.getObjectById(Person.class, key); pm.deletePersistent(storePer); }finally{ pm.close(); } return this.loadAll(); } /* (非 Javadoc) * @see test.EntitiyDao#deleteAll() */ @SuppressWarnings("unchecked") public List<Person> deleteAll() { PersistenceManager pm = PMF.get().getPersistenceManager(); Query query = pm.newQuery(Person.class); try{ pm.currentTransaction().begin(); List<Person> list = (List<Person>)query.execute(); pm.deletePersistentAll(list); pm.currentTransaction().commit(); }finally{ if(pm.currentTransaction().isActive()){ // ロールバック pm.currentTransaction().rollback(); } query.closeAll(); pm.close(); } return this.loadAll(); } /** * Setter * @param helloMessages */ public void setHelloMessages(Map<String,String> helloMessages) { this.helloMessages = helloMessages; } }
Actionクラスの変更
前回作成した、PersonEntry.java、PersonList.javaという2つのアクションクラスには、これらにPersonDaoをインジェクション(具体的にはセッター・インジェクション)を変更するための変更をした。
PersonEntry.java
package test; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.struts.action.Action; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import test.entities.Person; public class PersonEntryAction extends Action { /** * Spiringでインジェクション */ private PersonDao pdao; public void setPdao(PersonDao pdao) { this.pdao = pdao; } public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { PersonEntryForm pef = (PersonEntryForm) form; Person person = new Person(); // -1が入ってくる。 person.setNumber(new Long(pef.getNumber())); person.setName(pef.getName()); person.setSalary(new Float(pef.getSalary()).floatValue()); person.setAddress(pef.getAddress()); // 登録 List<Person> personList = pdao.setEntity(person); // requestにセット request.setAttribute("personList", personList); return (mapping.findForward("success")); } }
PersonList.java
package test; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.struts.action.Action; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import test.entities.Person; public class PersonListAction extends Action { /** * Spiringでインジェクション */ private PersonDao pdao; public void setPdao(PersonDao pdao) { this.pdao = pdao; } public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) { List<Person> personList = pdao.getAll(); // requestにセット request.setAttribute("personList", personList); return (mapping.findForward("success")); } }
その他
これ以外に、以下のクラスを用いたが、これらは以前作成したときと同じ
CreateRootContextListner.java | RootTOエンティティー、Personエンティティーの初期設定をおこなう。GAE/Jのコンテキスト・リスナーとして起動する |
PMF.java | PersistenceManagerを生成する |
MethodLogMarker.java | SpringAOPを利用して、メソッドの開始と終了をロギングする |
以下のJSPについても変更なし。
personEntry.jsp | Personエンティティーを登録する画面 |
personList.jsp | Personエンティティーのリストを表示する |
Bean定義ファイル
war/WEB-INF下に、以下のBean定義ファイルを置いた。AutowiringをByNameでかけるので、ここで定義するPersonDaoImpleのbean nameは、(インジェクトする先の)Actionクラスのプロパティーと同名(pdao)にする。
beans.xml
<?xml version="1.0" encoding="utf-8"?> <beans default-lazy-init="true" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd"> <!--// Action --> <bean id="personListAction" class="test.PersonListAction"> </bean> <bean id="personEntryAction" class="test.PersonEntryAction"> </bean> <!--// Bean --> <!-- AutoWiringで差し込むBean;ByNameなので名前をあわせる --> <bean id="pdao" class="test.PersonDaoImpl"> <property name="helloMessages"> <ref bean="messages"/> </property> </bean> <!-- 普通のSetterインジェクションで差し込むBean --> <util:map id="messages" map-class="java.util.HashMap"> <entry key="msg1" value="インジェクションのテストだよ! " /> </util:map> <!-- AOPでメソッドのStartとEndをロギングする--> <bean id="methodLog" class="test.MethodLogMarker" /> <aop:config> <!-- Point Cutは、test.PersonDaoImplクラスの任意のメソッドに設定--> <aop:pointcut id="methodLogMarker" expression="execution(* test.PersonDaoImpl.*(..))" /> <aop:advisor pointcut-ref="methodLogMarker" advice-ref="methodLog" /> </aop:config> </beans>
struts-config.xml
コントローラー定義に、org.springframework.web.struts.AutowiringRequestProcessorを使う旨の設定を追加する。具体的には、以下のようにオプションも設定してみた。
<!-- =================================================== Controller --> <controller nocache="true" locale="true" inputForward="false" maxFileSize="1M" processorClass="org.springframework.web.struts.AutowiringRequestProcessor" />
web.xml
Springを起動するための設定と、ActionクラスにたいしてSpringのAutowiringを使う胸の設定を追加する。以下は全体。
<?xml version="1.0" encoding="utf-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <display-name>GAE/J Sample Application</display-name> <context-param> <description> Specify the beans.xml location. When you specify more than one, be separated by a camma. </description> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/beans.xml </param-value> </context-param> <listener> <description> Spring Bootstrap. </description> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <listener> <listener-class> org.springframework.web.context.request.RequestContextListener </listener-class> </listener> <listener> <listener-class> test.CreateRootContextListner </listener-class> </listener> <!-- Standard Action Servlet Configuration --> <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <init-param> <param-name>spring.autowire</param-name> <param-value>byName</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- Standard Action Servlet Mapping --> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> <!-- The Usual Welcome File List --> <welcome-file-list> <welcome-file>personEntry.jsp</welcome-file> </welcome-file-list> </web-app>
テストとデプロイ
ローカルサーバーでテストを行った後、GAE/Jにデプロイする。