Jersey + Hibernate + Guice による JavaEE 6 アプリケーション作成 - JAX-RS (JSR-311), JPA (JSR-317), DI (JSR-330)

Jersey, Hibernate, Guice を組み合わせて、JavaEE 6 を使ったアプリケーション(JAX-RS, JPA, DI)の簡単なサンプルを作成してみました。

環境は以下の通り。

ソースは http://github.com/fits/try_samples/tree/master/blog/20110821/

各種設定ファイルの作成

今回は Maven を使ってビルドするため、pom.xml ファイルを作成します。

Jersey や Hibernate EntityManager の基本的な依存関係に、GuiceJPA を使うのための guice-persist や Jersey で Guice を使うための jersey-guice を追加します。(その他にも Jersey で JSON を使うための jersey-json や MySQLJDBC ドライバーの設定あり)

なお、mvn jetty:run で動作確認するため、maven-jetty-plugin の設定も行っています。

pom.xml
<project ・・・>
  <modelVersion>4.0.0</modelVersion>
  <groupId>fits.sample</groupId>
  <artifactId>sample</artifactId>
  <packaging>war</packaging>
  <version>1.0</version>
  <name>Jersey + Hibernate + Guice Sample</name>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <jersey.version>1.8</jersey.version>
    <hibernate.version>3.6.6.Final</hibernate.version>
  </properties>
  <repositories>
    <repository>
      <id>maven2-repository.java.net</id>
      <name>Java.net Repository for Maven</name>
      <url>http://download.java.net/maven/2/</url>
      <layout>default</layout>
    </repository>
    <repository>
      <id>Hibernate</id>
      <name>Hibernate Repository for Maven</name>
      <url>http://repository.jboss.org/nexus/content/groups/public-jboss/</url>
    </repository>
  </repositories>
  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
      <scope>provided</scope>
    </dependency>
    <!-- Guice の JPA 用 -->
    <dependency>
      <groupId>com.google.inject.extensions</groupId>
      <artifactId>guice-persist</artifactId>
      <version>3.0</version>
    </dependency>
    <!-- Jersey -->
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-server</artifactId>
      <version>${jersey.version}</version>
    </dependency>
    <!-- Jersey の JSON 用 -->
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-json</artifactId>
      <version>${jersey.version}</version>
    </dependency>
    <!-- Jersey の Guice 用 -->
    <dependency>
      <groupId>com.sun.jersey.contribs</groupId>
      <artifactId>jersey-guice</artifactId>
      <version>${jersey.version}</version>
    </dependency>
    <!-- Hibernate -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>${hibernate.version}</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>${hibernate.version}</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.17</version>
    </dependency>
  </dependencies>
  <build>
    <finalName>sample</finalName>
    <plugins>
      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>maven-jetty-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

次に JPA の設定ファイルを作成します。(Hibernate を使ってローカルの MySQL に接続)
トランザクションJTA を使わない RESOURCE_LOCAL を指定しています。

src/main/resources/META-INF/persistence.xml
<persistence ・・・>
  <persistence-unit name="sample" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" />
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost/sample" />
      <property name="javax.persistence.jdbc.user" value="root" />
      <property name="javax.persistence.jdbc.password" value="" />
      <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" />
    </properties>
  </persistence-unit>
</persistence>

最後に web.xml ファイルを作成します。
GuiceFilter を使って SampleGuiceConfig クラスに記載した DI の構成内容が Web サーバー処理(今回は JAX-RS)に適用されるように設定しています。

通常の Jersey では web.xml に Jersey 用のサーブレット設定等を行いますが、Guice と組み合わせる場合は web.xmlGuice の設定を行って構成クラスで Jersey の設定を行う点が異なります。

src/main/webapp/WEB-INF/web.xml
<web-app ・・・>
  <listener>
    <listener-class>fits.sample.SampleGuiceConfig</listener-class>
  </listener>
  <filter>
    <filter-name>GuiceFilter</filter-name>
    <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>GuiceFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

JPA を使ったモデルクラス、DAO クラスの作成

JPA 用のアノテーションを使ってモデルクラスを作成します。
JAX-RS で JSON に変換してレスポンスを返せるように JAXB 用の @XmlRootElement も指定しています。

src/main/java/fits/sample/model/Task.java
package fits.sample.model;

import java.util.Date;
import javax.persistence.*;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
@Entity
@Table(name = "tasks")
public class Task {
    @Id
    @GeneratedValue
    @Column(name = "task_id")
    private long taskId;

    private String title;

    @Column(name = "created_date")
    private Date createdDate;

    ・・・各種 getter, setter メソッド定義 ・・・
}

DAO インターフェースとその実装クラスを以下のように作成しました。
実装クラスでは @Inject で JPA の EntityManager を DI するよう指定しています。
また、@Transactional アノテーションでメソッド単位でのトランザクション指定を行っていますが、ここだけ Guice に依存してしまうので改善の余地ありです。

src/main/java/fits/sample/dao/TaskDao.java
package fits.sample.dao;

import java.util.List;
import fits.sample.model.Task;

public interface TaskDao {
    long addTask(String title);
    List<Task> getTaskList();
}
src/main/java/fits/sample/dao/impl/TaskDaoImpl.java
package fits.sample.dao.impl;

import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaQuery;
import com.google.inject.persist.Transactional;
import fits.sample.dao.TaskDao;
import fits.sample.model.Task;

public class TaskDaoImpl implements TaskDao {
    @Inject
    private EntityManager em;

    @Transactional
    public long addTask(String title) {
        Task t = this.createTask(title);
        em.persist(t);
        return t.getTaskId();
    }

    public List<Task> getTaskList() {
        CriteriaQuery<Task> q = em.getCriteriaBuilder().createQuery(Task.class);
        return em.createQuery(q.select(q.from(Task.class))).getResultList();
    }

    private Task createTask(String title) {
        Task t = new Task();
        t.setTitle(title);
        t.setCreatedDate(new Date());
        return t;
    }
}

検索処理に JPA の CriteriaQuery を使ってみましたが、冗長な記述になるのでちょっとイマイチな印象です。

JAX-RS リソースクラスの作成

JAX-RS用のリソースクラスでは @Inject を使って TaskDao を DI するよう指定しています。
なお、処理結果を JSON で返すように @Produces("application/json") を指定しています。

src/main/java/fits/sample/ws/ToDoResource.java
package fits.sample.ws;

import java.util.List;
import javax.inject.Inject;
import javax.ws.rs.*;
import fits.sample.model.Task;
import fits.sample.dao.TaskDao;

@Path("/")
public class ToDoResource {
    @Inject
    private TaskDao dao;

    @GET
    @Path("task")
    @Produces("application/json")
    public List<Task> getTaskList() {
        return dao.getTaskList();
    }

    @POST
    @Path("task")
    @Produces("application/json")
    public String addTask(@FormParam("title") String title) {
        long taskId = dao.addTask(title);
        return String.valueOf(taskId);
    }
}

DI 構成クラスの作成

最後に、Guice 用の DI 構成を設定するクラスを作成します。
JerseyServletModule の configureServlets メソッドで以下のように Jersey と JPA の構成を指定します。

  • Jersey の構成を適用するため、serve("/*").with(GuiceContainer.class, params) 呼び出し
  • JPA の構成を適用するため、install(new JpaPersistModule("persistence.xmlのユニット名")) 呼び出し
  • JPA の適用を開始するため、filter("/*").through(PersistFilter.class) 呼び出し

なお、GuiceJPA の適用を開始するには PersistService の start メソッドを明示的に呼び出す必要があるようですが、サーブレットの場合は PersistFilter を through すれば PersistService が start される事になります。

src/main/java/fits/sample/SampleGuiceConfig.java
package fits.sample;

import java.util.HashMap;
import java.util.Map; 
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceServletContextListener;
import com.google.inject.persist.jpa.JpaPersistModule;
import com.google.inject.persist.PersistFilter;
import com.sun.jersey.api.core.PackagesResourceConfig;
import com.sun.jersey.guice.spi.container.servlet.GuiceContainer;
import com.sun.jersey.guice.JerseyServletModule;
import fits.sample.dao.TaskDao;
import fits.sample.dao.impl.TaskDaoImpl;

public class SampleGuiceConfig extends GuiceServletContextListener {
    @Override
    protected Injector getInjector() {
        return Guice.createInjector(
            //Jersey の構成を設定
            new JerseyServletModule() {
                @Override
                protected void configureServlets() {
                    //JPA の設定
                    install(new JpaPersistModule("sample"));

                    //DAO の DI 設定
                    bind(TaskDao.class).to(TaskDaoImpl.class);

                    //Jersey の設定
                    Map<String, String> params = new HashMap();
                    params.put(PackagesResourceConfig.PROPERTY_PACKAGES, "fits.sample.ws");
                    serve("/*").with(GuiceContainer.class, params);

                    //PersistService を start させるための設定
                    filter("/*").through(PersistFilter.class);
                }
            }
        );
    }
}

動作確認

以下のように mvn jetty:run で実行して動作確認を行います。(jetty:run を使った場合、コンテキストパスが artifactId の値になる点に注意)

jetty 上で JAX-RS アプリ実行
> mvn jetty:run
・・・
情報: Initiating Jersey application, version 'Jersey: 1.8 06/24/2011 12:17 PM'
8 21, 2011 1:00:57 午後 com.sun.jersey.guice.spi.container.GuiceComponentProviderFactory getComponentProvider
情報: Binding fits.sample.ws.ToDoResource to GuiceInjectedComponentProvider
2011-08-21 13:00:57.520:INFO::Started SelectChannelConnector@0.0.0.0:8080
[INFO] Started Jetty Server

今回は以下のような Ruby スクリプトを使って動作確認してみました。

タスク追加用 task_post.rb
require "net/http"
res = Net::HTTP.post_form(URI.parse("http://localhost:8080/sample/task"), "title" => ARGV[0])
puts res.body
タスク追加例
> jruby task_post.rb test2
2
タスク取得用 task_get.rb
require "net/http"
puts Net::HTTP.get(URI.parse("http://localhost:8080/sample/task"))
タスク取得例
> jruby task_get.rb
{"task":[{"createdDate":"2011-08-21T13:15:35+09:00","taskId":"1","title":"test1"},{"createdDate":"2011-08-21T13:18:53+09:00","taskId":"2","title":"test2"}]}