Posted at

JAX-RS(Jersey)とJPAのサンプルにCDIを使ってTomcatで動かす

More than 3 years have passed since last update.


はじめに

こちら(JAX-RS(Jersey)とJPAで簡単なサンプル)でJerseyとEclipseLinkを使ってDBを使った簡単なWebアプリケーションというかWeb APIを作成してみました。

今回はその続きです。

ただ、このままではtestabilityが悪かったりEntityManagerインスタンスのライフサイクル管理が面倒だったりするので、そこを任せるためにDIを使おうと思います。

ちょうどJersey2.15からJava EE環境以外でもCDIを利用できるようになったので、そいつを使います。

https://jersey.java.net/release-notes/2.15.html

ソースはこちら。"eclipselink-cdi"タグが対象のコードです。

https://github.com/kamegu/jersey-jpa/tree/eclipselink-cdi


CDIを使う


設定

まずはpom.xmlにweld-servletとjersey-cdi1xを追加します。

    <dependency>

<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>2.15</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-grizzly2-http</artifactId>
<version>2.15</version>
</dependency>
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet</artifactId>
<version>2.2.8.Final</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.ext.cdi</groupId>
<artifactId>jersey-cdi1x</artifactId>
<version>2.15</version>
</dependency>

それから/src/main/resources/META-INF/にbeans.xmlを置きます。中身は空でいいです。

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

<beans/>

設定はこれだけです。

ちなみにbeans.xmlを置いておかないと次のようなエラーがでました。

エラー

javax.servlet.ServletException: A MultiException has 3 exceptions. They are:
1. org.glassfish.hk2.api.UnsatisfiedDependencyException: There was no object available for injection at SystemInjecteeImpl(requiredType=CustomerService,parent=IndexResource,qualifiers={},position=-1,optional=false,self=false,unqualified=null,1605119007)
2. java.lang.IllegalArgumentException: While attempting to resolve the dependencies of com.github.kamegu.jerseyjpa.web.resources.app.IndexResource errors were found
3. java.lang.IllegalStateException: Unable to perform operation: resolve on com.github.kamegu.jerseyjpa.web.resources.app.IndexResource
以下略


DIを試す

リソースクラスから参照するビジネスロジック用のクラスを作成します

package com.github.kamegu.jerseyjpa.web.service;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class CustomerService {

public int size() {
return 1;
}
}

リソースクラスも変更します

@RequestScoped

@Path("")
public class IndexResource {

@Inject
private CustomerService service;

@GET
@Produces(MediaType.TEXT_PLAIN)
public String getText() {
return "size = " + service.size();
}
}

ここで、@ApplicationScopedとか@RequestScopedというのは、それぞれのインスタンスのライフサイクルを示すもので、それぞれ、アプリケーション内でひとつ、リクエスト毎にひとつ、インスタンスが作られて(削除される)ことを意味してます。

まずはこれでTomcatを起動してアクセスしてください。

"size = 1"と表示されるはずです。


EntityManagerもInjectしてみる

続いてEntityManagerもInjectしたいんですが、単に@Inject EntityManager em;とするだけではダメです。

ConsumerServiceとかの場合はBeanとして扱えるので特に何もする必要ないんですが、EntityManagerは具体的にインスタンスを指定する必要があります。そこで使用するのがProducerです(参考:きしださんのブログ)。次のように@Produceアノテーションをつけることで必要になったときにインスタンスが生成されるようにします。

package com.github.kamegu.jerseyjpa.common;

import javax.enterprise.context.RequestScoped;
import javax.enterprise.inject.Disposes;
import javax.enterprise.inject.Produces;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class EntityManagerProducer {

@Produces
@RequestScoped
public EntityManager create() {
EntityManagerFactory fac = Persistence.createEntityManagerFactory("demo");
return fac.createEntityManager();
}

protected void closeEntityManager(@Disposes EntityManager entityManager) {
if (entityManager.isOpen()) {
entityManager.close();
}
}
}

CustomerServiceも変えます。

@ApplicationScoped

public class CustomerService {
@Inject
private EntityManager em;

public int size() {
em.getTransaction().begin();
List<Customer> customers = em.createQuery("select c from Customer c", Customer.class).getResultList();
Customer customer = new Customer(customers.size() + 1);
em.persist(customer);
em.flush();
em.getTransaction().commit();
return customers.size();
}
}

これで、Injectできるようになりました。EntityManagerProducerのメソッドに@RequestScopedとつけていますが、これによってリクエストごとに新しいEntityManagerインスタンスを生成してくれます。@ApplicationScopedなCustomerServiceのフィールドとしてInjectされているので、リクエスト間で共有されているように見えますが、実際はCDIでEntityManagerをラップしたオブジェクトを作成してそれがセットされ、リクエストごとに新しいEntityManagerインスタンスがちゃんと使用されます。


Entity ListenerでもCDIを利用する

JPAを使っていて、結構大事な機能がEntity Listenerです。

この機能を使うことによってデータ保存前や取得後に特定の処理を入れることが出来ます。例えば、ログインユーザのIDを特定のカラムにセットしたい、といった処理が出来ます。

Entity ListenerでのCDI利用についてはJPA2.1から利用可能になっています。@Injectアノテーションで出来るようになるわけです。ただし、これはJava EE環境に限った話で、Java SE環境では必須要件となっておらず、今回試した限りでは動きませんでした。

ということで、他の方法がないか調べていてたどり着いたのがCDI.current()を使う方法です。

まず、Listenerクラスを作成します。今回は単純にEntityManagerを取得してselect文を実行するだけ。

package com.github.kamegu.jerseyjpa.jpa.listener;

import java.util.List;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Instance;
import javax.enterprise.inject.spi.CDI;
import javax.persistence.EntityManager;
import javax.persistence.PrePersist;

import com.github.kamegu.jerseyjpa.entity.Customer;

@ApplicationScoped
public class DummyListener {

@PrePersist
public void prePresist(Object o) {
Instance<EntityManager> e = CDI.current().select(EntityManager.class);
EntityManager em = e.get();

List<Customer> r = em.createQuery("SELECT c FROM Customer c", Customer.class).getResultList();
System.out.println(r.size());
}
}

次に、persistence.xmlと同じディレクトリにorm.xmlを作って、上のlistenerを登録します。

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

<entity-mappings version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd">
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="com.github.kamegu.jerseyjpa.jpa.listener.DummyListener"></entity-listener>
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>

これだけです。試しにTomcatにデプロイしてアクセスしてみると、標準出力に件数が表示されているかと思います。ちなみにこの件数は追加後の件数です(理由は分かりませんが、DB上には保存前でもEntityManager上では管理下に置かれているから??)。


Hibernateで試す

単純にpom.xmlでeclipselinkからhibernateに変更してpersistence.xmlをhibernate用に書き換えただけだと起動時にエラーになりました。

重大: A child container failed during start

java.util.concurrent.ExecutionException: org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Catalina].StandardHost[localhost].StandardContext[/jersey-jpa-web]]
at java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.util.concurrent.FutureTask.get(FutureTask.java:192)
(中略)
Caused by: org.jboss.weld.exceptions.DeploymentException: org.jboss.jandex.ClassInfo.hasNoArgsConstructor()Z
at org.jboss.weld.executor.AbstractExecutorServices.checkForExceptions(AbstractExecutorServices.java:66)
at org.jboss.weld.executor.AbstractExecutorServices.invokeAllAndCheckForExceptions(AbstractExecutorServices.java:43)
(中略)
Caused by: java.lang.NoSuchMethodError: org.jboss.jandex.ClassInfo.hasNoArgsConstructor()Z
at org.jboss.weld.environment.deployment.discovery.jandex.JandexClassFileInfo.<init>(JandexClassFileInfo.java:65)
at org.jboss.weld.environment.deployment.discovery.jandex.JandexClassFileServices.getClassFileInfo(JandexClassFileServices.java:82)

これの解決方法はこちらを見つけました。

http://stackoverflow.com/questions/25797866/hibernate-makes-weld-not-initializing-in-java-se

どうやらjandexというライブラリが必要とのこと。なので追加してみます。

    <dependency>

<groupId>org.jboss</groupId>
<artifactId>jandex</artifactId>
<version>1.2.2.Final</version>
</dependency>

これで問題なく動きました。

gitリポジトリに"hibernate-cdi"タグをつけてます。

ちなみにHibernateではDummyListenerで出力される件数は追加前の件数になってました(EclipseLinkより1件少ない)。

なぜでしょ??