今だからデータアクセスを真剣に考える!

  佐藤裕之 日本オラクル
プロダクトSC本部 テクノロジーSC部
佐藤 裕之



 はじめに

こんにちは、本来ならば前回「EJB vs. POJO ~準備編~」の続きをやるところですが、Oracleのパーシステンステクノロジの中で大きなトピックありましたの予定を変更してお送りします。
その大きなトピックとは、「Oracle Application Server EJB 3.0 Preview」の登場です。JCP(Java Comminity Process)で策定が進められているEJB 3.0仕様はEarly Draft Review2(以下EDR2)が2005/2/7に発表されましたが、そのEDR2にほぼ準拠した、EJBコンテナであるOracle Application Server EJB 3.0 Previewが2005/3/23に発表されました。
そこで今回から数回にわたり現状のEJB3.0 EDR2の基本を取り上げていきたいと思います。


 EJB 3.0仕様

EJB 3.0仕様は、現在Early Draft Review2(以下EDR2)のステータスであり、以下の3つのドキュメントから構成されています。

1. EJB 3.0 Simplified API - EJB 3.0における簡略化されたAPIの概要(主にEJB 3.0の基本とSession Bean/Message Driven-Bean)
2. Enerprise JavaBeans Core Contracts and Requirements
3. Persistence API - EJB 3.0におけるパーシステンスAPI(主にCMP:Container Managed Persistence)

この内2.のEnerprise JavaBeans Core Contracts and Requirementsは、EDR2の時点では提供されていません。今回はEJB 3.0 Simplified APIを中心にEJB 3.0仕様を学んで見たいと思います。


  EJB 3.0仕様のゴール

EJB 3.0仕様での、主要な目標を一言で言うと「EoD(Ease of Development)の実現」と言えます。以下は仕様のゴールとして挙げられている主要なものです。

1. アノテーションの導入
  J2SE5.0の新機能であるアノテーションを利用して、開発者の負荷を軽減しています。アノテーションに関しては、後で簡単にご紹介しますが、ここではJavaプログラムの中に意味のある(動作に影響がある)メタデータを追加できる機能だと思ってください。EJBでアノテーションを利用する主なメリットはEJB 2.1までEJBで開発をする上で必須だったEJB固有のインタフェースを使用しなくてすむことや、開発者がデプロイメント・ディスクリプタを作成する必要がなくなることや、アノテーションによるDependency Injectionメカニズムの導入により、リソース等の環境に依存した情報の取得が簡略化されることが挙げられます。またEntity BeanのCMP(Container Manged Persistence)では、O/Rマッピングの情報もアノテーションで記述します。
2. Configuration by Exceptionアプローチの実現
  これは、簡単に言えば「デフォルト以外(例外)を設定するアプローチ」といえます。具体的にはアノテーションにより実現されます。アノテーションはそれ自体にデフォルト値を設定する事ができ、そのデフォルトと異なる場合のみ明示的に設定を行うことにより。設定項目を少なくし開発者の負担を下げようとするアプローチです。
3. 環境依存情報、JNDIアクセスのカプセル化
  EJB2.1までは、他のEJBを呼び出す場合、当然呼び出すBeanのJNDI名を知っている必要がありました。EJB 3.0ではアノテーションを用いたDependency Injectionのメカニズムにより、このような環境に依存した情報をカプセル化し開発者が直接意識をしなくても良いようになります。
4. POJO(Plain Old Java Object)/POJI(Plain Old Java Interface)による実装
  EJB 2.1まで必須だったjavax.ejb.EJBHome/javax.ejb.EJBObject/javax.ejb.SessionBean/javax.ejb.EntityBean等を実装する必要がなく、基本的に通常のクラス(POJO)/インタフェース(POJI)として開発できるようになります。
5. ドメインモデリングの容易性向上
  EJB 2.1までは、仕様上はドメインクラスとしてのCMP(Container Managed Persistence)の継承の方法を規定していませんでしたが、EJB 3.0から継承も含めドメインクラスとDBスキーマの柔軟なマッピングができる様に仕様上規定されています。
6. EJB-QLの大幅な拡張
  EJB 2.1まで不足していたEJB-QLの機能が大幅にブラッシュアップされています。
7. 例外チェックの付加軽減
  EJB 2.1までは、EJB固有の様々な例外(javax.ejb.CreateException/java.rmi.RemoteException等)を意識して開発者がコードを記述する必要がありましたが、EJB 3.0からほぼ意識をしなくて済みます。
8. コールバックインタフェース実装の負荷軽減
  EJB 2.1まではejbXXX()というEJBのライフサイクルに応じてコンテナから呼び出されるメソッドを実装する必要がありましたが、EJB 3.0ではデフォルトでは記述する必要がなく、任意(実装したいときに)アノテーションを利用して実装する事ができます。

EJB 3.0には上記のようなゴールが設定されています。今回はこれら事項からEJB3.0一般的な事項とSession Bean/Message Driven-Beanに関わることについて触れていきたいと思います。


 百聞は一見にしかず(Stateless Session Bean)

では、まず初めに単純なStateless Session BeanのサンプルをもとにEJB 2.1からEJB 3.0に変わるとどのように変化するかを確認したいと思います。サンプルはクライアントからHelloWorldBeanへはリモートアクセスで、HelloWorldBeanからMessageBeanへはローカルアクセスで呼び出すサンプルです。


  EJB 2.1 Stateless Session Beanの例

EJB 2.1ではHomeインタフェース/コンポーネント・インタフェース/Beanクラス/デプロイメント・ディスクリプタといったファイルをEJB 2.1仕様に従い、実装する必要がありました。では、簡単にHelloWorldBeanとMessageBeanとクライアントの実装例を見ていきましょう。


Home インタフェース(HelloWorldBeanの場合)

HelloWorldBeanは、クライアントからのリモートアクセスを想定しているので、javax.ejb.EJBHomeインタフェースを継承してHomeインタフェースを作成します。

【コード】HelloWorldHome.java
// HelloWorldHome.java
                                            

package oracle.ejb21;
                                          

import java.rmi.*;
import javax.ejb.*;

public interface HelloWorldHome extends EJBHome {
HelloWorld create() throws RemoteException, CreateException;
}


上記の様に、EJB 2.1以前では、クライアントからインスタンスを生成することのできるcreate()メソッドを実装する必要がありました。実際にインスタンスを生成するのはコンテナの役割なので、本当にコンテナ上で新規のインスタンスが生成されるか、コンテナにプールされているインスタンスが渡されるかは、クライアント側では意識する必要はありません。また、Stateless Session Beanの場合、引数を持たないcreate()メソッドを作成する必要がある等、細かいEJBの規則を覚えておく必要がありました。


コンポーネント・インタフェース(HelloWorldBeanの場合)

HelloWorldBeanはリモートアクセスを想定しているのでjavax.ejb.EJBObjectを継承してリモートインタフェースとして実装します。

【コード】HelloWorld.java
// HelloWorld.java
                                            

package oracle.ejb21;
                                          

import java.rmi.*;
import javax.ejb.*;

public interface HelloWorld extends EJBObject {
String sayHello(String name) throws RemoteException, CreateException;
}


コンポーネント・インタフェースでは、クライアントに公開するメソッドの定義を行う必要がありました。このHelloWorld.javaの例では、sayHello()メソッドという文字列を引数にとり、新たな文字列を返す単純なメソッドを定義しています。また、リモートインタフェースを利用する場合、RemoteExceptionを投げる様に実装する必要がありました。


Beanクラス(HelloWorldBeanの場合)

HelloWorldBeanのStateless Session Beanなのでjavax.ejb.SessionBeanを実装してBeanクラスを作成します。

【コード】HelloWorldBean.java
// HelloWorldBean.java
                                            

package oracle.ejb21;
                                          

import javax.ejb.*;
import javax.naming.*;

public class HelloWorldBean implements SessionBean {
private SessionContext _context;
private MessageLocalHome messageLocalHome;

public void ejbCreate() throws CreateException {
try {
InitialContext context = new InitialContext();
messageLocalHome = (MessageLocalHome) context.lookup(
"java:comp/env/ejb/Message");
} catch (NamingException ne) {
throw new CreateException(ne.getMessage());
}
}

public void ejbActivate() {
}

public void ejbPassivate() {
}

public void ejbRemove() {
}

public void setSessionContext(SessionContext ctx) {
_context = ctx;
}

public String sayHello(String name) throws CreateException {
MessageLocal messageLocal = messageLocalHome.create();
String message = messageLocal.getMessage();
return message + name;
}
}


HelloWorldBean内には継承をしていないので実装の必要がないようにみえるコンポーネント・インタフェースのsayHello()メソッドの実装を記述する必要があり、Homeインタフェースで定義したcreate()メソッドに対応したejbCreate()やその他SessionBeanインタフェースで定義されたコールバックメソッドであるejbXXX()メソッドを実装する必要がありました。コールバックメソッドであるejbXXX()メソッドは、EJBのライフサイクルを理解し、ライフサイクルがコンテナにより変更されたときに、実行したいロジックを実行する必要がありました。
この様にEJB 2.1までは、Homeインタフェースやコンポーネント・インタフェースの実装クラスを作成せず、それとは別にBeanクラスを作成し、かつそれぞれが関連しあうという判りにくい構成でコードを記述する必要がありました。
またsayHello()メソッド内では、ローカルインタフェースを用い、MessageBeanにアクセスをしています。そのHelloWorldBeanから呼び出されているMessageBeanの例も見ておきます。


Homeインタフェース(MessageBeanの場合)


MessageBeanはローカルアクセスを想定しているので、javax.ejb.EJBLocalHomeインタフェースを継承して実装します。


【コード】MessageLocalHome.java
// MessageLocalHome.java
                                            

package oracle.ejb21;
                                          

import javax.ejb.*;

public interface MessageLocalHome extends EJBLocalHome {
MessageLocal create() throws CreateException;
}


MessageBeanもHelloWorldBeanと同様のStateless Session Beanですので特に変わりはありません。


コンポーネント・インタフェース(MessageBeanの場合)


MessageBeanは、ローカルアクセスを想定しているので、javax.ejb.EJBLocalObjectインタフェースを継承して実装します。


【コード】MessageLocal.java
// MessageLocal.java
                                            

package oracle.ejb21;
                                          

import javax.ejb.*;

public interface MessageLocal extends EJBLocalObject {
String getMessage();
}


クライアントに公開する、ビジネスメソッドを定義します。MessageBeanはローカルインタフェースによるアクセスを想定しているので、ビジネスメソッドであるgetMessage()メソッドでRemoteExceptionは、投げられません。


Beanクラス(MessageBeanの場合)

MessageBeanクラスをStateless Session Beanなのでjavax.ejb.SessionBeanを継承して実装します。


【コード】MessageBean.java
// MessageBean.java
                                            

package oracle.ejb21;
                                          

import javax.ejb.*;

public class MessageBean implements SessionBean {
private SessionContext _context;

public void ejbCreate() {
}

public void ejbActivate() {
}

public void ejbPassivate() {
}

public void ejbRemove() {
}

public void setSessionContext(SessionContext ctx) {
_context = ctx;
}

public String getMessage() {
return "Hello ";
}
}


MessageBean内には単純な文字列を返すgetMessage()メソッドを用意しています。また、当然、javax.ejb.SessionBeanインタフェースで定義されている、コンテナがEJBインスタンスの状態を変更する時に呼び出されるejbXXX()メソッドを特に実行するロジックがなくとも実装する必要があります。


デプロイメント・ディスクリプタ

J2EE汎用のデプロイメント・ディスクリプタを作成します。EJB 2.1までは環境に依存する設定情報は、デプロイメント・ディスクリプタというXMLファイルに記述し、分離することによりコンポーネントとしての特性(ロジックと環境に依存した設定情報の分離)を実現していました。


【コード】ejb-jar.xml
<?xml version = '1.0' encoding = 'windows-31j'?>
                                            

<ejb-jar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                            

         xsi:schemaLocation
			="http://java.sun.com/xml/ns/j2ee
			 http://java.sun.com/xml/ns/j2ee/ejb-jar_2_1.xsd"
                                            

         version="2.1" xmlns="http://java.sun.com/xml/ns/j2ee">
                                            

  <enterprise-beans>
                                            

    <session>
                                            

      <description>Session Bean (Stateless)</description>
                                            

      <display-name>HelloWorld</display-name>
                                            

      <ejb-name>HelloWorld</ejb-name>
                                            

      <home>oracle.ejb21.HelloWorldHome</home>
                                            

      <remote>oracle.ejb21.HelloWorld</remote>
                                            

      <ejb-class>oracle.ejb21.HelloWorldBean</ejb-class>
                                            

      <session-type>Stateless</session-type>
                                            

      <transaction-type>Container</transaction-type>
                                            

      <ejb-local-ref>
                                            

        <ejb-ref-name>ejb/Message</ejb-ref-name>
                                            

        <ejb-ref-type>Session</ejb-ref-type>
                                            

        <local-home>oracle.ejb21.MessageLocalHome</local-home>
                                            

        <local>oracle.ejb21.MessageLocal</local>
                                            

        <ejb-link>Message</ejb-link>
                                            

      </ejb-local-ref>
                                            

    </session>
                                            

    <session>
                                            

      <description>Session Bean (Stateless)</description>
                                            

      <display-name>Message</display-name>
                                            

      <ejb-name>Message</ejb-name>
                                            

      <local-home>oracle.ejb21.MessageLocalHome</local-home>
                                            

      <local>oracle.ejb21.MessageLocal</local>
                                            

      <ejb-class>oracle.ejb21.MessageBean</ejb-class>
                                            

      <session-type>Stateless</session-type>
                                            

      <transaction-type>Container</transaction-type>
                                            

    </session>
                                            

  </enterprise-beans>
                                            

  <assembly-descriptor>
                                            

    <container-transaction>
                                            

      <method>
                                            

        <ejb-name>HelloWorld</ejb-name>
                                            

        <method-name>*</method-name>
                                            

      </method>
                                            

      <trans-attribute>Required</trans-attribute>
                                            

    </container-transaction>
                                            

    <container-transaction>
                                            

      <method>
                                            

        <ejb-name>Message</ejb-name>
                                            

        <method-name>*</method-name>
                                            

      </method>
                                            

      <trans-attribute>Required</trans-attribute>
                                            

    </container-transaction>
                                            

  </assembly-descriptor>
                                            

</ejb-jar>
                                            

                                          

EJBの論理的な名前/Homeインタフェース/コンポーネント・インタフェース/Beanクラス/トランザクション属性等のEJB仕様上で定めされた設定を行います。今回のサンプルで注目すべき点はHelloWorldBeanからMessageBeanはローカルインタフェースでアクセスするのでHelloWorldBeanの定義内でejb-local-ref要素を利用し定義している点です。

また、このejb-jar.xmlの他に、アプリケーションサーバ固有のデプロイメント・ディスクリプタを作成します。EJB仕様上網羅することのできていな各アプリケーションサーバ特有の設定情報を記述するデプロイメント・ディスクリプタです。OC4J 10g(10.1.3) Developers Preview 3を利用する場合、orion-ejb-jar.xmlというファイル名で作成します。

【コード】orion-ejb-jar.xml
<?xml version = '1.0' encoding = 'windows-31j'?>
                                            

<orion-ejb-jar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                            

     xsi:noNamespaceSchemaLocation=
"http://xmlns.oracle.com/oracleas/schema/orion-ejb-jar-10_0.xsd"
                                            

     schema-major-version="10" schema-minor-version="0">
                                            

  <enterprise-beans>
                                            

    <session-deployment name="HelloWorld" location="ejb/HelloWorld"/>
                                            

    <session-deployment name="Message" location="ejb/Message"/>
                                            

  </enterprise-beans>
                                            

</orion-ejb-jar
                                            

                                          

ここでは、アプリケーションサーバに依存する設定である、JNDI名等をしています。


EJBクライアント
HelloWorldBeanにアクセスするクライアントです。

【コード】Client.java
…
                                            

Context context = new InitialContext();
                                            

HelloWorldHome helloWorldHome = (HelloWorldHome)
                                            

PortableRemoteObject.narrow(context.lookup(
                                            

                        "HelloWorld"), HelloWorldHome.class);
                                            

HelloWorld helloWorld = helloWorldHome.create();
                                          

System.out.println(helloWorld.sayHello("Hiroyuki Sato"));

イニシャル・コンテキストを取得し、Homeオブジェクトを取得し、リモートオブジェクトを生成し、アクセスしています。EJBのコンポーネントを取得するためのファクトリとなるHomeオブジェクトを取得し、そこからリモートオブジェクトを取得するという手順です。

ここまででEJB 2.1のサンプルは終わりで、結構ものすごく簡単なロジックなのに結構な記述量になったかと思われます。


  EJB 3.0 Stateless Session Beanの例

では、EJB 2.1で作成したHelloWorldBeanとMessageBeanをEJB 3.0で記述するとどのようになるのかを見ていきましょう。


ビジネス・インタフェース(HelloWorldBeanの場合)

HellowWorldBeanのビジネス・インタフェースです。EJB 3.0の仕様内ではコンポーネント・インタフェースとは言わずビジネス・インタフェースと呼んでいます。

【コード】HelloWorld.java
// HelloWorld.java
                                            

package oracle.ejb30;
                                          

import javax.ejb.Remote;

@Remote
public interface HelloWorld {
public String sayHello(String name);
}


ビジネス・インタフェースには、EJB 2.1のコンポーネント・インタフェースと同様にクライアントに公開するビジネスロジックの定義を行います。EJB 2.1の様にjavax.ejb.EJBObjectの様なEJB仕様固有のインタフェースを実装する必要はなく、単純なPOJI(Plain Old Java Interface)として実装します。また、@Remoteという記述がありますが、これがアノテーションと呼ばれるものです。このRemoteアノテーションにより、リモートインタフェースであることを宣言しています。そして、このインタフェースにはクライアントに公開するメソッドの定義を行います。ここでは、単純なsayHello()メソッドを定義しています。


Beanクラス(HelloWorldBeanの場合)

HelloWorldBeanのBeanクラスです。

【コード】HelloWorldBean.java
// HelloWorldBean.java
                                            

package oracle.ejb30;
                                          

import javax.ejb.Stateless;
import javax.ejb.Inject;

@Stateless
public class HelloWorldBean implements HelloWorld {

@Inject
Message message;

public String sayHello(String name) {
return message.getMessage() + message;
}
}


javax.ejb.SessionBeanインタフェースEJB 2.1の様にを実装する必要はなく、単純なPOJO(Plain Old Java Object)として実装します。その代わりに先ほど作成したビジネス・インタフェース(HelloWorld)を実装し、sayHello()メソッドの実装を記述します。StatelessアノテーションによりStateless Session Beanであるのことの宣言をしています。Injectアノテーションにより、HelloWorldBeanから呼び出しているMessageBeanのDependency Injectionを行っています。Dependency Injectionにより依存が注入されたフィールドmessageはコード上では特に初期化等することなく利用する事ができます。
このようにEJB 3.0のStateless Sesison Beanの場合、ユーザに公開するメソッドを定義したビジネス・インタフェースを実装したBeanクラスを作成するという素直な実装方法になります。
このHelloWorldBeanから呼び出されているMessageBeanについても見ておきます。


ビジネス・インタフェース(MessageBeanの場合)

MessageBeanのビジネス・インタフェースです。

【コード】Message.java
// Message.java
                                            

package oracle.ejb30;
                                          

import javax.ejb.Local;

@Local
public interface Message {
public String getMessage();
}


こちらもHelloWorldBeanと同様にjavax.ejb.EJBLocalObjectの様なEJB仕様固有のインタフェースを実装する必要はなく、単純なPOJI(Plain Old Java Interface)として実装します。また、@Localという記述がありますが、これはLocalアノテーションで、ローカルインタフェースとしての定義です。


Beanクラス(MessageBeanの場合)

MessageBeanのBeanクラスです。

【コード】MessageBean.java
// MessageBean.java
                                            

package oracle.ejb30;
                                          

import javax.ejb.Stateless;

@Stateless
public class MessageBean implements Message {

public String getMessage() {
return "Hello ";
}
}


こちらもHelloWorldBeanと同様にjavax.ejb.SessionBeanインタフェースを実装する必要はなく、単純なPOJO(Plain Old Java Object)として実装します。そして先ほど作成したビジネス・インタフェース(Message)を実装しています。またStatelessアノテーションによりStateless Session Beanであるのことの宣言をしています。
このようにEJB 3.0では、ビジネス・インタフェースを作成し、その実装クラスであるBeanクラスを作成するという素直な実装方法になります。また、EJB固有のインタフェースを利用しなくてすみ、EJB固有のException等のハンドリングもありませんので、開発者にとって視認性の高いコードになります。
ここまででEJB 3.0仕様でのHelloWorldBean/MessageBeanはおわりです。デプロイメント・ディスクリプタは、アノテーションを利用する事により開発時に作成する必要はありません。EJB 3.0のEDR2時点では、デプロイメント・ディスクリプタを作成する事もできよう様に記述されており作成されていた場合、アノテーションの設定を上書きする様な事を想定している様です。


EJB クライアント

HelloWorldBeanのクライアントです。

【コード】Client.java
Context context = new InitialContext();
                                            

HelloWorld helloWorld =
 (HelloWorld)context.lookup("java:comp/env/ejb/HelloWorld");
                                            

      System.out.println(helloWorld.sayHello(args[0])
                                          

EJB 3.0では、イニシャルコンテキスを生成しルックアップするといきなりリモートオブジェクトを取得することができます。Homeオブジェクトを扱う必要がありません。

ここまで見てきたようにEJB 3.0ではアノテーションを利用する事により開発者が作成するコードが非常に簡略化されます。またこのサンプルではHelloWorldBeanからMessageBeanの呼び出しにDependency Injectionも利用されています。このアノテーションとDependency InjectionはEJB 3.0を利用する上では重要な概念ですので少しまとめておきましょう。

 アノテーション

アノテーションとは、Javaプログラムに対し、動作上影響のあるメタデータを記述する事のできる機能です。前述のEJB 3.0のコードの@で始まるものがアノテーションになります。
EJB 3.0ではアノテーションを利用する事により、EJB 2.1まで必要であったが、開発者の負担となっていたEJB固有のインタフェース(javax.ejb.EJBObject/javax.ejb.SessionBean等)やデプロイメント・ディスクリプタ等の開発の負荷を排除することができます。
アノテーション自身は単なるメタデータであり、それを読み取り新たなコードを生成したり、ロジックを追加したりするのは、フレームワークやツール側で行うことになります。つまりEJB 3.0はアノテーションを解釈しEJB固有の振る舞い(ロジック)を与えるフレームワークといえるかと思います。
先ほどの@Statelessなどのアノテーションは、EJB 3.0仕様上定義されたものであり、開発者はそれを利用するだけです。アノテーション自身を開発者の方が定義することは少ないと思われますが、EJB 3.0を理解する上である程度は知っておいたほうが良いと思われるので、今回は簡単なアノテーションを作成してみたいと思います。


  簡単なアノテーションの作成

では、アノテーションとはどのようなものなのかを簡単なアノテーションのサンプルを作成しながらご紹介します。
まずは、アノテーション・タイプの定義を行います。アノテーション・タイプとはアノテーションを定義した型です。アノテーション・タイプとアノテーションの関係は、クラスとそこから生成されるオブジェクトの関係に似ていいます。ここでは独自のMyTaskアノテーションを作成して利用してみます。まずはMyTaskアノテーションのアノテーション・タイプの定義してみます。

【コード】MyTask.java
// MyTask.java
                                            

package sample;
                                          

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface MyTask {

TaskName name() default TaskName.COOK;
}


アノテーション・タイプの定義は、通常のJavaコードとして行われますが、@interfaceという特殊なインタフェースとして実装します。また、アノテーションに付加する事のできる属性をメンバと呼びます。例えば上記のアノテーション・タイプを利用する場合@MyTask(name=”TaskName.BOIL”)と指定する事ができます。メンバは、メソッドとして定義しデフォルト値を設定することができます。ここでは、メンバに対応したname()メソッドを用意し、列挙型のTaskNameの戻り値とし、デフォルト値はTaskName.COOKをデフォルト値としています。このデフォルト値を設定することによりEJB 3.0仕様のゴールで説明した“Configuration by Exceptionアプローチ”を実現しています。また、この戻り値は、アノテーションのメンバとして指定することのできる値を表し、今回の場合は列挙型のTaskNameを以下のように定義してみます。

【コード】TaskName.java
// TaskName.java
                                            

package sample;
                                            

                                             

public enum TaskName {
                                            

    COOK,
                                            

    BAKE,
                                            

    BOIL
                                            

}
                                            

                                          


J2SE5.0から導入された列挙型としてCOOK、BAKE、BOILの3つの定数を定義しています。つまりMyTaskアノテーションのメンバとしてCOOK/BAKE/BOILの3種類を指定する事ができます。

また、このMyTaskアノテーション・タイプの定義であるMyTask.javaの中にRetentionアノテーションがありました、これはアノテーションをアノテートするためのアノテーションです?。つまりアノテーション定義のためにアノテーションを利用している(できる)と言うことです。
Retentionアノテーションは、アノテーションがどこで解釈されるのかをメンバとして指定します。メンバとして指定できる値は、列挙型のRetencionPolicyに3種類の定数が定義されています。

定数 概要
SOURCE アノテーションは、コンパイル時に無視をされる。つまり、ソースだけで有効。
CLASS アノテーションは、コンパイル時に解釈されクラスファイルに埋め込まれるが、JVMにクラスがロードされてから解釈はできない。(デフォルト)
RUNTIME アノテーションは、コンパイル時に解釈されクラスファイルに埋め込まれ、JVMにクラスがロードされてから解釈できる。

では、EJB 3.0仕様で定義されているアノテーション・タイプのRetentionPolicyは上記のうちどれかというと、EDR2時点では、全てRUNTIMEとして定義されています。つまり、クラスをJVM上でロードして解釈可能であるアノテーション・タイプとして定義されています。

これでアノテーション・タイプの定義は終わりです。

では、このアノテーションを利用するサンプルを見ていきます。アノテーションは、パッケージ・クラス・メソッド・フィールド等に指定することができます。以下のサンプルはメソッドに対してMyTaskアノテーションを適用している例です。

【コード】Client.java

// Client.java
                                            

package sample;
                                          

public class Client {
public Client() {
}

@MyTask
public void foo() {
System.out.println("foo() called");
}

@MyTask(name = TaskName.BAKE)
public void bar() {
System.out.println("bar() called");
}

@MyTask(name = TaskName.BOIL)
public void hoge() {
System.out.println("hoge() called");
}
}


MyTaskアノテーションはRetentionPolicyとしてRUNTIMEを指定していましたので、このソースをコンパイルするとクラスファイル内にMyTaskアノテーションの情報が格納され、JVMにロードしてからアノテーションを参照することができます。

では、クラスをロードして格納されたアノテーションを表示するサンプルを作ってみましょう。

【コード】ReflectionAnnotation.java
// ReflectionAnnotation
                                            

package sample;
                                          

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;

public class ReflectMethodAnnotation {

public static void main(String args[]) {

try {
Class cls = Class.forName("sample.Client");

AnnotatedElement[] annotatedElements = cls.getDeclaredMethods();

for (AnnotatedElement annotatedElement : annotatedElements) {

System.out.print(annotatedElement +" は ");
Annotation[] annotations = annotatedElement.getAnnotations();
if (annotations.length != 0) {
System.out.print("アノテートされており、\n\tアノテーションは\n");
for (Annotation annotation : annotations) {
System.out.println("\t" + annotation);
}
System.out.println("\tです。");
} else {
System.out.println("アノテートされていません。\n");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}


このサンプルでは、独自に定義してMyTaskアノテーションを利用しているClient.javaをコンパイルしたクラスファイルからアノテーションを取得するサンプルです。実行結果は以下の様になります。

public void sample.Client.bar() は アノテートされており、
アノテーションは
@sample.MyTask(name=BAKE)

です。

public void sample.Client.foo() は アノテートされており、
アノテーションは
@sample.MyTask()

です。

public void sample.Client.hoge() は アノテートされており、
アノテーションは
@sample.MyTask(name=BOIL)

です。

public void sample.Client.hogeFooBar() は アノテートされていません。


ちなみに、MyTask.javaのRetentionPolicyをデフォルトのCLASSにした場合、実行結果は以下のようになり、クラスをJVM上にロードしてからでは、解釈ができない事がわかります。

public void sample.Client.bar() は アノテートされていません。


public void sample.Client.foo() は アノテートされていません。


public void sample.Client.hoge() は アノテートされていません。


public void sample.Client.hogeFooBar() は アノテートされていません。



  Statelessアノテーション

では、EJB 3.0仕様で定義されている、Statelessアノテーションの定義を見ておきましょう。

【コード】Stateless.java
@Target(TYPE) @Retention(RUNTIME)
public @interface Stateless {
String name() default "";
}

上記アノテーション・タイプの定義内には今まで出ていていないTargetアノテーションが利用されています。Targetアノテーションは、アノテーションを利用できる場所を定義しています。指定できる値は、列挙型のElementType内の定数として定義されています。定義されている8種類の定数は以下のようになります。

定数 概要
ANNOTATION_TYPE アノテーション・タイプに適用可能
CONSTRUCTOR コンストラクタに適用可能
FIELD フィールドに適用可能(列挙型定数も含む)
LOCAL_VARIABLE ローカル変数に適用可能
METHOD メソッドに適用可能
PACKAGE パッケージに適用可能
PARAMETER パラメータに適用可能
TYPE クラス、インタフェース(含むアノテーション・タイプ)もしくは、列挙型

Statelessアノテーション・タイプの定義内ではTargetアノテーションのメンバとしてTYPEが指定されているので、クラスにアノテーションを利用するとこができ、実際前述のサンプル内でも、以下のようにクラスに対してStatelessアノテーションを利用しています。

【コード】HelloWorldBean.java
// HelloWorldBean.java

@Stateless
public class HelloWorldBean implements HelloWorld {



 Dependency Injection

EJB 3.0の重要な概念としてDependency Injectionがあります。Dependency Injection(依存性注入)は、Inversion of Control(制御の逆転)とも言われます。コンポーネントで実行環境であるコンテナに登録されたコンポーネントを呼び出すのではなく、コンテナがコンポーネント内にコンテナに登録された他のコンポーネントの依存性を注人する為に、Dependency InjectionもしくはInversion of Controlと呼ばれています。
例えばAコンポーネントがBコンポーネントを利用している場合、AがBへの依存性を解決し、インスタンス変数に保持するのではなく、外部から依存性を注入する事により解決しようというものです。EJB 3.0でいえば典型的なものとして他のBeanクラスの呼び出しがあります。
前述の簡単なサンプルでもDependency Injectionが利用されていましたので、そのサンプルを見てみましょう。


  EJB 2.1

EJB 2.1では、依存性を解決するために、開発者自身が他のコンポーネントを呼び出す為のコードを記述し、デプロイメント・ディスクリプタ内に設定する必要がありました。

【コード】HelloWorldBean.java
// HelloWorldBean.java

                                          

…
                                          

public class HelloWorldBean implements SessionBean {
                                          

    …
                                          

    private MessageLocalHome messageLocalHome;
                                        

public void ejbCreate() throws CreateException {
try {
InitialContext context = new InitialContext();
messageLocalHome = (MessageLocalHome) context.lookup(
"java:comp/env/ejb/Message");
} catch (NamingException ne) {
throw new CreateException(ne.getMessage());
}
}

public String sayHello(String name) throws CreateException {
MessageLocal messageLocal = messageLocalHome.create();
String message = messageLocal.getMessage();
return message + name;
}
}


呼び出し側のHelloWorldBeanでは予めコンテナに登録されているMessageBeanのJNDI名を利用し呼び出すことにより、コンポーネント間の依存性を解決していました。かつ上記の例の様にローカルインタフェースを利用した呼び出しの場合、EJB汎用のデプロイメント・ディスクリプタejb-jar.xmlにローカルインタフェースの定義を行わなければなりませんでした。

【コード】ejb-jar.xml
…
                                          

  <enterprise-beans>
                                          

    <session>
                                          

      ..
                                          

      <ejb-local-ref>
                                          

        <ejb-ref-name>ejb/Message</ejb-ref-name>
                                          

        <ejb-ref-type>Session</ejb-ref-type>
                                          

        <local-home>oracle.ejb21.MessageLocalHome</local-home>
                                          

        <local>oracle.ejb21.MessageLocal</local>
                                          

        <ejb-link>Message</ejb-link>
                                          

      </ejb-local-ref>
                                          

    </session>
                                          

…
                                        

上記の様に、ローカルインタフェースの定義を行う必要があります。アプリケーションサーバによってはアプリケーションサーバ固有のデプロイメント・ディスクリプタにも設定を記述しなければなりません。


  EJB 3.0

EJB3.0 ではInjectアノテーションを利用して、依存性をコンテナにより注入させます。

【コード】HelloWorldBean.java
// HelloWorldBean.java
                                          

…
                                        

@Stateless
public class HelloWorldBean implements HelloWorld {

@Inject
Message message;

public String sayHello(String name) {
return message.getMessage() + name;
}
}


上記のようにInjectアノテーションを記述しマークしておくだけで、コンテナが依存関係を自動的に解決しオブジェクトを注入します。Dependency Injectionを利用すると、コンポーネント間がより疎結合になり、かつ副次的な効果としてコードが非常にシンプルになります。

EJB 2.1以前は、コンポーネントの呼び出し、データソースの取得、環境変数の取得等を予めJNDIのネームスペースに登録しておき、利用するときに開発者が呼び出すことで依存関係を作成していましたが、EJB 3.0からは、EJB 2.1までのJNDIに登録するようなリソースを、アノテーションを利用したDependency Injectionにより解決する方法も(従来どおりJNDIネームスペースから取得することも可能)提供し、コンポーネント間や、コンポーネントと設定間の疎結合を実現しています。


 Stateless Session Bean: Requirement

簡単なStateless Session BeanをもとにEJB 3.0を眺めてきましたが、改めてEJB 3.0仕様内でのStateless Session Beanの主な要求を見てみたいと思います。

1. Businessインタフェース
 
Businessインタフェースは必須
通常のインタフェース(POJI: Plain Old Java Interface)として実装し、javax.ejb.EJBObjectやjavax.ejb.EJBLocalObjectインタフェースは実装しない
2. Homeインタフェース
 
Homeインタフェースは必要なし
3. Beanクラス
 
Statelessアノテーション、もしくはデプロイメント・ディスクリプタで定義
通常のクラス(POJO: Plain Old Java Object)として実装しjavax.ejb.SessionBeanインタフェースは実装しない
4. CallBack
 

以下、2種類のライフサイクル・イベント・コールバックをサポート

PostConstruct、PreDestroy
5. Dependency Injection
 
リソースや他のオブジェクトの参照などをDependency Injectionメカニズムを利用して取得する場合、予めコンテナはそれら参照を注入しておかなければならない
6. Intercepter
 
Intercepterメソッド/クラスのサポート

EJB 3.0仕様で定義されているStateless Sesison Beanに対する要求は上記の様になります。


 Stateful Sesison Bean: Requirement

続いてEJB 3.0仕様でのStateful Session Beanの主な要求も見ておきましょう。

1. Businessインタフェース
 
Businessインタフェースは必須
通常のインタフェース(POJI: Plain Old Java Interface)として実装し、javax.ejb.EJBObjectやjavax.ejb.EJBLocalObjectインタフェースは必要なし
2. Homeインタフェース
 
Homeインタフェースは必要なし
3. Beanクラス
 
Statefulアノテーション、もしくはデプロイメント・ディスクリプタで定義
通常のクラス(POJO: Plain Old Java Object)として実装しjavax.ejb.SessionBeanインタフェースは実装しない
4. CallBack
 

以下、4種類のライフサイクル・イベント・コールバックをサポート

PostConstruct、PreDestroy、PostActivate、PrePassivate
5. Dependency Injection
 
リソースや他のオブジェクトの参照などをDependency Injectionメカニズムを利用して取得する場合、予めコンテナはそれら参照を注入しておかなければならない
6. Intercepter
 
Intercepterメソッド/クラスのサポート

Stateless Session Bean/Stateful Sesison Bean共にいままで見てきた内容で理解できると思いますが、CallBackとIntercepterに関してはまだ見ていませんの少し触れておきたいと思います。


 コールバック

前述の例のようにEJB 2.1までは、EJBのライフサイクルが変化した時にコンテナにより呼び出されるejbXXX()というメソッドをBeanクラスで実装する必要がありました。EJB3.0ではデフォルトでは、EJBのライフサイクルの変化により呼び出されるメソッドを実装する必要がなくなり、任意で実装することが可能です。
では、どのようにコールバックの仕組みを実現するかと言うとアノテーションで実現しています。以下は、Stateful Session Beanでコールバックを利用したときのサンプルです。

【コード】CarBean.java
// CarBean.java
                                          

package oracle.ejb30;
                                        

import java.util.ArrayList;
import java.util.Collection;

import javax.ejb.PostConstruct;
import javax.ejb.Stateful;

@Stateful
public class CartBean implements Cart {
private ArrayList items;

@PostConstruct
public void initialize() {
items = new ArrayList();
}

public void addItem(String item) {
items.add(item);
}

public void removeItem(String item) {
items.remove(item);
}

public Collection getItems() {
return items;
}
}


このサンプルではPostConstructアノテーションを利用しています。これはEJB 2.1までのejbCreate()メソッドに対応しています。PostConstructアノテーションを利用する事によりコンテナによりCarBeanのインスタンスが生成される時にinitialize()メソッドが呼ばれ、ArrayListが初期化されます。この様にEJB 3.0ではデフォルトでコールバックメソッドを実装する必要はなく、必要なときに記述する様に開発者の負荷を軽減しています。


 インターセプタ

Interceptorは、ビジネスメソッドの呼び出しをインターセプトし、追加のロジックを埋め込むことのできる機能です。Interceptorは、AroundInvodeアノテーションにより記述でき、Interceptor/Interceptorsアノテーションを利用して別クラスに記述することも可能です。
それではサンプルを見ていきます。

【コード】HelloWorldBean.java
// ProfilingIntercepter.java
                                          

package oracle.ejb30;
                                        

import javax.ejb.Stateless;
import javax.ejb.Interceptor;
import javax.ejb.Interceptors;
import javax.ejb.AroundInvoke;
import javax.ejb.InvocationContext;

@Stateless
@Interceptor(value="oracle.ejb30.ProfilingInterceptor")
//@Interceptors({@Interceptor(value="oracle.ejb30.ProfilingInterceptor"),
@Interceptor(value="oracle.ejb30.ProfilingInterceptor2")})

public class HelloWorldBean implements HelloWorld {

public String sayHello(String name) {
System.out.println("sayHello() called");
return "Hello " + name;

}

@AroundInvoke
public Object checkPermission(InvocationContext ctx) throws Exception {
System.out.println("checkPermission() called");

if (!ctx.getEJBContext().getCallerPrincipal().getName().equals("jazn.com/admin")) {
throw new SecurityException("Caller: '"
+ ctx.getEJBContext().getCallerPrincipal().getName() +
"' does not have permissions for method " + ctx.getMethod());
}
return ctx.proceed();
}
}


この例では、AroundInvokeアノテーションを利用して、ビジネス・メソッドであるsayHello()がクライアントにより呼び出されたときに、インターセプトしcheckPermission()メソッドを呼び出します。chackPermission()メソッドは、メソッド呼び出しのユーザを識別し、権限がないユーザで呼び出されたときにはSecurityExceptionを投げるという簡易的なセキュリティチェックの機能を提供しています。またcheckPermittion()メソッドの引数はInvocationContextが渡されます。このInvocationContextはメソッドの呼び出しの情報をカプセル化したオブジェクトであり、メソッド名やパラメータを取得することができます。上記の例では、InvocationContextからEJBContextを取得して、メソッドを呼び出したプリンシパルを取得してセキュリティチェックを行い、proceed()メソッドによりsayHello()メソッドの処理を継続させています。
また、Interceptorアノテーションを利用して、外部クラスにインターセプト時に実行するロジックを記述する事もできます。コメントアウトされているInterceptorsアノテーションを利用すれば複数の外部クラスを指定することもできます。

以下は、Intersepterアノテーションにより、指定されたインターセプタクラスです。

【コード】ProfilingInerceptor.java
// ProfilingInterceptor.java
                                          

package oracle.ejb30;
                                        

import javax.ejb.AroundInvoke;
import javax.ejb.InvocationContext;

public class ProfilingInterceptor {

@AroundInvoke
public Object profile(InvocationContext ctx) throws Exception {
System.out.println("profile() called");
long startTime = 0;
long endTime = 0;
try {
startTime = System.currentTimeMillis();
return ctx.proceed();
} finally {
endTime = System.currentTimeMillis();
System.out.println("Method Name: " + ctx.getMethod() +
" Execution Time : " + (endTime - startTime) + " ms");
}
}
}

sayHello()メソッドが呼び出されたときの実行結果は以下のようになります。

05/04/04 11:17:55 profile() called
05/04/04 11:17:55 checkPermission() called
05/04/04 11:17:55 sayHello() called
05/04/04 11:17:55 Method Name: public java.lang.String
oracle.ejb30.HelloWorldBean.sayHello(java.lang.String) Execution Time : 0 ms

この様にインターセプタを利用するとメソッドの前後にAOP(Aspect Oriented Programing)的?にロジックを追加することが可能になります。


 Message Driven-Bean: Requirement

今回はもうひとつMDBも見ておきましょう。EJB 3.0仕様でのMDBの要求は以下のようになります。

1. Businessインタフェース
 
Beanで利用するメッセージ・タイプが定義されたメッセージ・リスナーインタフェース
JMSを利用する場合、javax.jms.MessageListenerインタフェースを利用
2. Beanクラス
 
MessageDrivenアノテーション、もしくはデプロイメント・ディスクリプタで定義
javax.ejb.MessageDrivenBeanインタフェースは実装しない
3. CallBack
 
以下、2種類のライフサイクル・イベント・コールバックをサポート
PostConstruct、PreDestroy
4. Dependency Injection
 

リソースや他のオブジェクトの参照などをDependency Injectionメカニズムを利用して取得する場合、予めコンテナはそれら参照を注入しておかなければならない

5. Intercepter
 
Intercepterメソッド/クラスのサポート

MDBの要求に関しては今までの内容でほぼ理解できるのではないかと思われます。


  Message Driven-Beanの例

それでは、Message Driven-Bean(以下MDB)の例を見ていきましょう。メッセージとしてJMSを利用したMDBのサンプルです。

【コード】MessageLogger.java
// MessageLogger.java
                                          

package oracle.ejb30.mdb;
                                        

import javax.ejb.MessageDriven;
import javax.ejb.MessageDrivenContext;
import javax.ejb.ActivationConfigProperty;
import javax.ejb.Inject;
import javax.jms.*;
import java.util.*;

@MessageDriven(
activationConfig = {
@ActivationConfigProperty(propertyName="connectionFactoryJndiName",
propertyValue="jms/TopicConnectionFactory"),
@ActivationConfigProperty(propertyName="destinationName",
propertyValue="jms/demoTopic"),
@ActivationConfigProperty(propertyName="destinationType",
propertyValue="javax.jms.Topic"),
@ActivationConfigProperty(propertyName="messageSelector",
propertyValue="RECIPIENT = 'MDB'")
}
)
public class MessageLogger implements MessageListener {

@Inject MessageDrivenContext mc;

public void onMessage(Message message) {
System.out.println("onMessage() - " + message);
try {

String subject = message.getStringProperty("subject");
String inmessage = ((TextMessage)message).getText();

System.out.println("Message received\n\tDate: " + new java.util.Date()
+ "\n\tSubject: " + subject + "\n\tMessage: " + inmessage + "\n");
System.out.println("Creating Timer a single event timer");

} catch (Throwable ex) {
ex.printStackTrace();
}
}
}


MDBに関していえば、それ程大きな変更はありませんが、EJB 2.1までのデプロイメント・ディスクリプタで定義していたコネクション・ファクトリやディスティネーションの情報もアノテーションとして定義している点が、目に留まる変化かと思われます。


 おわりに

今回は、EJB3.0のStatelss/Stateful/Message Driven Beanを中心に見てみました。次回はEJB 3.0の大きな目玉である、CMP(Container Managed Persistence)のEntity Beanを中心に見ていきます。


【参考】

「Enterprise JavaBeans 3.0 Specification」
http://java.sun.com/products/ejb/