kagamihogeの日記

kagamihogeの日記です。

GlassFish 3 + Arquillian

Arquillian · Write Real Tests

GlassFish 3でArquillianを動かす。Arquillianを使用したJUnitで、CDIEJB入れて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ってそーいうもんといえばそーいうもんなんだけだけどもね。