Takashi Nishigaya
Nishigaya's Weblog
Profile
Takashi Nishigaya
Sr. Java Architect
SunJava Consulting
Professional Services Delivery
アーカイブ
« 11月 2009
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
     
       
今日
Click me to subscribe
Search

Java.net
リンク
 
« EJB3 リモートとローカルでビジネス・... | メイン | Java EE 5:デプロイメント記述子... »
木曜日 7 13, 2006
Java EE 5チュートリアル:EJB3 セッション・ビーン編(2) 〜EJBクライアント〜

今回はEJBクライアントからのセッション・ビーンの参照の取得についてです。例によって、ステートレス・セッション・ビーン(SLSB)本体として以下のようなCalcImplクラスと、ビジネス・インタフェースCalcを用いて説明します。

package com.example;
import javax.ejb.Stateless;
import javax.ejb.Remote;

@Stateless
@Remote(Calc.class)
public class CalcImpl implements Calc {
    public int add(int i, int j) {
        return i + j;
    }
}
package com.example;

public interface Calc {
    public int add(int i, int j);
}

事前準備として、上記2つのクラスからEJBモジュール(JARファイル)を作成し、SJS AppServer 9(glassfish)のautodeployディレクトリにコピーしてデプロイします。以前のエントリでも述べましたが、@StatelessなどのアノーテーションがEJBモジュールのためのメタ情報を提供してくれるため、J2EE 1.4では必須であったデプロイメント記述子META-INF/ejb-jar.xmlを省略できるようになりました。そのため、ほとんどの場合、EJBモジュールの作成には、javacコマンドとjarコマンドさえあれば十分です。以下に、EJBモジュールcalc.jarの作成とデプロイまでのコマンド実行例を示します。

$ $ASROOT/bin/asadmin start-domain domain1                    # AppServerの起動
$ ls com/example/
Calc.java  CalcImpl.java
$ javac -classpath $ASROOT/lib/javaee.jar com/example/*.java  # コンパイル
$ jar cvf calc.jar com/example/*.class                        # EJB JARの作成
$ cp calc.jar $ASROOT/domains/domain1/autodeploy/             # デプロイメント

ブラウザからhttp://localhost:4848/の管理コンソールにログインし、Applications>EJB Modulesのツリーにcalcという名前のモジュールが現れていれば正しくデプロイメントできています。

まずは、これをスタンドアロン・クライアントからリモート呼出ししてみましょう。従来、EJBのリモート参照を取得するためには、JNDIのルックアップコードを記述する必要がありました。EJB 3.0では@EJBアノーテーションによって、クライアント・オブジェクトに対してEJB参照を自動的にインジェクションしてくれるようになったため、クライアントのコードは以下のようにとてもシンプルになります。

import com.example.Calc;
import javax.ejb.EJB;

public class Client {

    @EJB
    static Calc calc;

    public static void main(String args[]) {
	int i = 10;
	int j = 20;
	System.out.printf("%d + %d = %d%n", i, j, calc.add(i, j));
    }
}

クライアント・オブジェクトへのリソース・インジェクションは、クライアントのメイン・クラスのインスタンスが生成された時の1回だけです。そのため、@EJBアノーテーションを付与するメンバ変数は必ずstaticで宣言する必要があります。変数のスコープはなんであっても(privateであっても)構いません。

ただし、(Webサービスの場合と同様に)スタンドアロン・クライアントでリソース・インジェクション機能を利用するためには、クライアントはJava EEクライアント・コンテナ上で動かす必要があります。SJS AppServer 9 (glassfish)におけるクライアント・コンテナの起動はappclientコマンドによって提供されています。従来、クライアント・コンテナでEJBクライアントを実行するためには、クライアント用デプロイメント記述子META-INF/application-client.xmlの作成と、クライアントEARファイルの作成が必須でしたが、@EJBアノーテーションがapplication-client.xmlにおける<ejb-ref>タグの情報を提供してくれるようになったため、META-INF/application-client.xmlの作成を省略することができます。また、SJS AppServer 9 (glassfish)ではクライアントEARの作成も省略することができるため、リソース・インジェクションを含むクライアントの実行はjavaコマンドの代わりにappclientコマンドを使うことで簡単に実行できます。

$ javac -classpath $ASROOT/lib/javaee.jar:. Client.java     # コンパイル
$ $ASROOT/bin/appclient Client                              # 実行
10 + 20 = 30

※ASROOTはAppServerのインストールルートのディレクトリ

もし、クライアント・アプリケーションの構成上の都合で、クライアント・コンテナ(すなわちappclientコマンド)が使用できない場合は、@EJBアノーテーションによるリソース・インジェクションは機能しません。その場合はEJB参照をJNDIルックアップで取得するためのコードを記述する必要があります。この場合の修正方法を考えるため、@EJBを使用した場合のClientクラスの振舞をもう少し詳しく調べてみましょう。

@EJBの役割は、<ejb-ref>に相当するメタ情報の提供と、インジェクションすべき変数の場所をクライアントコンテナへ伝えるという2つの役割を持っています。また、インジェクションの実際の処理は、JNDIルックアップによるEJB参照の取得処理です。従って、以下のように、InitialContext#lookup(String)によるコードがコンテナによって自動的に実行されていると考えられます。

@EJB
static Calc calc = (Calc)new InitialContext().lookup("java:comp/env/Client/calc");

ここで2つの点について補足しておかなければなりません。まず1点目は、EJB 3.0におけるEJBのルックアップ処理は簡略化され、looup()メソッドの戻り値はEJBホーム・インタフェースではなく、EJB参照が直接取得できるようになっていることです([1])。

2点目は、lookup()に指定されているEJB参照のJNDI名です。@EJBによるインジェクション処理では、JNDI名は"java:/comp/env"で始まる論理名でルックアップされます。これは、デプロイメント記述子の<ejb-ref-name>に相当するものであり、@EJB.name要素から決定されます。もし、@EJB.nameが省略された場合のデフォルト値は、Java EE 5仕様で決まっており、インジェクションされる側のクラス名(パッケージ名含む)+ "/" + 変数名に設定されます([2])。上記の例では、@EJB.nameのデフォルト値は"Client/calc"となるため、lookup()メソッドに指定すべきJNDI名は"java:comp/env/Client/calc"となるわけです。デフォルト値を使用しない場合は、以下のように@EJB.name要素を明示することもできます。

    @EJB(name="ejb/Calc")
    static Calc calc;

    public static void main(String args[]) throws Exception {

	InitialContext ctx = new InitialContext();
	calc = (Calc)ctx.lookup("java:comp/env/ejb/Calc");
             :

上記のように、lookup()を明示的に実行するとしても、クライアント・コンテナを使用しないでクライアントを実行する場合、@EJBアノーテーションは意味をなしません。そのため、"java:comp/env"で始まる論理名を使用することができません。クライアントコンテナを使用しないスタンドアロン・クライアントでは、java:comp/envで始まる論理名ではなく、対象EJBのJNDI物理名を指定してEJB参照を取得する必要があります。デプロイされているEJBのJNDI物理名を知るにはどうしたらいいでしょうか。Calc EJBをデプロイする際にはJNDI名を明示しませんでしたので、何らかのデフォルト名が設定されているはずです。SJS AppServer 9 (glassfish)の場合は、asadminコマンドを用いて、AppServerに登録されているJNDI名の一覧を取得することができますので、これを用いてJNDI物理名を調べます。

$ $ASROOT/bin/asadmin list-jndi-entries
Jndi Entries for server within root context: 
UserTransaction: com.sun.enterprise.distributedtx.UserTransactionImpl
com.example.Calc#com.example.Calc: javax.naming.Reference
com.example.Calc__3_x_Internal_RemoteBusinessHome__: javax.naming.Reference
com.example.Calc: javax.naming.Reference
jdbc: com.sun.enterprise.naming.TransientContext
ejb: com.sun.enterprise.naming.TransientContext
Command list-jndi-entries executed successfully.

Calcモジュールに関連するものとしては、1) "com.example.Calc#com.example.Calc"、2) "com.example.Calc__3_x_Internal_RemoteBusinessHome__"、3) "com.example.Calc"の3つのJNDI名が登録されています。この中でCalcビジネス・インタフェースの参照を表すものは1)または3)です。1)の様に#を含むJNDI名はセッション・ビーンが複数のリモート・ビジネス・インタフェースを持つ場合にそれらを区別するために割り当てられるものです。また、2)はAppServer内部で使用しているホームインタフェース用のようです(内部的にはEJB 2.1のランタイムを共有しているということですね)。CalcインタフェースのEJB参照を取得するという目的においては、1)と3)どちらのアドレスを使用しても構いません。ここでは3)の"com.example.Calc"というアドレスを使用しましょう([3])。JNDIも分かりましたので、クライアントを以下のように修正します。@EJBアノーテーションはここでは意味をなさないため、削除しています。

import com.example.Calc;
import javax.naming.InitialContext;

public class Client {

    static Calc calc;

    public static void main(String args[]) throws Exception {

	InitialContext ctx = new InitialContext();
	calc = (Calc)ctx.lookup("com.example.Calc"); // 物理名によるlookup

	int i = 10;
	int j = 20;
	System.out.printf("%d + %d = %d%n", i, j, calc.add(i, j));
    }
}

今度は、クライアント・コンテナなしのjavaコマンドから直接起動することができますが、クラスパスにはJava EE 5 APIのjavaee.jarとランタイム・ライブラリのappserv-rt.jarを含める必要があります。

$ java -classpath $ASROOT/lib/javaee.jar:$ASROOT/lib/appserv-rt.jar:. ¥
       Client
10 + 20 = 30

また、上記のクライアントは、サーバと同じホストで実行していたため、JNDIサーバのアドレスを明示する必要がありませんでした。リモートホストからアクセスする場合は、システム・プロパティorg.omg.CORBA.ORBInitialHostにAppServerのホスト名を(JNDIのポート番号をデフォルトの3700から変更している場合はorg.omg.CORBA.ORBInitialPortにポート番号も)指定してクライアントを起動します([4])。

$ java -Dorg.omg.CORBA.ORBInitialHost=server_host ¥
       -Dorg.omg.CORBA.ORBInitialPort=3700 ¥
       -classpath $ASROOT/lib/javaee.jar:$ASROOT/lib/appserv-rt.jar:. ¥
       Client

このように、クライアント・コンテナを使用しない場合、EJB呼出しの手軽さはかなり失われます。GlassFish EJB FAQでもクライアント・コンテナの使用を薦めていますが、実際のリッチ・クライアント開発でクライアントのブートストラップをappclientコマンドに依存させるかどうかはかなり悩ましい問題です。

Webサービスでクライアント・コンテナを使用しない場合でもインジェクションは使用できませんが、EJBの場合に較べたらクライアント・コンテナを使用しないことのインパクトは小さいです。また、Java SE 6 (Mustang)にJAX-WSランタイムが含まれることを考えても、リッチ・クライアントが使用するプロトコルとしては、EJB呼出しよりもWebサービスが有利であると考えられます。


[1] EJB 2.xでは、lookup()メソッドでホーム・インタフェースを取得し、ナローイング手続きを行なった後、そのホーム・インタフェースからcreate()メソッドによって初めてEJB参照が取得できました。参考にEJB 2.xの場合のEJB参照取得時のサンプル・コードを示します。

InitialContext ctx = new InitialContext();
Object ref = ctx.lookup("java:comp/env/ejb/Calc");
CalcHome calcHome =
    (CalcHome)PortableRemoteObject.narrow(ref, CalcHome.class);
Calc calc = calcHome.creat();

このように非常に繁雑であり、あちこちで繰返し定義されるコードであることから、J2EEパターンでは、上記のコードを隠蔽するサービス・ロケータを用意しましょうというのが、J2EE 1.4におけるベスト・プラクティスの基本でした。Java EE 5環境でのEJBルックアップは非常に簡略化されたため、もはやサービス・ロケータは実装する必要がないと言えます。

[2] アノーテーションによりインジェクションされるリソースのJNDIコンテキスト名(java:comp/env配下の名前)のデフォルト値に関しては、Java EE Specification v5 (JSR-244), EE.5.2.3節 Annotations and Injection, pp.63に記述されています。以下、該当部分の抜粋です。

A field of a class may be the target of injection. The field may not be final.
By default, the name of the field is combined with the fully qualified name of the class
and used directly as the name in the application components naming context.
For example, a field named myDatabase in the class MyApp in the package
com.example would correspond to the JNDI name java:comp/env/com.example.MyApp/myDatabase.

[3] EJB 2.1仕様までは、EJBコンポーネントのJNDI物理アドレスのマッピング方法は標準化の範囲外でした。各AppServerベンダは独自にJNDI名のマッピング方法を提供しています。例えば、SJS AppServerでは、sun-ejb-jar.xmlという独自のデプロイメント記述子にjava:comp/envで始まる論理名と物理名のマッピングを定義するようになっています。EJB 3.0では、@Stateless@Statefulおよび@MessageDrivenアノーテーションに、標準仕様としてJNDI物理名を指定するためのmappedNameメンバ要素が定義されています(同時にejb-jar.xmlのスキーマにも<mapped-name>要素が追加されています)。ただし、APIドキュメントが示すように、AppServerベンダによってはmappedName要素をサポートしていない可能性があること、mappedName要素を省略した場合のデフォルト名のルールが異なる可能性があることに注意しなければなりません。

A product specific name(e.g. global JNDI name) that this session bean should be
mapped to. Application servers are not required to support any particular form
or type of mapped name, nor the ability to use mapped names. The mapped name is
product-dependent and often installation-dependent. No use of a mapped name is
portable.

なんとも中途半端な仕様ですが仕方がありません。一応、SJS AppServer 9 (glassfish)ではmappedNameをサポートしており、これを指定することによりJNDI物理アドレスを変更することができます。mappedName要素を明示してJNDI名を変更する場合は、サーバ側の@Stateless.mappedNameと同時にクライアント側の@EJB.mappedName要素もサーバ側と同じ名前を明示しなければいけない点に注意して下さい。

[4] 本来、JNDIの標準仕様からすれば、プロパティjava.naming.factory.initialとjava.naming.provider.urlで制御可能であるべきだと思うのですが、GlassFish V1 UR1では正しく設定することができませんでした。

Posted at 07:55午後 7 13, 2006 by Takashi Nishigaya in Java  |  投稿されたコメント[1]

投稿されたコメント:

cool

Posted by wow gold on 11月月 03日, 2008年 at 10:40 午前 JST #

コメント
  • HTML文法 不許可
« EJB3 リモートとローカルでビジネス・... | メイン | Java EE 5:デプロイメント記述子... »