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にデプロイする。