2009年4月9日木曜日

GAE/Javaでt2-frameworkを使ってみる

Wicketを試す合間にT2(with Guice+JPA)を試していたのでその手順を。

開発環境の準備

Eclipseを使用している事と、EclipseにGAE用のプラグインをインストールしてある事。

プロジェクトを作ってT2用に設定する

Eclipse上でプロジェクトを作る
新規作成Wizardで[Goolge/Web Application Project]を選択する。なお、JDKは1.6に変更しておく事!
T2関連のモジュールをコピーする
t2-frameworkのサイトから「Core/T2-${version}-ga」と「Core/Lucy-${version}-ga」をダウンロードする。また、今回はついでにGuiceも使いたいので「Samples/T2 + Guice + JPA sample application」もダウンロードしておく。以下をダウンロードしたモジュールから抜き出して${baseDir}/war/WEB-INF/libにコピーして、[Build path][Add to Build Path]しておく。他にはcommons-langも適当に拾って来て同様に配置、ビルドパスに含めておこう。
  • commons-0.5.1-ga.jar
  • guice-1.0.jar
  • guice-servlet-1.0.jar
  • guiceadapter-0.5.1-ga.jar
  • logback-classic-0.9.15.jar
  • logback-core-0.9.15.jar
  • slf4j-api-1.5.6.jar
  • t2-0.5.1-ga.jar

各種設定ファイルの準備をする

web.xmlを修正する
対象ファイルはwar/WEB-INF/web.xml。
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
  <context-param>
    <param-name>t2.encoding</param-name>
    <param-value>UTF-8</param-value>
  </context-param>

  <filter>
    <filter-name>Guice Servlet Filter</filter-name>
    <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
  </filter>
  <filter>
    <filter-name>t2</filter-name>
    <filter-class>org.t2framework.t2.filter.T2Filter</filter-class>
    <init-param>
      <param-name>t2.rootpackage</param-name>
      <param-value>com.shin1ogawa.page</param-value>
    </init-param>
    <init-param>
      <param-name>t2.container.adapter</param-name>
      <param-value>org.t2framework.t2.adapter.GuiceAdapter</param-value>
    </init-param>
    <!--
    <init-param>
      <param-name>t2.eagerload</param-name>
      <param-value>true</param-value>
    </init-param>
    -->
    <init-param>
      <param-name>t2.exclude-resources</param-name>
      <param-value>css, js</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>Guice Servlet Filter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>  
  <filter-mapping>
    <filter-name>t2</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>
log4.propertiesを修正する
src/log4j.propertiesに以下を追記しておく。
log4j.category.DataNucleus.Connection=DEBUG, A1
log4j.category.DataNucleus.Query=DEBUG, A1
log4j.logger.org.t2framework=TRACE, A1
log4j.logger.com.shin1ogawa=TRACE, A1
最後の行は自分のアプリに合わせて適宜変更する事。
セッションを有効にする
GAEの設定ファイルである、war/WEB-INF/appengine-web.xml内のappengine-web-app要素の直下に以下を追加する。
<sessions-enabled>true</sessions-enabled>
persistence.xmlを作成する。

src/META-INFにpersistence.xmlを作成する。内容は以下のとおりで、これはGAE/JavaでJPAを使う時に必要なもの。JDOを使わずJPAを使うのであればGAEプラグインがデフォルトでsrc/META-INF直下に作成するjdoconfig.xmlは削除してもかまわない、と思う。

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

  <persistence-unit name="transactions-optional">
    <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
    <class>com.shin1ogawa.entity.Messages</class>
    <properties>
      <property name="datanucleus.NontransactionalRead" value="true"/>
      <property name="datanucleus.NontransactionalWrite" value="true"/>
      <property name="datanucleus.ConnectionURL" value="appengine"/>
    </properties>
  </persistence-unit>
</persistence>
class要素に記述するのはJPAのEntityクラスなので、自分のアプリに合わせて適宜修正する事。
T2に読ませるServiceProviderの設定ファイルを作成する
src/META-INFにservicesフォルダを作成し、com.google.inject.Moduleというファイルを作成する。内容は以下の一行で、自分のアプリに合わせて適宜修正する事。
com.shin1ogawa.ApplicationModule

Javaのクラスを作成する

JPA用のModule
package com.shin1ogawa;
import javax.persistence.*;
import com.google.inject.*;
public class JpaModule extends AbstractModule {
  @Override protected void configure() {
    bind(EntityManager.class).toProvider(new Provider() {
      public EntityManager get() {
        EntityManager manager = Persistence.createEntityManagerFactory(
            "transactions-optional").createEntityManager();
        manager.setFlushMode(FlushModeType.COMMIT);
        return manager;
      }
    }).in(Singleton.class);
  }
}
JPA用のEntity
今回は超単純なEntityをひとつだけ用意してみる。persistence.xmlのclass要素でJPA的に使えるように登録する必要がある(上記手順で設定済み)。
package com.shin1ogawa.entity;
import java.util.Date;
import javax.persistence.*;
@Entity
public class Messages {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
  private String userName;
  private String message;
  private Date updated;
  public Long getId() { return id; }
  public void setId(Long id) { this.id = id; }
  public String getUserName() { return userName; }
  public void setUserName(String userName) { this.userName = userName; }
  public String getMessage() { return message; }
  public void setMessage(String message) { this.message = message; }
  public void setUpdated(Date updated) { this.updated = updated; }
  public Date getUpdated() { return updated; }
}
処理モジュールを作成する
Guiceを使って管理する対象にしたいので、インターフェースと実装クラスを分離している。
package com.shin1ogawa.logic;

import java.util.List;
import com.google.appengine.api.users.User;
import com.shin1ogawa.entity.Messages;

public interface IMessagesLogic {
  public Messages newMessage(User user, String messageBody);
  public List<Messages> selectAll();
}
package com.shin1ogawa.logic;

import java.util.*;
import javax.persistence.*;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.slf4j.*;
import com.google.appengine.api.users.User;
import com.google.inject.Inject;
import com.shin1ogawa.entity.Messages;

public class MessagesLogic implements IMessagesLogic {
  static final Logger logger = LoggerFactory.getLogger(MessagesLogic.class);

  @Inject EntityManager em;

  public Messages newMessage(User user, String messageBody) {
    Messages message = new Messages();
    message.setUserName(user != null ? user.getNickname() : "unknown");
    message.setMessage(messageBody);
    message.setUpdated(new Date(System.currentTimeMillis()));

    EntityTransaction transaction = em.getTransaction();
    transaction.begin();
    em.persist(message);
    transaction.commit();
    return message;
  }

  public List<Messages> selectAll() {
    @SuppressWarnings("unchecked")
    List resultList = em.createQuery(
        "Select m from com.shin1ogawa.entity.Messages m").getResultList();
    return resultList;
  }

  @Override public String toString() {
    return ToStringBuilder.reflectionToString(this);
  }
}
実装クラス中のJPQLでクラス名をFQCNで指定ている。"Select m from Messages m"とか短いクラス名で記述したいのだけど、そうするとORMの実装であるDataNucleusがクラス名を解決できないというエラーを出してくる。oem.xmlを作ってやってもダメ。なのでとりあえずFQCNで記述しておいた。誰か解決方法がわかる人は教えてください!
T2のアプリケーション用のModule
package com.shin1ogawa;
import org.slf4j.*;
import com.google.inject.*;
import com.shin1ogawa.logic.IMessagesLogic;
import com.shin1ogawa.logic.MessagesLogic;
public class ApplicationModule extends AbstractModule {
  @Override protected void configure() {
    install(new ServletModule());
    install(new JpaModule());
    bind(IMessagesLogic.class).to(MessagesLogic.class).in(Scopes.SINGLETON);
  }
}
T2のページクラス
ちなみに、jspは好きではないのでjspは使ってない。データの投稿もGetで行う事が前提で書いた。データの一覧表示用とデータの投稿用のページクラス。
package com.shin1ogawa.page;

import java.util.*;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.t2framework.t2.annotation.core.Default;
import org.t2framework.t2.annotation.core.Page;
import org.t2framework.t2.contexts.WebContext;
import org.t2framework.t2.navigation.SimpleText;
import org.t2framework.t2.spi.Navigation;

import com.google.inject.Inject;
import com.google.inject.servlet.RequestParameters;
import com.google.inject.servlet.RequestScoped;
import com.shin1ogawa.entity.Messages;
import com.shin1ogawa.logic.IMessagesLogic;

@RequestScoped
@Page("messageList")
public class MessageList {
  @Inject @RequestParameters Map params;

  @Inject IMessagesLogic logic;
  
  @Default public Navigation index(WebContext context) {
    StringBuilder b = new StringBuilder();
    b.append(ToStringBuilder.reflectionToString(this));
    
    List<Messages> messageList = logic.selectAll();
    if (messageList != null) {
      b.append("messages count=").append(messageList.size()).append("\n");
      for (Messages messages : messageList) {
        b.append(ToStringBuilder.reflectionToString(messages)).append("\n");
      }
    } else {
      b.append("messages count=0").append("\n");
    }
    return new SimpleText(b.toString());
  }
}
package com.shin1ogawa.page;

import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.t2framework.t2.annotation.core.Default;
import org.t2framework.t2.annotation.core.Page;
import org.t2framework.t2.contexts.WebContext;
import org.t2framework.t2.navigation.Redirect;
import org.t2framework.t2.navigation.SimpleText;
import org.t2framework.t2.spi.Navigation;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.inject.Inject;
import com.google.inject.servlet.RequestParameters;
import com.google.inject.servlet.RequestScoped;
import com.shin1ogawa.logic.IMessagesLogic;

@RequestScoped
@Page("addMessage")
public class AddMessage {
  @Inject @RequestParameters Map params;

  @Inject IMessagesLogic logic;

  @Default public Navigation index(WebContext context) {
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();

    final String message = params.get("message") != null ? params.get("message")[0] : null;
    if (StringUtils.isEmpty(message)) {
      return new SimpleText("メッセージが入力されていません。");
    } else {
      logic.newMessage(user, message);
      return new Redirect(MessageList.class);
    }
  }
}
表示がめっちゃくちゃ適当だけど、自分はWebのUIをメインに使う気がない(だからこそT2っていう選択肢でもある)のでこんなもんで十分なのだ。

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

GAEプラグインでプロジェクトを作った場合は、プロジェクトと同時にWeb Applicationの実行の構成も作られているのでそれを実行し、「http://localhost:8080/messageList」を開けばおk!最初は空っぽだが、「http://localhost:8080/addMessage?message=hoge」とかやるとデータが増えていくはずだ。データはアプリケーションを終了しても消えない模様。

コメントを投稿