JAX-RS(Jersey) + JPA (on CDI) / on Tomcat

Tomcat上でJAX-RX(Jersey)を使う環境で、CDIからJPAのEntityManagerを取得するようにする。開発環境はeclipseでMarvenプロジェクトを使用。

分かりやすいサンプルがあったので楽勝・・・ではなかった。。。
参考(というかほぼそのまま)
JAX-RS(Jersey)とJPAのサンプルにCDIを使ってTomcatで動かす - Qiita

環境

  • JavaSE 8
  • Tomcat8
  • 他ライブラリversionは記事中

JPA設定

src/main/resourcesに「META-INF」フォルダ追加
META-INFにpersistence.xml作成

<?xml version="1.0" encoding="UTF-8" ?>
<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_2_0.xsd"
        version="2.0" xmlns="http://java.sun.com/xml/ns/persistence">

    <persistence-unit name="testProvider" transaction-type="RESOURCE_LOCAL">
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:(略)" />
            <property name="javax.persistence.jdbc.user" value="(略)" />
            <property name="javax.persistence.jdbc.password" value="(略)" />
            <property name="eclipselink.logging.level" value="ALL" />
        </properties>
    </persistence-unit>
</persistence>

※EclipseLink使用、DBはPostgreSQL、unit名はtestProvider

CDI設定

pom.xmlの依存設定

<dependency>
    <groupId>org.jboss.weld.servlet</groupId>
    <artifactId>weld-servlet</artifactId>
    <version>2.3.1.Final</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.ext.cdi</groupId>
    <artifactId>jersey-cdi1x</artifactId>
</dependency>

CDIコンテナはWeldを使用
追記:たぶん「jersey-cdi1x-servlet」の方がいい。 理由はここ

/src/main/resources/META-INF/にbeans.xml作成

<?xml version="1.0" encoding="UTF-8"?>
<beans/>

EntityManagerのProducerクラス

public class EntityManagerProducer {
    @Produces
    @RequestScoped
    protected EntityManager createEntityManager() {
        EntityManagerFactory factory = Persistence.createEntityManagerFactory("testProvider");
        return factory.createEntityManager();
    }
    protected void closeEntityManager(@Disposes EntityManager entityManager) {
        if (entityManager.isOpen()) {
            entityManager.close();
        }
    }
}

※@PersistenceContextは使えない(後述)

EntityManager使用(@Inject)クラス

@ApplicationScoped
public class UserService {
    @Inject
    private EntityManager entityManager;

    public User getUser(Integer id) {
        User user = this.entityManager.find(User.class, id);
        return user;
    }
}

※@InjectでEntityManagerを注入
※Userは単純な@Entityなクラスなので省略
※UserServiceクラス自体もCDIで管理してInjectしたいので@ApplicationScoped指定

JAX-RSのリソースクラス

@RequestScoped
@Path("myresource")
public class MyResource {
    @Inject
    private UserService userService;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public User getIt() {
        User user = userService.getUser(1);
        return user;
    }
}

CDI管理しているUserServiceを@InjectしてUserを取得
※本当はUser自体ではなくDTOを返す方がいいと思うけど

これでブラウザから該当パスにアクセスすればUserのjsonが表示される。

半日ハマる

最初はEntityManagerの注入どころか、JPAを使わないUserServiceクラスの注入さえ
javax.ws.rs.WebApplicationException: Trying to register multiple service locators into single service locator application.
というエラーが出てできなかった。調べても解決法が見つからないので、weldのドキュメントでJNDIの設定をしてみたり、beans.xmlを書き換えてみたり、etc...と色々試したが一向に解決せず。

結論としては、pom.xmlに追加したjersey-cdi1xの設定がおかしかっただけ。。。
eclipseのプロジェクトは半年前に作っていたもので、その時のjerseyのバージョンは2.17。 にもかかわらず、jersey-cdi1xのversionタグに最新(この時は2.22.1)を手動設定してしまっていたので、整合性が取れていなかった様子。
pomの依存関係あたりを適当にいじっていたら急に動いたので、逆に何故動いたのか一瞬分からなかった。
要はversionタグを消してmanagedにしてやればよかった。

念のためプロジェクトを作りなおしてJerseyの最新版(2.22.1)で揃えてちゃんと動くことを確認。
Maven使うのも初めてとは言え、何のためのMavenなんだか。。。