Java EE7 CDI1.1のまとめ : その1 重要な概念

Javaのベンダー公式フレームワークであるJava EEでは、DI(Dependency Injection)の仕様である
CDI(Context and Dependency Injection for Java EE)が提供されています。

CDIJava EEでアプリケーションを開発する上で避けては通れませんが、それ自体が複雑なのに加え、
仕様が複数にまたがっていて理解するのが難しいです。
そのため、CDI使う上で理解する必要のある重要な概念や基本的な使い方、
はまりポイントなどをまとめていきます。

この記事は基本的な使い方、概念についての解説なので、CDIや周辺仕様を全て網羅していません。その点はご了承ください。
なお、本記事で使用するのは、最新のCDI1.2ではなく、Java EE7の一部であるCDI1.1になります。
また、Spring, Seasar2などの経験があることを前提とし、DIの概念は詳細に解説しません。
DIのコンセプトについては、以下の@マークITの記事が参考になります。
なぜDIコンテナを使うのか - @マークIT

CDIとは?

CDIの特徴、ポイントを列挙します。
  • Java EE環境(必ずしもサーバ上で実行する必要はない)で使用可能なDIの標準仕様です。
  • DIにより、プログラムの実行時にオブジェクトの依存関係を組み立てることで、オブジェクト間の依存関係を排除します。
  • Servlet, JAX-RS, JAX-WS, JSF, EJB等、Java EE上で実行するコンポーネント上で利用可能です。
  • DIコンテナのデファクトスタンダードであるSpringに似ています(一部アノテーションは互換性があります)
  • オブジェクト間のイベント通知、AOP相当の機能等、単なるDIコンテナよりも、カバー範囲が広いです。

事前知識

CDIを学ぶ上で、予め頭に入れておいたほうが良い知識をまとめました。

複数の仕様を横断する

CDIは複数の仕様が複雑に絡み合っており、他のJava EEの仕様を横断します。この仕様だけでは完結しないところもあります。
以下は関連する仕様の一部です。
  • リクエスト、セッション、カンバセーションの各スコープ => Servlet, JAX-RS, JAX-WS, EJBの各仕様の処理と密接に関連します。
  • セッション・ビーン => EJBの仕様です。
  • インターセプター => 似ている機能であるデコレータと異なり、JSR-318として別仕様です。
etc...

Springとくらべて、ビーン管理の思想がやや異なっている

Springと似てはいますが、ビーンの管理の考え方がやや異なっています。
詳細は後述しますが、Springと同じだと考えるとハマる可能性があります。

重要な概念

マネージド・ビーン

CDIで扱うことのできるビーンです。条件は以下のとおりです。 (要するに、EJBを除いたほとんどの普通のJavaクラス)
  • non-static inner classでない
  • 具象クラス(or @Decorator付きの抽象クラス)
  • javax.enterprise.inject.spi.Extensionを実装しない
  • EJBではない(@Statelessなどがない)
  • @Vetoedがついていない(これで対象から除去可能)
  • 実行時に解決できる適切なコンストラクタを持つ ※CDIではコンストラクタ・インジェクションをサポートするので、必ずしも、引数無しのコンストラクタをサポートする必要はない

なお、わかりづらいのですが,「マネージド・ビーンであること」と、「コンポーネント・スキャンされてDIで使用可能なこと」は
別の概念になります。CDI1.0では全てのビーンがDIの対象なので、ほぼイコールでしたが、CDI1.1では仕様が変更され、
後述するスコープ・アノテーションか、EJBのセッション・ビーンアノテーションを付与する必要があります。
アノテーションが付与され、DIで使用可能なビーンのことをイネーブルド・ビーンと呼びます。
(設定により、CDI1.0と同じ挙動に変更できます。)

また、CDIではコンストラクタをインジェクション対象とする場合は、コンストラクタを@Injectでの修飾が
必要なのにも注意が必要です。そのため、CDIで管理対象とするビーンがデフォルト・コンストラクタを
サポートするか、パラメータ付きコンストラクタが@Injectで修飾されているかは、
よく確かめるべきです。
特に外部ライブラリのビーンがどちらもサポートしていない場合は、CDIのためだけに
ラップする必要があるかもしれません。

スコープとコンテキスト

CDIを使う上で最も重要の概念になります。考え方がSpringと若干異なるため、注意が必要です。

スコープ

オブジェクトのライフサイクルを決定する識別子になります。CDIの仕様のみでは、以下のスコープが予め定義されています。

アプリケーション (@ApplicationScoped)

アプリケーションのライフサイクルと一致します。Springのシングルトン相当です。

セッション (@SessionScoped)

Servletのセッションのライフサイクルと一致します。Springのセッション相当です。

カンバセーション(@ConversationScoped)

JSFのためのスコープです。複数のHTTP通信をまたがるJSFリクエストを一つのユニット・オブ・ワークとして処理するときに、
そのユニット・オブ・ワークとライフサイクルが一致するオブジェクトを定義します。
セッションに似ていますが、ブラウザで現在表示中の画面(タブ)をまたがず、
その画面内の特定の処理の範囲で完結します。Springには無いスコープです。
(Spring4.0のデフォルトの場合。Web FlowやJSF用拡張は未調査。)

リクエスト (@RequestScoped)

ServletEJBのリモート呼び出し、タイマーコールバック、MDBコールバック、Webサービス呼び出しの1リクエストの
処理の間と、ライフサイクルが一致します。
Springの同名スコープはHTTPのリクエスト処理専用なので、よりカバー範囲が広いです。

ディペンデント (@Dependent)

使われ方によってライフサイクルが変わり、擬似スコープと呼ばれます。
(一方でアプリケーション〜リクエストまでの@NormalScopeで修飾されたスコープはノーマル・スコープと呼ばれます。)

他のビーンにインジェクトされる場合や、デコレータ、インターセプターで特定のビーンと関連付けられる場合は、
インジェクト先ビーンのライフサイクルと一致します。また、プロデューサや、イベント、JSP/JSFのEL上から参照された場合は、
呼び出し毎の使い捨てとなります。
その都度、新しいインスタンスを生成するのはSpringのプロトタイプと似ていますが、ライフサイクル管理がなされる点が異なります。
(Springの場合、払いだしたらそれっきりで、@PreDestoryのコールバックは無い)

また、CDIと関連する仕様である「JSR 330: Dependency Injection for Java」では以下のスコープを定義しています。

シングルトン (@Singleton)

所謂シングルトンとしてオブジェクトを定義します。CDIのビルトイン・スコープではないので、詳細な動作が定義されていません。
CDIのリファレンス・実装であるWeldのマニュアルに、注意点等が記述されているので、参考になります。

5.4. The singleton pseudo-scope - Weld – CDI Reference Implementation

このシングルトン・スコープも擬似スコープなので、クライアント・プロキシが適用されず、同期化、シリアライズの時に
問題が発生する可能性があるようです。

以上、CDIのスコープを見ていきましたが、スコープのまとめとして、挙動を確かめるための簡単なサンプルテストケースを作成しました。
テストケースは組み込みEJBコンテナを使用しており、Java SE6以上の環境ならば、APサーバ無しで動かせるはずです。
(テストケースの実行にとても時間がかかります)


ScopeTest.Java

コンテキスト・オブジェクト

それぞれのスコープに紐づくオブジェクトです。コンテキストにより、インスタンスの生成、破棄のタイミング、可視性等が決定されます。
どのスレッドから、どのコンテキスト・オブジェクトが見えるのかが重要です。以下の図は、コンテキストとスレッドの関係を可視化したものです。

クライアント・プロキシ

シングルトン、ディペンデントを除くノーマル・スコープの場合、オブジェクトをインジェクトしても、オブジェクトのインスタンス
直接参照されません。代わりに、特定のコンテキストに属するオブジェクトへの参照を持つプロキシがインジェクトされます。
この仕組により、アプリケーション・スコープとリクエスト・スコープ、セッション・スコープとリクエスト・スコープ等、
ライフサイクルが異なるオブジェクト同士を連携させて、適切なコンテキストのインスタンスを操作可能となります。
以下の図は、クライアント・プロキシの動作のイメージを可視化したものです。

クライアント・プロキシはSpringの世界で言うところの「動的プロキシベースのインターフェースによるプロキシ」ではなく、
「クラスの拡張ベースのプロキシ」であることに注意が必要です。

動的プロキシベースの場合は、必ずインターフェースを作成する必要があるため、面倒ですが挙動が素直です。
一方でクラスの拡張ベースの場合は、実行時に対象マネージド・ビーンを動的に継承し、プロキシ・ビーンを定義するため、
インターフェースの定義が不要ですが、拡張対象のビーンのコンストラクタが実行されます。

CDIの実装は未見ですが、恐らくフィールドのメモリ領域が確保されますし、
クライアント・プロキシと本物のインスタンスで2倍の処理時間が必要になります。
これがアプリケーション・スコープのビーンならば、オブジェクトの総数が少ないはずですし、
初期化処理の実行はAPサーバの起動時のみで済みます。
一方でリクエスト・スコープやセッション・スコープの場合、処理毎の初期化処理時間、メモリ使用量が
2倍になる可能性があるため、要注意です。
(実装によってはクライアント・プロキシがキャッシュされるかもしれないし、デフォルト・コンストラクタや、
フィールドの定義を上手く削除する可能性もありますが・・・。そこまでするとは思えない。)

また、プロキシは余計なフィールドやメソッドが追加される可能性が高いので、
リフレクションや永続化等の処理でも注意が必要です。

クオリファイヤ(@Qualifier)

オブジェクト(のグループ)を識別するための識別子です。イジェクト対象の識別や、後述するイベント、
インターセプター・バンディング等、CDI全体を通して、識別子として使用されます。
クオリティファイヤは@Qualifierアノテーションで定義し、以下のように独自に拡張可能です。


@Qualifier // 識別子の一種であることを指定
@Retention(RUNTIME) // 実行時に欲しい情報なのでRetention PolicyはもちろんRuntimeで!
@Target({METHOD, FIELD, PARAMETER, TYPE}) // 識別子はあらゆるところに書きたいはず
public @interface Asynchronous {}

クオリファイヤの動作を確認するためのサンプルテストケースを作成しました。使い方のイメージは掴めるかと思います。

QualifierTest.java

最後に

以上、CDIを扱う上で知る必要のある重要な概念を見ていきました。
特にクライアント・プロキシはSpringのデフォルトの動的プロキシの挙動と異なりますし、
スコープはよくわからないで使うと、ハマる可能性があるので良く理解しておくべきかと思います。
次回はCDIで提供される機能について見ていきます。

参考URL