金曜日 4 23, 2010

EJB 3.1 Embeddable EJB Container improved JUnit-testability

Java EE 6仕様を実装したGlassFish v3ではEJB仕様がマイナーアップデートされ、EJB 3.1仕様となっています。EJB 3.1がEJB 3.0から何が変わったのかの概要については、寺田さんのエントリ「EJB 3.1 の新機能概要」がとてもよくまとまっているのでそちらを参照して頂くとよいと思います。

EJB 3.1はマイナーアップデートではありますが、とても重要な機能が追加されており、中でも組み込みEJBコンテナ(Embeddable EJB Container)とJNDI物理名の標準化(portable global JNDI name)はEOD(Ease Of Development)の方針をさらに押し進めた重要な進化ということができます。今回は、GlassFish v3におけるEmbeddable EJB Containerについて掘り下げてみたいと思います。

Embeddable EJB Containerは何がそんなに嬉しいの?

EJB仕様は3.0からPOJOベースとなり、開発そのものはとてもシンプルになりました。しかし、EJBコンポーネントを単体テストしようとしても、実際のEJBの実装は、JDBCやJMSリソースアクセス、JNDIルックアップ、リソースインジェクション、JTAトランザクション、インターセプターなど、EJBコンテナ上でなければ利用できない様々な機能を前提として実装します。したがって、これらの機能が利用できないJava SEスタンドアロン環境では、実質的に意味のある単体テストを実施することは非常に困難でした。JDBCデータソースはSpring FrameworkのDriverManagerDataSourceで代用したり、JPAのpersitence-unit設定をtransaction-mode="JTA"からtransaction-mode="RESOURCE_LOCAL"にしたりしたところで、UserTransactionやグローバルトランザクションをスタンドアロン環境では利用できないため、アプリケーションのコードを全く変えずに単体テストを実施することは事実上不可能だったと思います。Embeddable EJB Containerは、EJBモジュールのサーバデプロイを必要とせずにEJBの単体テストに必要な組み込み環境をJava SE環境に実現することができる機能です。

Embeddable EJB Containerと同様の機能は、従来Embedded JBossOpenEJBなどで利用することは可能でしたが、これらの組み込みコンテナで単体テストをしても、プロダクション・サーバのベンダと組み込みコンテナのベンダが異なる場合は、デプロイ後も同じコードが問題なく実行される保証はありませんでした。EJB 3.1のEmbeddable EJB Containerはベンダに依存しない標準化されたAPIを用いてEJBの単体テストを実装できる、長い間待ち望まれたAPIということができます。

Embeddable EJB Containerの使い方

Embeddable EJB Containerの起動方法そのものはとても簡単で、起動するときはjavax.ejb.embeddable.EJBContainerクラスのcreateEJBContainer()メソッドを実行し、停止するときはclose()メソッドを実行するだけです。

// EJB組み込みコンテナの起動
EJBContainer container = EJBContainer.createEJBContainer();

// EJB組み込みコンテナの停止
container.close();

EJBContainerを正しく起動するためには、Java EE 6コンテナベンダが提供するランタイムライブラリを予めテスト用のCLASSPATHに設定しておく必要があります。GlassFish v3の場合は、glassfish-embedded-static-shell.jar(または、glassfish-embedded-all.jar)がこのためのライブラリになっており、GlassFish v3をインストール済みの場合は以下の場所に配置されています。

$AS_INSTALL/glassfish/lib/embedded/glassfish-embedded-static-shell.jar

ただし、NetBeans 3.8を使ってJava EE 6のプロジェクトを作成し、デプロイターゲットのサーバをGlassFish v3に設定している場合は、上記のJARファイルは自動的にテスト用のクラスパスに追加されます。

nb68_embedded_jar

Embeddable EJB Containerを使用した実際のJUnitコードは以下のようになります。

package com.example;

import java.util.HashMap;
import java.util.Map;
import javax.ejb.embeddable.EJBContainer;
import javax.naming.NamingException;
import junit.framework.Assert;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class CalcTest {

    static Calc calc;               // テスト対象のEJB
    static EJBContainer container;  // EJB組み込みコンテナ

    /\*\* テストクラスの前処理 \*/
    @BeforeClass
    public static void setUpClass() throws Exception {
        // EJB組み込みコンテナの起動
        container = EJBContainer.createEJBContainer();
        // テスト対象EJB参照を取得
        calc = (Calc)container.getContext().lookup("java:global/classes/Calc");
    }

    /\*\* テストクラスの後処理 \*/
    @AfterClass
    public static void tearDownClass() throws Exception {
        // EJB組み込みコンテナの停止
	container.close();
    }

    @Before
    public void setUp() {
    }

    @After
    public void tearDown() {
    }

    /\*\* Calc#add()メソッドのテスト \*/
    @Test
    public void testadd() {
	int x = 10;
	int y = 20;
	int ret = calc.add(x, y);
	System.out.printf("%d = %d + %d", ret, x, y);
	Assert.assertEquals(30, ret);
    }
}

EJB参照の取得は、EJBContainer#getContext()からlookup()を実行して行いますが、EJB 3.1からEJBのJNDI名が標準化されましたので、EJBのJNDI物理名が"java:global"ネームスペースから取得します。実際のアプリケーションサーバ上では、EJBの標準化されたJNDI名として"java:module"ネームスペースと"java:app"ネームスペースからもEJB参照を取得できますが、Embeddable EJB Containerを使用する場合は、"java:global"ネームスペースからしかEJB参照を取得できないことに注意してください[1]

NetBeans 6.8とEJBContainerを使用した場合のEJBのJNDIについての注意

上記では、Embeddable EJB ContainerからEJB参照を取得する場合は、EJB参照のJNDIとして"java:global"ネームスペースを使用することを述べました。そして、先のJUnitテストサンプルコードでは、EJB参照のJNDI名が"java:global/classes/Calc"でした。EJB 3.1で標準化された"java:global"ネームスペースのJNDI名は、

java:global[/<app-name>]/<module-name>/<bean-name>[!<fully-qualified-interface-name>]

ですから、この場合EJBのモジュール名が"classes"になっていることになります。Embeddable EJB Containerを使用してEJBをテストする場合は、必ずしもEJB JARを作成する必要はなく、コンパイル済みのEJBクラスを含むディレクトリをクラスパスに含めれば、EJBContainerが起動時にクラスパスからEJBを探し出してきて、組み込みコンテナ上にJNDI登録してくれることになっています[2]。EJBモジュールが展開形式の場合は、デフォルトでEJBクラスを含むクラスパスの最後のディレクトリ名がそのEJBのモジュール名になります。NetBeans 6.8のEJBモジュール・プロジェクトでは、コンパイルしたソースのビルド先は以下のように常に${basedir}/build/classesになっており、JUnitテストの実行時にはこのディレクトリをテスト用のクラスパスに設定して展開形式のEJBモジュールとして実行しているため、EJBモジュール名は"classes"になってしまったのです。

calc-ejb/build
calc-ejb/build/classes
calc-ejb/build/classes/com
calc-ejb/build/classes/com/example
calc-ejb/build/classes/com/example/Calc.class

一方、このEJBをcalc-ejb.jarとしてアーカイブした場合はモジュール名が"calc-ejb"になりますので、アプリケーションサーバにデプロイ後の、このEJBのグローバルなJNDI物理名は"java:global/calc-ejb/Calc"になります。このようにテスト環境とプロダクション環境でコンテナに登録されるEJB名が異なるのは好ましくありません。この問題を解決するには、以下の何れかの方法で対処することができます。

  • このEJBモジュール用に<module-name>のみを指定した以下のようなデプロイメント記述子src/conf/ejb-jar.xmlに用意する。これにより、classes/ディレクトリに展開された場合であってもejb-jar.xmlに指定したモジュール名"calc-ejb"が優先される。
    <?xml version="1.0" encoding="UTF-8"?>
    
    <ejb-jar xmlns = "http://java.sun.com/xml/ns/javaee" 
             version = "3.1"
             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/ejb-jar_3_1.xsd">
                 <module-name>calc-ejb</module-name>
    </ejb-jar>
    
  • NetBeansプロジェクトのbuild.xmlが使用しているプロパティファイルproject.propertiesのbuild.classes.dirプロパティを修正して、ビルド先ディレクトリが想定するEJBモジュール名と同じになるようにする。
    --- nbproject/project.properties.orig  2010-04-13 12:00:23.000000000 +0900
    +++ nbproject/project.properties       2010-04-13 12:01:26.000000000 +0900
    @@ -1,4 +1,4 @@
    -build.classes.dir=${build.dir}/classes
    +build.classes.dir=${build.dir}/calc-ejb
     build.classes.excludes=\*\*/\*.java,\*\*/\*.form,\*\*/.nbattrs
     build.dir=build
     build.ear.classes.dir=${build.dir}/classes
    

上記何れかの対処をすることで、単体テスト時にもプロダクション環境と同じグローバルのJNDI物理名でEJBを取得することができます。

        // テスト対象EJB参照を取得
        calc = (Calc)container.getContext().lookup("java:global/calc-ejb/Calc");

なお、上記の例は該当のEJBを単独のEJBモジュール(EJB JARファイル)としてデプロイすることを想定した場合のJNDI名です。EJBモジュールをEARに含めてデプロイすることを想定した場合のJNDI名は、"java:global/<app-name>/<module-name>/<bean-name>"の形式になります。単体テスト時にこの<app-name>の部分がJNDI名に挿入されるようにする(EARモジュールをエミュレートする)ためには、以下のようにEJBContainer.APP_NAMEプロパティをcreateEJBContainer(Map)メソッドに設定してコンテナを開始します。

        Map env = new HashMap();
        // <app-name>が"calc-app"になるようにエミュレートしてもらう。
        env.put(EJBContainer.APP_NAME, "calc-app");
	container = EJBContainer.createEJBContainer(env);
        // <app-name>="calc-app"を前提のJNDI名でEJB参照を取得する。
        calc = (Calc)container.getContext().lookup("java:global/calc-app/calc-ejb/Calc");

ただし、JNDI名の部分にEJBContainer.APP_NAMEプロパティの値が正しく適用されるためには、テスト用のCLASSPATHに2つ以上のEJBモジュールが設定されなければいけません。もし、プロダクション用のEARアプリケーションでもそれに含まれるEJBモジュールが1つだけしかない場合は、以下のようにダミーのEJB3モジュールを用意してCLASSPATHに設定する必要があります[3]

$ CLASSPATH=$AS_INSTALL/glassfish/lib/embedded/glassfish-embedded-static-shell.jar
$ CLASSPATH=~/projects/calc-ejb/build/classes:~/projects/dummy-ejb/dist/dummy-ejb.jar:$CLASSPATH
$ export CLASSPATH

NetBeans 3.8の場合は以下のようにプロジェクトのライブラリ設定の「テストを実行」タブでダミーのEJB3モジュールのプロジェクトを追加します。

nb38_dummy_ejb

Embeddable EJB Containerで使える機能の範囲はどこまでか?

それでは、EJB 3.1の組み込みコンテナは本物のEJBコンテナに較べてどこまでの機能が使えるのでしょうか。EJB 3.1の仕様書上では、EJB 3.1 Liteの機能が必須であるとしています。EJB 3.1 Liteの機能一覧は、EJB 3.1仕様書の21.1節、pp.596、Table 7に一覧されていますのでそちらを参照ください。EJB 3.1 Liteレベルであっても、JDBC DataSource、JTA、リソース・インジェクションに加え、transaction-type="JTA"なJPA 2.0が使用できますので、モックを必要とせずに実際のコンテナとの同等の単体テストが実施可能と言えます。

これまでのテストコードではEJBContainer#getContext()からネーミング・コンテキストを取得していましたが、実際にはEJBContainerを起動した後なら以下のようにInitialContextからlookup()することが可能です。したがって、JPAを使用したDAOクラスを単体テストするのに、セッション・ビーンで包む必要もなく、JTAモードで設定したJPAベースのDAOクラスを直接テストすることが可能です。

        container = EJBContainer.createEJBContainer();
        // 実際のコンテナ上と同じように、InitialContextからリソース参照を取得
        InitialContext ctx = new InitialContext();
        DataSource ds = ctx.lookup("jdbc/__default");

なお、GrassFish v3のEmbeddable EJB Containerのglassfish-embedded-static-shell.jarを使用する場合は、EJB Timer Service、Felix OSGiフレームワーク以外のほとんどの機能が利用可能だそうなので、JMSやMDBの単体テストも実施することが可能のようです。


[1] テスト対象のEJBの中からなら、"java:module"や"java:app"をネームスペースにしたJNDIのlookup()は可能です。

[2] EJBContainerクラスが起動時にクラスパスからEJBを登録するルールは、, EJB 3.1の仕様書"JSR 318: Enterprise JavaBeansTM,Version 3.1"の22.2.1 EJBContainer, pp.608に記述があります。以下、抜粋です。

By default, the embeddable container searches the JVM classpath(the value of the Java System property java.class.path) to find the set of EJB modules for initialization. A classpath entry is considered a matching entry if it meets one of the following criteria:

  • It is an ejb-jar according to the standard module-type identification rules defined by the Java EE platform specification
  • It is a directory containing a META-INF/ejb-jar.xml file or at least one .class with an enterprise bean component-defining annotation

Each matching entry is considered an EJB module within the same application.[105] If an ejb-jar.xml is present the module-name element defines the module name. Otherwise, for ejb-jars the module name is the unqualified file name excluding the “.jar” extension and for directories the mod- ule name is the unqualified name of the directory (the last name in the pathname’s name sequence). The embeddable container is not required to support more than one matching entry with the same module name.

[3] EAR形式のJNDI名をエミュレーションする場合のEJBContainer.APP_NAMEプロパティについては、EJB 3.1の仕様書"JSR 318: Enterprise JavaBeansTM,Version 3.1"の22.2.2.3 javax.ejb.embeddable.appName, pp.610に記述があります。以下、抜粋です。

This property specifies an application name for the EJB modules executing within the embeddable con- tainer. If specified, the property value applies to the portion of the portable global JNDI name syntax. It is recommended that this property be set whenever an embeddable container is executed with more than one ejb module.

The property name is defined as EJBContainer.APP_NAME.

上記スペックの表現からは読み取りづらいのですが、JSR 318のスペックリードのKenneth Saks氏に確認したところ、EAR形式のJNDI名をエミュレーションするためには複数EJBモジュールを設定することが条件であることは、仕様として意図したものであるとのことです。

About

Takashi Nishigaya
Principal Consultant
Technology Solution Consulting
Oracle Consulting Services

Search

Categories
Archives
« 4月 2014
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
今日