Takashi Nishigaya
Nishigaya's Weblog
Profile
Takashi Nishigaya
Sr. Java Architect
SunJava Consulting
Professional Services Delivery
アーカイブ
« 6月 2006 »
      
1
2
4
5
6
8
9
10
11
12
14
15
16
17
18
19
21
22
23
24
26
28
29
30
31
     
今日
Click me to subscribe
Search

Java.net
リンク
 
月別メイン | 2006年Jun月月 »
木曜日 7 27, 2006
Java EE 5:Dependency Injection (2) 〜インジェクション可能なクラス〜

前回のエントリでは、DI(Dependency Injection)のメリットと、Java EE 5におけるDIの設定としてアノーテーションとXML形式の使い分けについて議論しました。インジェクションは非常に便利なのですが、2つの制約があることに気をつけなければなりません。1つは既に何度も述べているように、インジェクションはEJBコンテナ、Webコンテナ、クライアント・コンテナのいずれかの上でしか機能しないことです。もう1つの注意点は、インジェクションは任意のクラスで可能なわけではないということです。これは、インジェクションされる側のクラスの種類が限定されるだけでなく、そのクラスから参照されるクラスの種類も限定されます。

また、インジェクションはクラスのメンバ変数に直接値を設定するフィールド・インジェクションか、プロパティを表すsetterメソッドを経由してオブジェクトを設定するセッタ・インジェクションのどちらかに限定され、コンストラクタ・インジェクションはサポートされません。

従って、任意のオブジェクトに対してDI可能なSpring FrameworkSeasar2に較べて、Java EE 5のインジェクションにはかなり制約があることを知っておかなければなりません。しかし、DIやAOPは無闇に使用するとかえってシステム全体の振舞を分かりにくくしてしまう恐れがあるため、DIの使用はほどほどにしておいた方がいいでしょう。その意味では、ある程度フレームワーク側で制約がある方が歯止めが効くという考え方もできなくはありません。一般的には、アプリケーションのティアをまたがって連係するコンポーネント同士が疎結合になっていること、ロジックとコンフィギュレーションが疎結合になっていれば十分であると言えます。

インジェクションが可能な(@Resourceなどのアノーテーションが宣言できる)クラスの一覧は、Java EE 5 Specification (JSR-244), EE.5.2.3節, 表EE.5-1, pp.64に明示されています。以下に抜粋します。

インジェクション可能なクラスの一覧

分類 インジェクション可能なクラス
Web サーブレット(javax.servlet.Servlet実装クラス),
サーブレット・フィルタ(javax.servlet.Filter実装クラス),
リスナ(javax.servlet.*Lstener, javax.servlet.http.*Listener,javax.el.ELContextListener実装クラス),
タグハンドラ(javax.servlet.jsp.tagext.Tag実装クラス),
JSFマネージド・ビーン
EJB セッション・ビーン,
メッセージ・ドリブン・ビーン,
インターセプター
Webサービス Webサービス・エンドポイント,
ハンドラ(javax.xml.ws.handler.Handler実装クラス)
クライアント メイン・クラス

一方、インジェクションにより参照可能なオブジェクトにも制約があります。参照可能なオブジェクトの種類に関しては、Java EE 5 Specification (JSR-244), EE.5.4-EE.5.13, pp.68-112JSR-220: EJB V3.0 EJB Core Contracts and Requirements, 16.4-16.15, pp.407-451に記述があります。以下に、参照可能なオブジェクト毎にインジェクションに使用するアノーテーション、対応するデプロイメント記述子の要素名、使用可能なコンテナを一覧にまとめます。

参照可能なオブジェクトの一覧

Java type Annotation DD element Container
基本データ型(String, Character, Integer, Boolean, Double, Byte, Short, Long, Float) @Resource <env-entry> all
javax.sql.DataSource,
javax.jms.ConnectionFactory,
javax.jms.QueueConnectionFactory,
javax.jms.TopicConnectionFactory
javax.mail.Session,
java.net.URL,
javax.resource.cci.ConnectionFactory
@Resource <resource-ref> all
javax.resource.cci.InteractionSpec @Resource <resource-env-ref> all
javax.jms.Queue,
javax.jms.Topic
@Resource <message-destination-ref> all
javax.transaction.UserTransaction @Resource <resource-env-ref> all
javax.transaction. TransactionSynchronizationRegistry @Resource <resource-env-ref> all
org.omg.CORBA.ORB @Resource <resource-ref> all
javax.persistence.EntityManagerFactory @PersistenceUnit <persistence-unit-ref> all
javax.persistence.EntityManager @PersistenceContext <persistence-context-ref> all
EJB参照 @EJB <ejb-ref>, <ejb-local-ref> all
Webサービス/ポート参照 @WebServiceRef <service-ref> all
javax.ejb.TimerService @Resource <resource-env-ref> EJB container only
javax.ejb.EJBContext @Resource <resource-env-ref> EJB container only

TimerServiceとEJBContextはEJBコンテナ内でのみ利用可能ですが、それ以外のオブジェクトは、クライアントコンテナ、Webコンテナ、EJBコンテナのいずれの中でも取得することができます。

それでは、上記の仕様を眺めながら、ティア間の疎結合が可能か?ロジックとコンフィギュレーションの分離が可能か?という観点で考察してみましょう。

ティア間の疎結合に関しては、プレゼンテーション(Web)層とビジネス層、ビジネス層とインテグレーション(persistence)層について考えます。Web層とビジネス層については、JSFマネージド・ビーンがインジェクション可能クラスになっているため、各ページのバッキング・ビーンにビジネスロジックとしてのセッション・ビーンを@EJBでワイヤリングすることで目的を達成することができます。ただし、これはWeb層のフレームワークにJSFを採用した場合に限定されてしまいます。JSF以外のWebフレームワークを使用する場合には@EJBは使用できません。それぞれのWebフレームワークが@EJBと同等のEJB3セッション・ビーンのインジェクションをサポートしている可能性もありますので、使用しているフレームワークの最新状況を確認してみてください。

ビジネス層とインテグレーション層の分離に関しては、Java EE 5インジェクション仕様では実現できないようです。以下のように、@ResourceアノーテーションでDAOオブジェクトをセッション・ビーンにインジェクトできればよいのですが、@ResourceはJava EE管理オブジェクト以外はStringなどの基本データ型のみがインジェクションの対象になっています。

@Stateless
public CutomerManagerImpl implements CustomerManager {
    @Resource CustomerDAO dao;  // NG: 基本データ型でない
        :
}

もし、@Resourceが任意のビーン・オブジェクトをサポートしてくれれば上記の定義は可能だったのですが、この点は非常に残念に思います。将来のJava EE仕様でこの点が拡張されることを望みます。

しかし、そもそも、DAOの目的はデータアクセスの実装を隠蔽することでした。RDBに対するデータアクセスはJPA(Java Persistence API)で標準化されました。JPAのEntityManagerはDAOの標準APIと捉えることもできます。将来に渡ってデータ保管方法をRDBから変更する予定がないのであればDAOを作らず、直接EntityManagerを使用すると判断してもいいでしょう。

@Stateless
public CutomerManagerImpl implements CustomerManager {
    @PersistenceContext EntityManager em; // 汎用(標準)DAOと捉える
        :
}

ただし、上記のデザインにはいろいろと問題もありそうです(ビジネスロジックからEntityManagerを直接扱う場合の問題については別途議論したいと思います)。もし、EntityManagerを隠蔽化したいと考えるなら、そのクラスをシングルトンなDAOクラスとして実装するのではなく、CRUDのみのfine-grainedなセッション・ビーンとして定義し、course-grainedなセッション・ビーンから@EJBで参照されるようにデザインしてもいいでしょう。

次にロジックとコンフィギュレーションの分離についてはどうでしょうか。コンフィギュレーション情報のインジェクションとして使えそうなアノーテーションとしては@Resourceがあります。しかし、コンフィギュレーション情報が非常に少なければ問題ありませんが、非常に多くの構造的な情報を与えるには@Resourceは役不足です。この場合には、アプリケーション独自のコンフィギュレーションをXMLで定義し、このXMLファイルののパスのみをデプロイメント記述子の<env-entry-value>に与えるか、コンフィギュレーションのオブジェクト・ツリーをJMXのMBeanとして実装することも考えられます。

いずれの場合もSpringやSeasarと併用することも検討の候補として挙げられますが、アプリケーションが依存するフレームワークの種類は少ない方がいいでしょう。複数フレームワークの採用に関しては開発効率と同時に将来の保守性についても考慮して検討して下さい。

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

火曜日 7 25, 2006
Java EE 5: Dependency Injection, XML or Annotation?

Java EE 5では、Spring FrameworkSeasar2でよく知られる「依存性の注入(DI: Dependency Injection)」の機能が加わりました。DIはPOJOベースのアプリケーション・デザインを支える基盤技術の一つです。DIを効率よく使用すると以下のようなメリットがあります。

DIの機能を利用するためには、コンポーネント間結合のコードを書く代わりに、DIのためのコンフィギュレーション(メタ情報)をDIコンテナに与えてあげます。DIのためのメタ情報をコンテナに伝える手段はDIコンテナによって異なりますが、一般にはXML形式の外部ファイルやソースコードに埋め込むアノーテーションなどが使われます。例えば、Spring FrameworkにおけるワイヤリングはXMLファイルであり、Searsar2ではXMLに加えコンベンションによるDIをサポートしています。一方、Java EE 5の場合はどうでしょうか。Java EE 5のDIはアノーテーションで定義するものだと思っている方が多いと思います。しかし、実はJava EE 5のDIコンフィギュレーションは、アノーテーションを定義した場合と同等のメタ情報をXMLの外部ファイル(デプロイメント記述子)でも定義することが可能です。

例えば、「Java EE 5チュートリアル:EJB3 セッション・ビーン編(2)」で紹介したように、セッション・ビーンの参照をクライアントにインジェクションするには、以下の様に@EJBアノーテーションを使用しました。

public class Client {

    @EJB static Calc calc;
         :
}

これと全く同じメタ情報をXML外部ファイルとして定義するためには、デプロイメント記述子(クライアント・コンテナの場合はMETA-INF/application-client.xml)にCalcビーンのための<ejb-ref>要素の定義とそれをインジェクションする場所を<injection-target>要素で定義してあげます。

<application-client version="5"
    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/application-client_5.xsd">
  <ejb-ref>
    <ejb-ref-name>Client/calc</ejb-ref-name>
    <ejb-ref-type>Session</ejb-ref-type>
    <remote>com.example.Calc</remote>
    <injection-target>
      <injection-target-class>Client</injection-target-class>
      <injection-target-name>calc</injection-target-name>
    </injection-target>
  </ejb-ref>
</application-client>

上記の例では、クラスCalcのメンバ変数calc(またはsetterメソッドsetCalc(Calc))に環境コンテキスト名java:comp/env/Client/calcのEJBをインジェクションすることを定義しています([1], [2])。この定義があれば、Clientクラスの@EJBアノーテーションを削除してもインジェクションは適切に行なわれます。実際に動作を確認したい場合は、以前のエントリ「Java EE 5チュートリアル:EJB3 セッション・ビーン編(2)」のClient.javaと同じディレクトリで以下の作業を行なって確認してみて下さい。

$ mkdir META-INF
$ vi META-INF/application-client.xml                    # エディタで上記内容のxmlを作成
$ vi Client.java                                        # エディタで@EJBを削除
$ javac -classpath $ASROOT/lib/javaee.jar:. Client.java # コンパイル
$ $ASROOT/bin/appclient Client                          # 実行
10 + 20 = 30

@EJBに限らず、Java EE 5環境で利用可能な全てのインジェクション用のアノーテション(@EJB、@WebServiceRef、@Resource、@PersistenceUnit、@PersitenceContext)はデプロイメント記述子でも同等のメタ情報を定義することが可能です。それでは、インジェクションのコンフィギュレーションには、アノーテーションを使うべきでしょうか?それとも設定ファイルとしてのデプロイメント記述子を使うべきでしょうか?その答えを決めるには、コンポーネントの再利用のライフサイクルとソースコードのコンパイルのコストを考慮する必要があります。

コンポーネントの再利用には短期的な観点と長期的な観点があります。短期的な再利用とは、例えば、あるコンポーネントをスタンドアロン環境で単体テストする場合と本来のJava EEコンテナ上で動作させる場合に、そのコンポーネントに依存するオブジェクトの実体(環境)を変更することです。Java EE 5のインジェクションは(テストのやり方にもよりますが)Java EEコンテナ上でなければ機能しないのですから、単体テストでは依存関係にあるオブジェクトをテストコードで直接設定してあげればよいため、テスト対象のコンポーネントにインジェクション用アノーテーションがあっても邪魔にはなりません。また、開発中はコンポーネント間の依存関係が頻繁に代わる可能性がありますが、開発中はコンパイルと実行を頻繁に行なうため、アノーテーションを含めたソースコード修正のコストはそれほど大きくありません。開発者にとっては、デプロイメント記述子に注意を払うよりは、ソースコードに注意を払う方が作業効率としても高いでしょう。従って、短期的な観点ではアノーテーションを積極的に使用する方がよいと考えられます。

長期的なコンポーネントの再利用の観点では、あるコンポーネントが使用している別のコンポーネントの実装が変更になったり、コンポーネントを動作させるプラットフォームそのものをマイグレーションさせる必要が生じたりする場合に、そのコンポーネントのソースに全く(あるいはほとんど)手を加えることなく動作させたいと考えます。あるいは、JNDI名の物理名を変更するだけであったり、@Resourceで指定した初期設定の値を変更するだけよい場合もあります。この場合の再利用のライフサイクルは数カ月から数年の期間が想定されますので、担当の開発者が代わっていることも多いでしょう。このような場合には、ソースコードには手を加えずアノーテーションの定義はそのままで、デプロイメント記述子の追加・変更で対応するのがよいと思われます。

このように、基本的にはインジェクション用のアノーテーションは積極的に使用することが推奨されますが、1つ注意しておきたい点があります。それは、アノーテーションを定義する際に、デフォルトをなるべく変更しない(すなわち、不必要にアノーテーションのメンバ要素を明示しない)ということです[3]。特に物理アドレスを示すものに関しては、アノーテーションのメンバ要素には明示しない方がいいでしょう。例えば、あるコンポーネントのアノーテーションにJNDI物理アドレスを示すmappedName要素が明示してあったとしましょう。しばらくしてから、このJNDI物理名を変更する必要がでてきた時、ソースコードのmappedNameを修正せずに、デプロイメント記述子に新しいJNDI物理名を定義して対応することにしたとします。Java EE 5仕様では、アノーテーションとデプロイメント記述子の両方に同じ意味の定義があった場合、ランタイムの動作としてはアノーテーションよりもデプロイメント記述子の定義が優先されることになっていますので、デプロイメント記述子の修正だけで問題ありません。しかし、アノーテーションとデプロイメント記述子で異なる設定が重複して存在するのは気持ちが悪いだけでなく、混乱を招く恐れがあります。そのような状況を避けるためにも、アノーテーションのデフォルト値を変更せず、なるべく簡潔で読みやすいソースコードとすることをお薦めします。


[1] <ejb-ref-name>に設定する値は単にそのコンポーネントの環境コンテキスト名(java:comp/env以下のJNDI論理名)ですのでどんな名前をつけても構いません。実際の動作に重要なのはEJBリモート呼び出しの場合はデフォルトで設定されるJNDI物理名の方です。

[2] 対象のEJBがEJB 3.0仕様のものである場合、<ejb-ref-type>要素は省略することができます。また、<remote>要素または<local>要素にはビジネス・インタフェースのクラス名を指定しますが、<injection-target>要素で指定されているフィールドまたはsetterメソッドの引数のクラスが1つのビジネス・インタフェースを特定できるのであれば省略することができます。<injection-target>要素がない場合(インジェクションではなくContext#lookup()で取得することを前提としている場合)は<remote>要素または<local>要素を省略することはできません。詳細は、JSR 220: EJB V3.0 EJB Core Contracts and Requirements, 16.5.1.3 Declaration of EJB References in Deployment Descriptor, pp.416-417に記述されていますが、ここの部分はややこしいので、上で示したように<ejb-ref>に必要な全ての要素を省略せずに記述することをお薦めします。

[3] 逆にアノーテーションの全てのメンバを明示した方がよいと考える方もいます。おそらく、これはアノーテーション・メンバのデフォルト値が将来の仕様で変更になった場合、AppServerをバージョンアップすると意図しない動作をする可能性があることを恐れてのことだと思います。しかしながら、Java EEの将来の仕様で上位互換を捨ててまでメンバのデフォルト値が変更される可能性はとても低いのではないかと思われます。また、アノーテーション・メンバの明示が多ければ、ソースコード中に占めるアノーテーションの割合が多くなり、可読性を悪くする弊害が生じることの方が問題なのではないでしょうか。

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

木曜日 7 20, 2006
Java EE 5:デプロイメント記述子(web.xml, ejb-jar.xml, application.xml)を省略できる条件について

Java EE 5では、Webアプリケーションの構成情報をコンテナに伝えるためのデプロイメント記述子(Deployment Descriptor:DD)と呼ばれるXMLファイルを多くの場合省略することができるようになっています。Java EE 5仕様の標準デプロイメント記述子は、J2EE 1.4と同様、以下のものがあります。

  1. web.xml:Webモジュール(WARファイル)の設定情報
  2. ejb-jar.xml:EJBモジュール(EJB-JARファイル)の設定情報
  3. application.xml:Webアプリケーション(EARファイル)の設定情報
  4. application-client.xml:クライアント・アプリケーション(クライアントJARファイル)の設定情報
  5. webservices.xml:Webサービス用設定情報。WebモジュールまたはEJBモジュールのMETA-INFディレクトリに含めます。J2EE 1.4ではwebservices.xml以外に、WSDLファイル、WSDLマッピングファイルが必須のDDファイルとなっていました
  6. ra.xml:コネクタ・アーキテクチャ仕様に基づくリソース・アダプタ・モジュール(RARファイル)の設定ファイル

上記のうち、(Java EE 5でバージョンが上がらなかったコネクタ・アーキテクチャ仕様1.5の)ra.xml以外のデプロイメント記述子が省略可能になっています。一般的なWebアプリケーション(EARファイル)は以下のような構成になると思いますが、この場合、J2EE 1.4では必須であったweb.xml、ejb-jar.xml、application.xmlが省略可能となります。

ただし、web.xmlについてはWebモジュール内にServlet、Filter、リスナのいずれかが含まれる場合は、web.xmlは省略できないようです([1])。WebモジュールにJSPやWebサービスクラスだけが含まれる場合はweb.xmlが省略できます。

ejb-jar.xmlは、アノーテーションとデフォルトの振舞いによりejb-jarモジュールに含まれる全てのEJB、Webサービス、インターセプターおよびパーシスタンス・クラスの設定情報が十分に表現されているのであれば省略することができます。

また、EARファイルに必須であったapplication.xmlも省略可能となっています。例えば、EJBモジュールとしてcalc.jarがあり、Webモジュールとしてcalc.warがあったとします。この2つからearファイルを作成するためには、従来、以下のようなapplication.xmlをEARファイル内のMETA-INFディレクトリに含める必要がありました。

<application xmlns="http://java.sun.com/xml/ns/javaee" version="5"
             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/application_5.xsd">
  <description>Application description</description>
  <module>
    <ejb>calc.jar</ejb>
  </module>
  <module>
    <web>
      <web-uri>calc.war</web-uri>
      <context-root>calc</context-root>
    </web>
  </module>
</application>

実際には、上記のapplication.xmlは省略することができ、jarコマンドを用いて構成要素となるモジュールをEARファイルに含めればよいだけです。

$ jar cvf calc.ear calc.war calc.jar
$ jar tf calc.ear
META-INF/
META-INF/MANIFEST.MF
calc.war
calc.jar

application.xmlがEARファイルに存在しない場合のモジュールの判別ルールの詳細は、Java EE Specification V5.0, EE.8.4.2節 Deploying a Java EE Application, pp.166に記述がありますが、Configuration by Exceptionコンセプト(デフォルト・ルールを変更したい場合にのみ好みの設定をすればよいという考え方)に基づいた現実的なルールになっていることが分かります。以下にそのルールを抜粋します。

  1. EARファイル内で、ファイル拡張子が.warのものはWebモジュールとして扱う。ここで、そのWebモジュールのコンテキスト・ルート(<context-root>要素の値)は、そのWARファイル名から.warを除いたものになる。
  2. EARファイル内で、ファイル拡張子が.rarのものはリソース・アダプタとして扱う。
  3. ディレクトリlib内にあるファイル拡張子が.jarのファイルは、そのアプリケーションで使用する拡張ライブラリとして扱う。
  4. ディレクトリlib以外にあるファイル拡張子が.jarのファイルは、以下のルールに従う:
    1. そのJARファイルのMETA-INF/MANIFEST.MFにMain-Class属性があるか、またはMETA-INF/application-client.xmlがあれば、そのJARファイルをアプリケーション・クライアント・モジュールとして扱う。
    2. そのJARファイルにMETA-INF/ejb-jar.xmlが含まれているか、またはEJBコンポーネントを示すアノーテーション(@Statelessなど)が付与されたクラスが含まれていれば、そのJARファイルをEJBモジュールとして扱う。
    3. 他のJARファイルは無視する。ただし、他のモジュールのMETA-INF/MANIFEST.MFのClass-Path属性で参照されていれば、拡張ライブラリとして扱う。

このように、ほとんどの現実的なアプリケーションではapplication.xmlを省略することができそうです。しかしながら、アプリケーションの規模が大きくなってくると、AppServerのデプロイヤがアーカイブ内のファイルやクラスを走査するオーバヘッドが大きくなり、アプリケーションの起動時間が気になってくるかもしれません。そのような場合には、きちんとapplication.xmlやejb-jar.xmlをアプリケーションに含めてあげた方がいいでしょう。その場合でも、手作業でデプロイメント記述子を書く必要はありません。SJS AppServer 9(glassfish)では、AppServerのデプロイヤがデプロイ時に自動生成した各種デプロイメント記述子が以下のディレクトリに保存されています。

$ASROOT/domains/domain1/generated/xml/j2ee-apps/calc/calc_jar/META-INF/ejb-jar.xml
$ASROOT/domains/domain1/generated/xml/j2ee-apps/calc/calc_jar/META-INF/sun-ejb-jar.xml
$ASROOT/domains/domain1/generated/xml/j2ee-apps/calc/calc_war/WEB-INF/sun-web.xml
$ASROOT/domains/domain1/generated/xml/j2ee-apps/calc/calc_war/WEB-INF/web.xml
$ASROOT/domains/domain1/generated/xml/j2ee-apps/calc/META-INF/application.xml
$ASROOT/domains/domain1/generated/xml/j2ee-apps/calc/META-INF/sun-application.xml

上記のファイルを開発中のソース・コード・ツリーにコピーして利用すればOKです。


[1] Servlet APIはJava EE 5で2.5にマイナー・バージョン・アップされましたが、新しいJSRは割り当てられず、JSR-154のメンテナンス・リリースという位置付けになります。JSRドキュメントしてはServlet Specification V2.5, Maintenance Draft Review 5として公開されているものが最新のようで、Java EE 5の仕様としては、このドラフトMR5を参照していると思われます。web.xmlが省略できる場合についての記述は、ドラフトMR5の「SRV.9.13 Inclusion of a web.xml Deployment Descriptor」節, pp.73に記述があります。以下、この記述の抜粋です。

A web application is NOT required to contain a web.xml if it does NOT contain any
Servlet, Filter, or Listener components. In other words an application containing
only static files or JSP pages does not require a web.xml to be present.

Webサービス専用のWARファイルを作成するのでない限り、現実的なアプリケーション開発ではweb.xmlを省略できるケースはまずないと考えていいでしょう。Java EE 5全体のEoD化の目標に対して、Web層の仕様だけは出遅れた感が否めません。将来のServlet 3.0、JSF 2.0ではアノーテーションを導入したデプロイメント記述子の省略ができるようになることが期待されます。

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

木曜日 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]

月別メイン | 2006年Jun月月 »