X

An Oracle blog about WebLogic Channel

[連載] WebLogic Server 12cでJava EE 6を動かしてみよう!(8) CDI第1回

Guest Author
実践! Java EE 6 ロゴ


本連載では、サンプル・アプリケーションの開発を通じてJava Enterprise Edition 6 (Java EE 6)の仕様とその魅力をお伝えすることを目的としています。今回から2回にわたって、Contexts and Dependency Injection for the Java EE platform(CDI)をとりあげます。DIの仕組みは、Java EE 5から一部のJava EEコンポーネントで利用できましたが、Java EE 6からは、より汎用的なCDI(JSR 299)が取り込まれました。CDIは、コンテナ上で管理されているコンテキストに紐付いたDIの仕組みであり、Java EEアプリケーションで利用する上で、一般的なDIフレームワーク以上のメリットがあります。今回はCDIがどのような機能を提供するのか、実際にサンプルコードを動かしながら解説していきます。(日本オラクル Fusion Middleware 事業統括本部 二條智文)


CDI概要


依存性注入(Dependency Injection=DI)というソフトウェア・デザインパターンは、今日ではすでに一般的な用語として定着しており、本記事をご覧の皆様は、実際にプロジェクトで利用されていることが多いと思います。特にSpring FrameworkやGoogle GuiseといったDIフレームワークを活用し、コンポーネントを疎結合とし、チーム開発の効率化やメンテナンス性の向上など、実践レベルで使いこなしているという方も多いのではないでしょうか?


Java EEに着目するとこのようなDIの仕組みはJava EE 5から仕様として取り込まれました。ただ、Java EE 5のDI機能は、限られたコンポーネントのみで、かつDI対象となるコンポーネントの種類ごとに異なるDIの記述方法をしなければならないなど、非常に限定的な仕組みでした。

Java EE 6からは、より汎用的なDIの仕組みとしてCDI(JSR299)が仕様に取り込まれました。CDIは、Java EE 6仕様の中でも、Java EEアプリケーションをコードレベルから構造を改良し、開発に大きな変化をもたらす最も有力な仕様だと考えています。
CDI(Contexts and Dependency Injection)はその名前が表すとおり、次の2つが主要な機能となります。
  • Contexts
    • ステートフル・コンポーネントのライフサイクルや相互の関連性を、明確に定義され拡張可能なライフサイクル・コンテキストにバインドする

  • Dependency Injection
    • 型安全な方法でコンポーネントをアプリケーションに注入でき、どの実装を注入するのかを規約に基づいて選択する




重要なのは、CDIというのは単なる依存性注入の仕組みではなく、注入したオブジェクトを、コンテナが管理するライフサイクル・コンテキストにバインドするという点です。これにより、そのオブジェクトの下に挙げたような細かい利用方法を意識しなくても、利用するオブジェクトの型やセマンティクスを意識するのみで利用することができるようになります。
  • オブジェクトのライフサイクルをどう考えればいいのか?
  • オブジェクトのスレッドモデルは?
  • オブジェクトの他のクライアントは意識しなくていいのか?
  • オブジェクトを明示的に破棄する必要があるのか?
  • オブジェクトを使い回す場合にリファレンスをどこに保持するのか?
  • 異なるオブジェクト間で、このオブジェクトをどうやって共有するのか?



コンテナがコンテキストに紐付けてライフサイクルを管理するオブジェクトをCDIではBeanという一般的な用語で定義しています。Java EE 6より前は、Beanという用語の定義はあいまいで、Java EE仕様の中にもいくつかのBean定義がありました(EJB Beans、JSF Managed Beansなど)。また、その他のサードパーティ製フレームワークでもBeanは様々な定義で使われていました(SpringのBeanなど)。CDIでは、セッションBean、マネージドBeanやJava EEリソースなど様々なコンポーネントがBeanとなります。



その他にも、CDIには以下の機能も定義されています。
  • Expression Language(EL)と統合され、JSFページやJSPページから直接利用できる
  • 注入されたコンポーネントのデコレート機能
  • インターセプターをコンポーネントに型安全にバインドする機能
  • イベント通知モデル
  • 標準的なスコープ(request、session、application)に加えて、Java Servlet仕様で定義されているweb conversationスコープを利用可能
  • サードパーティフレームワークをJava EE 6に統合できるService Provider Interface(SPI)を提供



なお、CDIはJSR 299で定義されており、以下のURLから仕様書をダウンロードできます。

http://jcp.org/aboutJava/communityprocess/final/jsr299/index.html




それでは、実際にコードを書いて、CDIの動作を体験していきましょう。

このブログの内容


CDIの第1回では、CDIの各種機能を実際にシンプルなアプリケーションを用いて体験します。第2回目では、過去に連載した他の仕様と組み合わせて利用する方法をいくつかご紹介いたします。

第1回では以下のような内容を扱います。
  • 初めてのCDI
    • CDIでHello
    • スコープ定義
    • @Qualifierの使い方
    • @Producesの使い方
    • @Alternativeの使い方
  • インターセプターの利用
  • デコレータの利用

アプリケーション開発の準備


本連載では、Oracle Enterprise Pack for Eclipse(OEPE)を使用してサンプル・アプリケーションの開発を進めます。OEPEやWebLogic Server 12cのインストールおよび設定が終わっていない方は、記事「 [連載] WebLogic Server 12cでJava EE 6を動かしてみよう!(1) 概要」を参考にしてください。

「New Dynamic Web Project」画面
「New Dynamic Web Project」画面
(クリックして拡大)
OEPE起動後にまず今回のアプリケーション用のEclipseのプロジェクトを作成します。「New」->「Dynamic Web Project」を選択します。「New Dynamic Web Project」ウィンドウが表示されたら、「Project Name」としてここでは「CDISample」を入力し、「Finish」ボタンをクリックします。




デフォルトでは、CDI機能はオフとなっています。CDIを有効化するには、beans.xmlというマーカーファイルを、モジュールに含める必要があります。このファイルの中身は空で構いません。Webアプリケーションの場合は、WEB-INF直下にbeans.xmlファイルを配置します。
CDISample -> WebContent -> WEB-INFで右クリックしNew->Fileを選択し、File nameに”beans.xml”と指定してファイルをWEB-INF直下に作成してください。これで、Webアプリケーションの実行時にCDIが有効になります。

はじめてのCDI

「New Java Interface」画面
「New Java Interface」画面
(クリックして拡大)
まずは、最もシンプルな例として、Servletから”Hello”を表示するシンプルなプログラムを作成してみましょう。
まずは、Helloインタフェースを定義します。CDISample->Java Resources->srcを右クリックして、New->interfaceを選択し、右のように入力してください。


作成したインタフェースにsayHelloメソッドを定義します。
Hello.java
package cdi;
public interface Hello {

public String sayHello(String name);
}


「New Java Class」画面
「New Java Class」画面
(クリックして拡大)
次に、Helloの実装クラスを作成します。CDISample->Java Resources->srcを右クリックして、New->classを選択し、右のように入力し、Finishボタンを押します。


作成されたHelloInEnglish.javaクラスのsayHelloメソッドを以下のように記述します。
HelloInEnglish.java
package cdi;
public class HelloInEnglish implements Hello, java.io.Serializable {

@Override

public String sayHello(String name) {

return "Hello " + name + "!";

}
}


「Create Servlet」画面
「Create Servlet」画面
(クリックして拡大)
次に、Hello Beanを使用するServletを作成します。CDISample->Java Resources->srcを右クリックして、New->Servletを選択し、右のように入力し、Finishボタンを押します。


作成されたServletに以下のように赤字部分の記述を追記します。
TestServlet.java
package cdi;
… 略 …
import javax.inject.Inject;
@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
// Helloを注入
@Inject Hello hello;

… 略 …
// doGetに処理を追加
protected void doGet(HttpServletRequest request,

HttpServletResponse response)

throws ServletException, IOException {// Hello.sayHelloをコールした結果を出力
response.setCharacterEncoding("Shift_JIS");
PrintWriter out = response.getWriter();
out.print("<html><head><title>CDI Test</title></head><body>");
out.print("<h2> " + hello.sayHello("Duke") + "</h2>");
out.print("</body></html>");

}
… 略 …
}


「Run on Server」画面
「Run on Server」画面
(クリックして拡大)
CDIでは@Injectアノテーションにより、インタフェースに適切な実装クラスが注入されます。注入されるBeanはデプロイ時にチェックされます。
それでは、TestServletを実行してみましょう。TestServlet.javaを右クリックし、Run As -> Run On Serverを選択します。Run On Server画面で、”Oracle WebLogic Server 12c(12.1.1)”を選択して、Finishボタンをクリックします。

「Hello Duke」画面

「Hello Duke」画面
(クリックして拡大)
WebLogic Serverが起動するまで、しばらく待ちます。次のように”Hello Duke!”が表示されます。




TestServlet.javaのHello Beanを利用しているコードのみに注目すると、Helloという型のみを知っていれば良く、その注入にも@Injectアノテーションを付与しておくだけです。実際に注入される実装クラスは実行時に適切に選択されます。
TestServlet.javaの抜粋
// Helloを注入
@Inject Hello hello;
…略…
out.print("<h2> " + hello.sayHello("Duke") + "</h2>");

スコープの指定


CDIでは、Beanを適切なコンテキストに紐付けて管理するために、スコープという概念を利用して定義します。Beanにスコープを定義することで、スコープに沿ったライフサイクルや可視範囲がコンテナにより制御されます。

Java EE 6では、以下の組み込みの4つのスコープと、@Dependentという特殊な1つのスコープ定義が定義されています。






































スコープアノテーション説明
Request@RequestScoped単一リクエストごとのスコープ
Session@SessionScopedセッションごとのスコープ。同一セッションであれば、複数リクエストから共有される
Application@ApplicationScopedWebアプリケーション全体で共有されるスコープ
Conversation@ConversationScopedJSFアプリケーションにおいて、特定のページ遷移の間で保持されるスコープ
Dependent@Dependentデフォルトのスコープ。Beanクライアント側のスコープを自動的に引き継ぐ。

デフォルトのスコープは@Dependentで、そのBeanを注入したClient側のBeanと同じライフサイクルを取ることを意味します。先ほど作成したHello Beanでは、スコープの指定をしていないため、デフォルトの@Dependentが適用され、これは、Hello インスタンスがTestServletのライフサイクルと同じとなることを意味します。


それでは、Hello Beanに対して、スコープの影響を調べるために、HelloInEnglish.javaに以下のようにsayHelloメソッドが呼び出された回数を保持して、返すように実装を変更します。

HelloInEnglish.java
package cdi;
public class HelloInEnglish implements Hello {private int count;
@Override
public String sayHello(String name) {count++;
return "Hello " + name + "!: call count: " + count;
}
}



この状態で、TestServletを実行してみましょう。スコープ定義はしていないので、デフォルトで@Dependentとなり、Hello BeanのライフサイクルはTestServletと同等となります。よって、この場合は、リクエストやセッションなどにかかわらずに呼び出し回数が増加します。もし、お手持ちの環境に複数のブラウザがあれば、以下のURLで複数のブラウザからアクセスしてみてください。


http://localhost:7001/CDISample/TestServlet



アクセスするたびに、呼び出し回数がカウントアップされることが分かります。



それでは次に、以下のようにHelloInEnglishe.javaに@RequestScopedを付与してみます。
HelloInEnglish.java
package cdi;
import javax.enterprise.context.RequestScoped;
@RequestScoped
public class HelloInEnglish implements Hello {
private int count;
@Override
public String sayHello(String name) {
count++;
return "Hello " + name + "!: call count: " + count;
}
}



同様にTestServletを実行して、複数のブラウザから何度かアクセスしてみましょう。@RequestScopedの場合は、リクエストごとにライフサイクルが終わるため、呼び出し回数は常に1回となります。




最後に、以下のように@SessionScopedを付与してみます。なお、SessionScopeはPassivating Scopeであるため、java.io.Serializableをimplementsに指定する必要があります。Passivating Scopeとはセッションのような長いライフサイクルにおいて、セッションがインアクティブになっている状態のときに、メモリを有効活用するために、適宜Beanをメモリからディスクなどに退避することが可能なスコープ定義です。このため、Beanをシリアライズ可能にしておく必要があります。(これをPassivation capable beanと呼びます。)
HelloInEnglish.java
package cdi;
import javax.enterprise.context.SessionScoped;
@SessionScoped
public class HelloInEnglish implements Hello, java.io.Serializable {
private int count;
@Override
public String sayHello(String name) {
count++;
return "Hello " + name + "!: call count: " + count;
}
}



TestServletを実行し、複数のブラウザから何度かアクセスしてみましょう。@SessionScopedが付与されている場合は、ブラウザごとのセッションに紐付いてライフサイクルが管理されるため、ブラウザごとに呼び出し回数が増加します。


これで、スコープごとの挙動の違いが確認できました。(Conversation Scopeは少し複雑なため、ここでは説明は割愛します。)

ここで注目すべきなのは、Hello Beanのクライアント側(TestServlet)では、ライフサイクルやその可視範囲(リクエストごとやセッションごとなど)を一切意識せずに、Helloインタフェースという型のみを意識するだけで良いということです。それらは、すべてHello Bean側にだけ定義しておけばよい内容となり、Hello Beanとそのクライアント(TestServlet)は、コード上では非常に疎結合な状況で動作していることになります。

CDIは、このように疎結合(loose-coupling)および強い型付け(strong typing)をテーマに機能を実現しています。



疎結合(Loose-coupling)

Beanを利用する側は他のBeanの型やセマンティクスのみを定義し、実際のライフサイクルや具体的な実装、Beanが動作するスレッドモデルや、そのBeanと協調動作する他のBeanなどを詳しく知る必要ありません。これにより、コードのメンテナンス性は向上します。また、開発時やテスト時、本番環境などデプロイ環境により、そのBeanの実際の実装やライフサイクル、スレッドモデルなどをBean Client側を変更せずに適用できます。



強い型付け(Strong typing)

先ほどのコードの中にはDIのためのStringベースの識別子はありません。@Injectアノテーションを付与することで、設定より規約というデフォルトのルールに従って実装クラスを適切に選択して注入します。また、文字列による識別を排除したため、コンパイル時に型のチェックが可能であり、アノテーションによる宣言的な定義で、XMLは不要になります。また、アノテーションベースなので、開発時にもツールがコードを特定しやすい。といったメリットもあります。


おまけ


HelloInEnglish.javaに以下のようなアノテーションを指定したメソッドを利用することで、ライフサイクル(いつ作成されていつ破棄されるのか)を実際に監視してみることも可能です。
HelloInEnglish.java
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
//メソッドの定義
@PostConstruct
private void postConstruct() {
System.out.println("-- Hello Bean is created!");
}
@PreDestroy
private void preDestroy() {
System.out.println("-- Hello Bean is destroyed!");
}


@Qualifierによる実装の指定


ここまでのコードでは、Helloインタフェースに対する実装クラスがHelloInEnglish.javaの一つだけであったため、@Injectにより、このWebアプリケーションにおける唯一の実装クラス(HelloInEnglish)が注入されました。では、同じアプリケーション上に複数のHelloインタフェースの実装クラスが配置されていたらどうなるでしょうか?


次のようなもう一つのHelloインタフェースの実装クラスを作成してみましょう。
HelloInJapanese.java
package cdi;
import javax.enterprise.context.SessionScoped;
@SessionScoped
public class HelloInJapanese implements Hello, java.io.Serializable {
private int count;
@Override
public String sayHello(String name) {
count++;
return "こんにちは " + name + "さん。: 呼び出し回数: " + count;
}
}



この状態で、TestServletを実行してみてください。この場合は、デプロイ時にエラーが発生します。Helloの実装クラスが2つあるため、どちらの実装を使うべきか判断できずデプロイ時において、エラーが発生するという動作となります。これを回避するには、明示的にどちらの実装クラスを利用するかをあらかじめ指定しておく必要があります。その指定方法として、実装クラスを限定するために、CDIではQualifier機能を利用します。ここでは、@ Qualifierを利用した新たな@JapaneseというQualifierアノテーションを作成します。

「Create Annotation」画面
「Create Annotation」画面
(クリックして拡大)
CDISample->Java Resources->srcを右クリックして、New->Annotationを選択し、右のように入力し、Finishボタンを押します。




作成した、Japanese.javaを以下のように書き換えます。赤字のQualifierを指定している部分がCDI特有の内容となります。
HelloInJapanese.java
package cdi;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import javax.inject.Qualifier;
@Qualifier
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD,ElementType.PARAMETER,ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface Japanese {}



この作成した@Japaneseアノテーションを以下のように、HelloInJapanese.javaに付与します。
HelloInJapanese.javaのclass定義部の抜粋
@Japanese
@SessionScoped
public class HelloInJapanese implements Hello, java.io.Serializable {


「Hello Duke!」画面
(クリックして拡大)
この状態で、TestServletを実行してみてください。この場合、先ほどのようなエラーが発生せずに、右のように、HelloInEnglishが実行されていることが分かります。




これは、Hello実装クラスのうちHelloInJapanseは@Japanseアノテーションで限定されているため、特にTestServletで指定しない限りは、限定されていないデフォルトのHelloInEnglishが注入されます。(これは@Defaultという組み込みのQualifierによるものです。組み込みQualifierについては後述します。)



次に、TestServletを以下のように@Japaneseを指定して実行すると、HelloInJapaneseが注入されるようになります。
TestServlet.javaの抜粋
public class HelloInJapanese implements Hello, java.io.Serializable {
…略…
@Inject@Japanese
private Hello hello;


「Hello Duke!」画面
(クリックして拡大)
実行結果は右のようになります。




なお、CDIには、組み込みの4つのQualifierが定義されています。(@Default, @Named, ,@New, @Any)ここで、簡単にそれぞれの意味を説明します。


@Default

@DefaultはデフォルトのQualifierを表します。もし、Beanに@Named以外のQualifierが定義されていない場合は、@Defaultが適用されます。以下の2つは同等になります。
public class HelloInEnglish implements Hello {}
@Default
public class HelloInEnglish implements Hello {}



@Named

@Namedは、文字列ベースのQualifierです。

使用例
public class Car {
@Inject @Named("driver") Seat driverSeat;
@Inject @Named("passenger") Seat passengerSeat;
...
}

@Namedはvalue部分の文字列を指定しないような書き方もできます。以下のコードは、@Named(“paymentService”)と同じ意味になります。
@Inject @Named PaymentService paymentService;

また、@Namedで指定した文字列を利用して、JSPやJSFのEL式から@Namedで指定した文字列でInjectすることができるようになります。(@Namedを指定しない場合は、クラス名の頭を小文字にした文字列となります。)

なお、上記のEL式から利用する用途以外で@Namedを利用することは、Beanの識別に文字列が使われてしまい、強い型保証(Strong typing)というCDIのメリットがなくなります。CDIの仕様書では、文字列でBeanを特定している古いコードと連携するような場合を除いて、@Namedを使用することは推奨しないと明記しています。


@New

Beanのスコープ指定に関係なく、新しく@Dependentスコープでクラスを注入します。@Newで注入されたインスタンスは、@New Qualifierだけを持ち、@Defaultや@Anyを持ちません。(同じスコープで同じ@New Qualifierがあれば共有されます。)

以下のような@SessionScopedのBeanがあった場合、
@SessionScoped
Public class HelloInJapanese {

次のように注入されたインスタンスは、同じSessionスコープにHelloInjapaneseが存在したとしても、@Dependentスコープをもつインスタンスを新たに作成します。
@Inject @New(HelloInJapanese.class) Hello hello;


@Any

すべてのBeanが持つQualifierです。(ただし、@New Qualified Beanは除く)。

@Anyをインジェクションポイントに記述すると、すべてのBeanを参照できるようになります。

少し解かりづらいですが、後ほどデコレータの説明で登場しますので、そちらで実際に動作を確認すると納得できると思います。

@Producesの使い方


上記のコードでは、コード中に@Japaneseアノテーションを記述して、静的にHelloInJapaneseを注入しました。例えば、Locale情報をもとに、動的にHelloに注入する実装クラスを選択したいという場合もあります。このような場合は@Producesアノテーションを使用して実現できます。


まず、@Produces機能を利用するあらたな、@LocalLanguageというQualifierを作成します。

以下のような@LocalLanguageというアノテーションを作成してください。
LocalLanguage.java
package cdi;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.inject.Qualifier;
@Qualifier
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD,ElementType.PARAMETER,ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface LocalLanguage {}





次に、Locale情報を見て、適切なHello実装クラスを返すクラスを作成します。


以下のようなHelloGenerator.javaというクラスを作成してください。getHelloメソッドには、CDIの@Producesアノテーションと、先ほど作成した@LocalLanguageアノテーションを付与しておきます。

getHelloメソッドの実装内容は、デフォルトのLocaleにより、日本語・英語を判断してそれぞれのHello実装クラスを返すというシンプルなものです。
HelloGenerator.java
package cdi;
import java.util.Locale;
import javax.enterprise.inject.Produces;
import javax.enterprise.context.SessionScoped;
public class HelloGenerator {@Produces
@LocalLanguage
@SessionScoped // ここでもスコープを定義することが可能

public Hello getHello() {
Locale locale = Locale.getDefault();
if (locale.getLanguage().equals("ja")) {
return new HelloInJapanese();
} else {
return new HelloInEnglish();
}
}
}



TestServlet.javaにも先ほどの@LocalLanguageアノテーションを付与します。
TestServlet.java
public class HelloInJapanese implements Hello, java.io.Serializable {
…略…
@Inject@LocalLanguage
private Hello hello;



TestServletを実行すると、デフォルトLocaleを元に、動的にHello実装クラスが注入されるようになります。





@Producesは、他にもプリミティブ型の注入にも利用することができます。例えば、ランダムなint型の数値を@Producesを利用して注入してみます。


まず、新たな、@RandomNumberというQualifierを作成します。
RandomNumber.java
package test;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.inject.Qualifier;
@Qualifier
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface RandomNumber {}



次に、RandomNumberGeneratorというクラスを作成し、以下のように実装します。
RandomNumberGenerator.java
package cdi;
import java.util.Random;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import javax.inject.Named;
public class RandomNumberGenerator {

private Random random = new Random(System.currentTimeMillis());

@Produces

@RandomNumber

int getRandomNumber() {

return random.nextInt(100);

}
}



TestServlet.javaに以下のような内容を追記します。
TestServlet.java
public class TestServlet extends HttpServlet {
…略…
@Inject
@RandomNumber
private int randomNum;

…略…
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setCharacterEncoding("Shift_JIS");
PrintWriter out = response.getWriter();
out.print("<html><head><title>CDI Test</title></head><body>");
out.print("<h2> " + hello.sayHello("Duke") + "</h2>");out.print("<h2> Random Number: " + randomNum + "</h2>");
out.print("</body></html>");
}
…略…



TestServletを実行すると、ランダムな数字が出力されます。なお、int型などのプリミティブ型ではスコープ指定はできず、デフォルト・スコープのみとなります。

@Alternativeの使い方


コンポーネントを疎結合にすることで得られるメリットの一つとして、チームで開発をしている場合に、コンポーネントごとの開発を分離しやすいという点が挙げられます。この場合、まだ開発中のBeanを利用する側が単体テストを実行したい場合に、Beanのモック実装を利用したい場合があるかと思います。これを実現するための1つの手段とて、CDIでは@Alternativeアノテーションを利用することができます。


まず、以下のようなMock用のBeanを作成し、@Alternativeアノテーションを付与します。
HelloInJapaneseMock.java
package cdi;
import javax.enterprise.context.SessionScoped;
import javax.enterprise.inject.Alternative;
@SessionScoped
@Japanese
@Alternative
public class HelloInJapaneseMock implements Hello, java.io.Serializable {
private int count;
@Override
public String sayHello(String name) {
count++;
return "これは、テスト実行です。こんにちは " + name + "さん。: 呼び出し回数: " + count;
}
}



@Alternativeアノテーションが付与さているBeanは、明示的に指定しない限りインジェクト対象にはなりません。なので、@Janaeseアノテーションが付与されているBeanが2つになりますが、この状態でTestServletを実行してもエラーにはなりません。この場合は、上記のHellInJapaneseMockは実行されず、HelloInJapaneseが実行されます。

このHelloInJapaneseMockを動作させるには、beans.xmlに明示的にクラス情報を定義する必要があります。


以下のように、WEB-INF/beans.xmlを記述してください。
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"><alternatives>
<class>cdi.HelloInJapaneseMock</class>
</alternatives>

</beans>



この状態で、TestServletを実行すると、以下のようにHelloInJapaneseMockが実行されます。
これは、テスト実行です。こんにちは Dukeさん。: 呼び出し回数: 1



このように、@Alternativeアノテーションを利用することで、Beanの利用側のコードを変更することなく、実装を入れ替えることが可能となります。

インターセプターの利用


CDIのインターセプター機能は、いわゆるアスペクト指向プログラミング(Aspect Oriented Programming、略してAOP)を実現するための機能です。インターセプター機能は、業務ロジックには直接関係ないモジュールに横断的な処理の記述に利用できます。例えば、Beanの呼び出しでログを出力したり、特定のメソッドがコールされた場合に、認証チェックを実施するなどの処理などがあげられます。このような処理を本来の業務ロジックとのコード上の依存性を排除することで、メンテナンス性の高いコードを記述できます。


今回は、HelloのsayHelloメソッドが呼ばれるたびに、標準出力にログを出力するというシンプルな機能をインターセプターを利用して実行してみます。

まずは、インターセプター・バインディング・タイプを定義します。以下のようなアノテーションを作成してください。
Loggable.java
package cdi;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.interceptor.InterceptorBinding;
@InterceptorBinding
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {}



次に、インターセプターを実行するクラスを作成します。以下のようなクラスを作成します。下記は、logメソッドの中で、標準出力にインジェクトされたインスタンスのクラスやメソッドの情報を標準出力に出力しています。なお、インターセプターを利用するBeanが@SessionScopeなどを指定したPassivation capable beanの場合は、インターセプターをSerializableにしておく必要がありますのでご注意ください。
LogInterceptor.java
package cdi;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
@Loggable
@Interceptor
public class LogInterceptor implements java.io.Serializable {

@AroundInvoke

public Object log(InvocationContext ctx) throws Exception {

System.out.println("class : " + ctx.getMethod().getDeclaringClass().getName() + " :method : " + ctx.getMethod().getName() + " is called!");

Object obj = ctx.proceed();

return obj;

}
}



インターセプタークラスを有効化するためにbeans.xmlにクラス情報を記述する必要があります。

以下のようにWEB-INF/beans.xmlを修正します。
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"><interceptors>
<class>cdi.LogInterceptor</class>
</interceptors>

</beans>



最後に、インターセプトしたいBeanにインターセプター・バインディング・タイプ(@Loggable)を指定します。
HelloInJapanese.java
@SessionScoped
@Loggable
public class HelloInJapanese implements Hello, java.io.Serializable {
…略…
}



この状態で、TestServletを実行してみてください。ブラウザをリロードするたびに以下のように標準出力に情報が出力され、処理がインターセプトされていることを確認できます。

class : cdi.HelloInJapanese :method : sayHello is called!

デコレータの利用


CDIのデコレータは、インターセプターと似ていますが、Beanの処理をオーバーラップして、業務ロジックに対して追加の処理を実行することが可能となる機能です。インターセプターとの違いは、インターセプターは業務ロジックには関連しない、横断的関心事(Cross-Cutting Concerns)を扱うのに対して、デコレータは業務ロジックに処理を追加します。


それでは、デコレータ機能を利用するコードを記述してみます。今回は、HelloのsayHelloメソッドの戻り値に、デコレータ機能により文字列を追加して返すというシンプルな機能を実装します。

まず、以下のようなデコレータクラスを作成してください。@Anyにより、すべてのHello Beanに対してデコレータが実行されます。HelloのsayHelloのメソッドの戻り値に”Decorate:”という文字列が付与されるようになります。
HelloDecorator.java
package cdi;
import java.io.Serializable;
import javax.decorator.Decorator;
import javax.decorator.Delegate;
import javax.enterprise.inject.Any;
import javax.inject.Inject;
@Decorator
public abstract class HelloDecorator implements Hello, Serializable {


@Inject

@Delegate

@Any // @Anyによりデコレータ対象をすべてのHello Beanに指定する

private Hello hello;

@Override

public String sayHello(String name) {

String res = hello.sayHello(name);

return "Decorate: " + res;

}
}



デコレータを有効化するには、インターセプターと同様に、beans.xmlにクラス情報を追記する必要があります。
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">

<interceptors>

<class>cdi.LogInterceptor</class>

</interceptors>

<decorators>

<class>cdi.HelloDecorator</class>

</decorators>

</beans>



それでは、TestServletを実行してみてください。以下のように出力される文字列に”Decorate:”が追加されます。
Decorate: こんにちは Dukeさん。: 呼び出し回数: 1


おまけ


上記のDecoratorは@Anyを指定しているため、すべてのHello Beanを対象にしていますが、例えば、以下のように@Japaneseを付与すれば、HelloInJapaneseに限定してデコレートすることも可能です。いろいろと試してみてください。
HelloDecorator.javaの抜粋
    @Inject
@Delegate@Japanese // @Japaneseによりデコレータ対象をHelloInJapaneseのみ
private Hello hello;


まとめ


今回はCDIの概要について簡単に説明し、実際にアプリケーションを動作させて、CDIの基本的な機能を試してみました。

  • @Injectによる注入
  • スコープ指定
  • @Qualifierによる実装の指定
  • @Producesの使い方
  • @Alternativeの使い方
  • インターセプターの利用
  • デコレータの利用

CDIを利用すると、今までのアプリケーションの大きく違ったコーディングスタイルになることが実感できたかと思います。これにより、簡単に疎結合なメンテナンスしやすいコードをJava EE標準のコーディングとして記述できます。

次回は、これまで連載してきた、JSF/JPA(EJB)/JAX-RSとCDIを連携した簡単なアプリケーションを作成します。お楽しみに。

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.