GlassFish 3でArquillianを動かす。Arquillianを使用したJUnitで、CDIでEJB入れてJPAでアクセスするのが動くところまでやる。Arquillianの動作モードは、embeded,remote,manageの三種類がある。embededがお手軽なんだけど、このエントリではremoteでやる。
環境
やったこと
まずはEclipseのプロジェクト構成。
> tree /F C:. │ .classpath │ .project │ pom.xml │ ├─src │ ├─main │ │ ├─java │ │ │ └─kagamihoge │ │ │ └─gf3arq │ │ │ HogeEJB.java │ │ │ HogePOJO.java │ │ │ │ │ ├─resources │ │ └─webapp │ │ │ index.jsp │ │ │ │ │ └─WEB-INF │ │ beans.xml │ │ │ └─test │ ├─java │ │ └─kagamihoge │ │ └─gf3arq │ │ HogeTest.java │ │ Resources.java │ │ │ └─resources │ └─META-INF │ test-persistence.xml │
pom.xmlの依存性にかかわる部分だけを抜粋。
<dependencyManagement> <!-- Arquillian API --> <dependencies> <dependency> <groupId>org.jboss.arquillian</groupId> <artifactId>arquillian-bom</artifactId> <version>1.1.1.Final</version> <scope>import</scope> <type>pom</type> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- Arquillian-JUnitインテグレーション --> <dependency> <groupId>org.jboss.arquillian.junit</groupId> <artifactId>arquillian-junit-container</artifactId> <scope>test</scope> </dependency> <!-- Embedded GlassFishコンテナアダプタ(remote用) --> <dependency> <groupId>org.jboss.arquillian.container</groupId> <artifactId>arquillian-glassfish-remote-3.1</artifactId> <version>1.0.0.CR4</version> <scope>test</scope> </dependency> <!-- 後述 --> <dependency> <groupId>com.sun.jersey</groupId> <artifactId>jersey-server</artifactId> <version>1.11</version> </dependency> <!-- 後述 --> <dependency> <groupId>javax.mail</groupId> <artifactId>mail</artifactId> <version>1.4.4</version> </dependency> <!-- 後述。JavaEE6の依存性。この位置にあることが重要 --> <dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>6.0</version> <scope>provided</scope> </dependency> </dependencies>
Arquillianを使用したJUnitのコード。
package kagamihoge.gf3arq; import java.nio.file.Path; import java.nio.file.Paths; import javax.inject.Inject; import kagamihoge.gf3arq.HogeEJB; import kagamihoge.gf3arq.HogePOJO; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.junit.Arquillian; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(Arquillian.class) public class HogeTest { @Deployment public static WebArchive createDeployment() { Path webinf = Paths.get( "src", "main", "webapp", "WEB-INF"); Path testPersistenceXML = Paths.get( "src", "test", "resources", "META-INF", "test-persistence.xml"); WebArchive arch = ShrinkWrap.create(WebArchive.class, "test-hoge.war") .addClasses(HogeEJB.class, HogePOJO.class, Resources.class) .addAsWebInfResource(webinf.resolve("beans.xml").toFile()) .addAsResource(testPersistenceXML.toFile(), "META-INF/persistence.xml"); //↓のようにすることで、Arquillianに渡したディレクトリ構成が参照できる。 System.out.println(arch.toString(true)); return arch; } @Inject private HogeEJB hogeEJB; @Test public void testInsert() { hogeEJB.insert(); } @Test public void testSelect() { hogeEJB.select(); } }
ここでは、beans.xmlはプロダクションと同等のもの、persistence.xmlはテスト用の任意のファイルを使う、という想定で書いている。具体的には、src/test/resources/META-INF/test-persistence.xml を META-INF/persistence.xml として扱うコードにしている。テスト用と本番用で別のXMLを使う素朴な例だが、もう少し高度なやり方はTesting Java Persistence · Arquillian Guidesを参照。
arch.toString(true)の部分は下記のような出力になる。デバッグとして使用する。beans.xmlやpersistence.xmlが所定のディレクトリに収まっているか、とかの確認に使える。
test-hoge.war: /WEB-INF/ /WEB-INF/beans.xml /WEB-INF/classes/ /WEB-INF/classes/META-INF/ /WEB-INF/classes/META-INF/persistence.xml /WEB-INF/classes/kagamihoge/ /WEB-INF/classes/kagamihoge/gf3arq/ /WEB-INF/classes/kagamihoge/gf3arq/Resources.class /WEB-INF/classes/kagamihoge/gf3arq/HogeEJB.class /WEB-INF/classes/kagamihoge/gf3arq/HogePOJO.class
beans.xml これは例によってマーカー用のカラのファイル。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd"> </beans>
EJBのコード。Injectを、JUnit -> EJB -> POJOを試したかったので、EJBは特に何もしないコードになっている。
package kagamihoge.gf3arq; import javax.ejb.LocalBean; import javax.ejb.Stateless; import javax.inject.Inject; @Stateless @LocalBean public class HogeEJB { @Inject private HogePOJO hogePOJO; public HogeEJB() { } public void select() { hogePOJO.select(); } public void insert() { hogePOJO.insert(); } }
EJBから参照されるPOJOのコード。EntityManagerをinjectしてデータアクセスする。
package kagamihoge.gf3arq; import java.util.List; import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.Query; public class HogePOJO { @Inject private EntityManager em; public HogePOJO() { super(); } public void insert() { Query q = em.createNativeQuery( "insert into users(id, name, email) values('14', 'ddd', 'ddddd@hogehoge.com')"); System.out.println("###insert count:" + q.executeUpdate()); } public void select() { Query q = em.createNativeQuery("select table_name from user_tables"); List r = q.getResultList(); System.out.println("###table names"); for (Object tableName : r) { System.out.println("##"+ tableName); } } }
EntityManagerのインスタンスをProducesするためのクラス。詳細は後述。
package kagamihoge.gf3arq; import javax.enterprise.inject.Disposes; import javax.enterprise.inject.Produces; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.PersistenceUnit; public class Resources { @PersistenceUnit(unitName="testdb") private EntityManagerFactory emf; @Produces public EntityManager getEntityManager() { System.out.println("#####open em"); return emf.createEntityManager(); } public void closeEntityManager(@Disposes EntityManager em) { System.out.println("#####close em"); em.close(); } }
src/test/resources/META-INF/test-persistence.xml あまり深い意味は無いんだがunitを二つ定義している。データソースに関してはGlassFish4でOracle11gXEのJDBC接続をつくる - kagamihogeの日記等で事前に作ってあるものとする。
<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"> <persistence-unit name="testdb" transaction-type="JTA"> <jta-data-source>jdbc/Oracle11gXE</jta-data-source> </persistence-unit> <persistence-unit name="hoge" transaction-type="JTA"> <jta-data-source>jdbc/Oracle11gXE</jta-data-source> </persistence-unit> </persistence>
実行したときのglassfishのサーバログ。/test-hogeでデプロイ、em生成、テスト実行でinsertとselect、em破棄、アンデプロイ、と流れているのが見て取れる。
情報: EclipseLink, version: Eclipse Persistence Services - 2.3.2.v20111125-r10461 情報: file:/C:/Java/glassfish/glassfish-3.1.2.2/glassfish/domains/domain1/applications/test-hoge/WEB-INF/classes/_testdb login successful 警告: Multiple [2] JMX MBeanServer instances exist, we will use the server at index [0] : [com.sun.enterprise.v3.admin.DynamicInterceptor@18551ae]. 警告: JMX MBeanServer in use: [com.sun.enterprise.v3.admin.DynamicInterceptor@18551ae] from index [0] 警告: JMX MBeanServer in use: [com.sun.jmx.mbeanserver.JmxMBeanServer@3e7bf0] from index [1] 警告: The collection of metamodel types is empty. Model classes may not have been found during entity search for Java SE and some Java EE container managed persistence units. Please verify that your entity classes are referenced in persistence.xml using either <class> elements or a global <exclude-unlisted-classes>false</exclude-unlisted-classes> element 情報: EJB5181:Portable JNDI names for EJB HogeEJB: [java:global/test-hoge/HogeEJB!kagamihoge.gf3arq.HogeEJB, java:global/test-hoge/HogeEJB] 情報: WEB0671: Loading application [test-hoge] at [/test-hoge] 情報: test-hogeは、1,328ミリ秒で正常にデプロイされました。 情報: Deleting file.... 情報: #####open em 情報: ###insert count:1 情報: ###table names 情報: ##USERS 情報: ##HOGE 情報: ##CUSTOMER2 情報: ##ORDERS2 情報: ##CUSTOMER3 情報: ##ORDERS3 情報: ##N_DEPT 情報: ##N_EMP 情報: ##EMP 情報: ##DEPT 情報: ##C_EMP 重大: No valid EE environment for injection of kagamihoge.gf3arq.Resources 情報: #####close em 情報: file:/C:/Java/glassfish/glassfish-3.1.2.2/glassfish/domains/domain1/applications/test-hoge/WEB-INF/classes/_testdb logout successful
はまりどころ
ハマったり、悩んだりしたところ。
Missing artifact org.jboss.arquillian:arquillian-bom:jar:1.1.1.Final
Missing artifact org.jboss.arquillian:arquillian-bom:jar:1.1.1.Final
bomなのでdependenciesセクションではなく、dependencyManagementセクションに入れる必要がある。また、repositoryにJBossを入れる必要がある。
<repository> <id>JBoss Repo</id> <url>http://repository.jboss.org/nexus/content/groups/public-jboss/</url> <name>JBoss Repo</name> </repository>
参考:
Absent Code attribute in method that is not native or abstract in class file
java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file javax/ws/rs/core/MediaType
この原因をごく簡単に言えば、javax/ws/rs/core/MediaTypeの実体が無いために起きる。これの解消はまず、下記のように依存性を追加してやる。なお、glassfish 3.1.2.2の場合なので、他バージョンや他アプリケーションサーバの場合は異なる可能性があると思われる。また、javaeeの依存性(ここではjavaee-web-api)を一番最後に持ってくる。
<dependency> <groupId>com.sun.jersey</groupId> <artifactId>jersey-server</artifactId> <version>1.11</version> </dependency> .... <dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>6.0</version> <scope>provided</scope> </dependency>
どうしてこうしなけばならないのかは俺自身詳しいことは良く理解していないのだけど。まず、javaee-web-apiはインタフェースなどのみで実装は保持していない。これは昨今のj2eeでは普通のことである。が、arquillianはその途中で実装を要求?するのだが、インタフェースしか無いのでこの例外が発生する(らしい) なので、まずは足りない実装(ここではjersey-server)を追加し、更に実装を先に読んでインタフェースを後回しにするためにjavaee-web-apiを後に持っていく……ことで上手く行くらしい。
他の解決策として、クラスパスにローカルにインストールしたアプリケーションサーバのライブラリを追加するとか、jboss-specとかの依存性で実装をまるっと入れるとか、が上げられている。
また、この環境だと下記の例外も発生する。
java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file javax/mail/internet/ParseException
よって、下記の依存性を追加して対処。
<dependency> <groupId>javax.mail</groupId> <artifactId>mail</artifactId> <version>1.4.4</version> </dependency>
参考:
JUnitでinjectしたejbのインスタンスがNullPointerException
beans.xmlがない、glassfish(weld?)のバージョンによってはinjectでなくejbアノテーション、など。beans.xmlを入れているつもりでも、/WEB-INF/beans.xmlとかのディレクトリに入ってないこともある。WebArchive#toString(true)でデバッグ出力をして確認する。
Could not connect to DAS
org.jboss.arquillian.container.spi.client.container.LifecycleException: Could not connect to DAS on: http://localhost:4848 | Connection refused: connect
remoteの場合、JUnit実行前にGlassFishサーバが起動していなければならない。起動せずにJUnit実行するとこんな例外が出る。
WELD-001408 Unsatisfied dependencies for type [EntityManager]
com.sun.jersey.api.container.ContainerException: exit_code: FAILURE, message: デプロイメント中にエラーが発生しました: Exception while loading the app : WELD-001408 Unsatisfied dependencies for type [EntityManager] with qualifiers [@Default] at injection point [[field] @Inject private kagamihoge.gf3arq.HogePOJO.em]。詳細はserver.logを参照してください。 [status: CLIENT_ERROR reason: Bad Request]
@Inject private EntityManager em;
EntityManagerはInjectアノテーションではなく、PersistenceContextアノテーションを使用する。
@PersistenceContext(unitName="testdb") private EntityManager em;
これの原因は、CDIがEntityManagerのインスタンスの生成方法を知らないから発生する。なので、PersistenceContextと専用のアノテーションを使ってやれば良い。
しかし、そうは言ってもInjectでやらせてくれよ、というのが人情である。CDIがEntityManagerのインスタンスの作成方法を知っていさえすればよいので、下記のようなクラスを作成する。下記のコードは、PersistenceContextでインスタンスを注入し、ProducesアノテーションでEntityManagerのインスタンスはコレを使え、と指示している。
public class Resources { @SuppressWarnings("unused") @Produces @PersistenceContext(unitName="testdb") private EntityManager em;
一応これでも問題ないのだが、コンテナ管理とかしてるわけではないので、誰もcloseしていない。どうせデプロイ&アンデプロイするんだから知ったことか、というのも一手ではあるだろうが、やはりcloseしておきたい。
上述のResourcesを書き加える。Disposesアノテーションを付与したメソッドを作り、そこでcloseさせる。これの実行結果はこのエントリの上の方を参照。
public class Resources { @PersistenceUnit(unitName="testdb") private EntityManagerFactory emf; @Produces public EntityManager getEntityManager() { System.out.println("#####open em"); return emf.createEntityManager(); } public void closeEntityManager(@Disposes EntityManager em) { System.out.println("#####close em"); em.close(); } }
なお、下記のような書き方はダメである。
public class Resources { @SuppressWarnings("unused") @Produces @PersistenceContext(unitName="testdb") private EntityManager em; public void closeEntityManager(@Disposes EntityManager em) { System.out.println("#####close em"); em.close(); } }
こんな感じの例外が発生する。なので、EntityManagerを取得するところはプロパティからメソッドに変更している。
com.sun.jersey.api.container.ContainerException: exit_code: FAILURE, message: デプロイメント中にエラーが発生しました: Exception while loading the app : WELD-001424 The following disposal methods were declared but did not resolve to a producer method: [Disposer method [[method] public gf3arq.Resources.closeEntityManager(EntityManager)]]。詳細はserver.logを参照してください。 [status: CLIENT_ERROR reason: Bad Request]
参考:
No valid EE environment for injection
重大: No valid EE environment for injection of gf3arq.Resources
謎。問題無く動くんで無視した。
感想とか
ぶっちゃけここまで作るのにも数日を要しており、ホンマStack Overflow様々やで。
Arquillian, CDI(Weld), EJB, JPA, Mavenが入り乱れるので、これらの知識は当然のように要求される。GlassFishなりJBossなりのアプリケーションサーバの知識も要る。その上で更に、JUnitで効果的なテストケースを組めるスキルがあってはじめて、Arquillianが活きて来る。なので、Arquillian使う敷居って結構高いんでないかねぇ……などと思うのであって。いやまぁ、J2EEってそーいうもんといえばそーいうもんなんだけだけどもね。