Java EE 7 検証環境構築(4) Java EE での DI(Dependency Injection) および CDI(Contexts and Dependency Injection)をながめる

  1. Java EE 7 検証環境構築(1) WildFly + JBoss Tools で EARプロジェクトを作成し Arquillian で ユニットテストをグリーンにするところまで
  2. Java EE 7 検証環境構築(2) WildFly に DataSourceを作成
  3. Java EE 7 検証環境構築(3) JPAからMySQLに接続するユニットテストをArquillianで実行
  4. Java EE 7 検証環境構築(4) Java EE での DI(Dependency Injection) および CDI(Contexts and Dependency Injection)をながめる
  5. Java EE 7 検証環境構築(5) JBoss Toolsが生成したサンプルソースのCDIを確認する
  6. Java EE 7 検証環境構築(6) JPA エンティティの作成と挿入
  7. Java EE 7 検証環境構築(7) JPA 問い合わせ(1) 名前付きクエリを使ってみる。テストでトランザクションも意識する
  8. Java EE 7 検証環境構築(8) JPA 問い合わせ(2) 動的クエリとCriteria API を試す
  9. Java EE 7 検証環境構築(9) jBatch 概要をおさえる
  10. Java EE 7 検証環境構築(10) JBoss Tools で作成した EARプロジェクトをJava EE 6 から 7 に変更する
  11. Java EE 7 検証環境構築(11) jBatch用 プロジェクトの作成を行う
  12. Java EE 7 検証環境構築(12) jBatch 簡易サンプル作成と Arquillian でユニットテスト
  13. Java EE 7 検証環境構築(13) jBatch REST サービス経由で実行する
  14. Java EE 7 検証環境構築(14) WildFly の管理をGUIで行う
  15. Java EE 7 検証環境構築(15) WildFly を サービスとして設定する(Windows/Linux)
  16. Java EE 7 検証環境構築(16) WildFly と Apache を mod_jk で連携させる(Widows)

JBoss Tools が作成した、サンプルエンタープライズアプリケーションの MemberRegistrationTest クラスの内容の大枠は把握できた。

今回の検証では、Spring から Java EE 7 への移行を狙っているのだが、SpringのDIとは異なり、Java EE のDIでは、アプリケーションサーバーなどのコンテナがオブジェクトを管理するためコンテナが必要となる。

ということは、依存性をユニットテスト実行時に解決するためには、必然的にコンテナが必要になるのだが、どうやって行うんだろう。。。と漠然と懸念していたのだが、Arquillian を利用するとArquillianがコンテナを管理したり、起動済みのコンテナに、パッケージを作成してデプロイしてくれるのでコンテナ上でのユニットテストが可能となる。すばらしい。

ではユニットテストの懸念は解決したので、次はサンプルプロジェクトでユニットテスト対象となっている、MemberRegistration サービスクラスのソースコードを見てみよう。

@Stateless
public class MemberRegistration {

    @Inject
    private Logger log;

    @Inject
    private EntityManager em;

    @Inject
    private Event memberEventSrc;

    public void register(Member member) throws Exception {
        log.info("Registering " + member.getName());
        em.persist(member);
        memberEventSrc.fire(member);
    }
}

ふむふむ。@Inject アノテーションにより、Logger や、EntityManagerの実装クラスがインジェクトされることは、容易に想像がつくが、一体どのように使用すべきで、どういう理屈で動いているだろう。

Loggerはのインスタンスはどこからやってくるのだ???

残念ながら(?) 直感で突っ切れるほど、Java EE は甘くない。

まず、Java EE 5 までの DI とJava EE 6 からの CDI では大きく内容が異なるようだ。

Java EE 6 のCDIとは何か?については、以下のサイトが非常にわかりやすい。サンプルも豊富ですばらしい。

まぁ、上記サイトを読めばいろいろ解決してしまう。。。とはいえ、自分でまとめないと覚えられないたちなので、以下にメモしていこうと思う。

1.Java EE5 までのDI

まず、Java EE 5 までのDI。本当に、この本 は、読みやすくわかりやすい。Java EE 7 版が出ていれば、この検証作業も本の内容をトレースするだけですみそうなのだが。。。早く出版されることを願いつつ、一部要約、抜粋する。

  • DIコンテナ(SpringフレームワークやSeasar2など) では任意のオブジェクトをインジェクション可能だが、Java EE 5 では、コンテナ管理オブジェクトのみが対応

  • Java EE 5 では、コンストラクタインジェクションに対応せず(フィールド、セッター)

1.1 インジェクション対象オブジェクト

Java EE 5 対応コンテナは、以下に示す「コンテナが管理するオブジェクト」をインジェクションする仕組みを持っている。

アノテーション インジェクション対象
@EJB セッションBean(EJB参照)
@Resource
  • データーソース
  • JMSのコネクションファクトリ
  • JMSのデスティネーション
  • Java Mailのセッション
  • JTAのユーザートランザクション
  • EJBコンテキスト
@PersistenceContext JPAのエンティティマネージャ
@PersistenceUnit JPAのエンティティマネージャファクトリ
@Timeout タイマーサービス

1.2 インジェクション先オブジェクト

Java EE 5 では、コンテナがライフサイクルを管理する以下のオブジェクトに対してインジェクション可能。

コンポーネント種類 オブジェクト
Web
  • サーブレット
  • フィルタ
  • リスナ
  • タグハンドラ
  • マネージドBean
EJB
  • セッションBean
  • インターセプタ
  • メッセージ駆動Bean
Webサービス サービス実装クラス

現在広く利用されているJava EE 5仕様でもDI機能は盛り込まれていましたが、非常に制限された形で仕様化されていました。インジェクションするオブジェクトも、インジェクションされるオブジェクトもその種類が制限されており、インジェクションに使用するアノーテーションも、インジェクションするオブジェクトの種類によって異なるアノーテーションクラスを使用する必要がありました(例えば、EJBのインジェクションには@EJBを使用し、データソースのインジェクションには@Resourceを使うなど)。そのため、Java EE 5仕様におけるインジェクションの機能は、汎用のDIと区別するため、一般に「リソース・インジェクション」と呼ばれています

ということらしい。

さてそれが、どう変わるのか?

2. Java EE 7 の CDI

では、Java EE 6 からの CDI!

といことで、Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION) を確認する。

こちらの本も全体として非常にわかりやすく、サンプルも豊富な良書だと思うのだが、なぜか、CDIについての記述がかなり少ない。出版の時期もあるのか?残念ながら @Inject が索引に出てきすらしないという状況。

ということで、Java EE 7 チュートリアルのCDIの箇所を斜め読みし、要点をまとめ(意訳&要約&感想)てみたい。(理解ミスがあったらごめんなさい)

あと、Redhat のサイトも参考になる

では。

2.1 概要

Java EE 7 の CDI では、さまざまなコンポーネントを疎結合かつタイプセーフな方法で柔軟に統合出来る。

CDIは、オブジェクトのライフサイクルを管理し依存性を自動的に提供する。

以下の様にメンバーを宣言することで、CDIランタイムにより自動的にインジェクトされる。

CDIランタイムは、インスタンスのスコープを知る必要がある。

@RequestScoped public class MessageB implements Message { ... }

CDI Bean は、CDIが管理、インジェクト出来るクラスでほとんどのJavaクラスが対象となる。

BeanアーカイブとはCDI Beanを含んだ、JAR、WAR ファイルを指す。

2.2 CDIが提供するサービス

最初の2つを基本として,大きく以下の様なことが可能となる。

  • ステートフルなコンポーネントのコンテキスト(定義されたライフサイクル)と相互作用させる
  • 依存性の注入(DI) タイプセーフ
  • EL言語との統合
  • インジェクトされたコンポーネントのデコレート
  • タイプセーフにインターセプターとコンポーネントをバインディング
  • イベント通知モデル
  • Web Conversation(会話)スコープ
  • SPI(Service Provider Interface) によりサードパーティとの統合を簡潔に

2.3 CDIのコンセプト

2.3.1 疎結合であること

  • サーバーとクライアントの分離(定義された型と限定子)。
  • 共同作業するコンポーネントのライフサイクル(コンテキスト依存コンポーネント、自動ライフサイクル管理)
  • ステートフルコンポーネントのサービス相互運用(純粋なメッセージパッシング、文字列による名称でのルックアップ削除)
  • イベントにより、メッセージプロデューサーをコンシューマーから完全に分離
  • インターセプターにより、直交する関心事を完全に分類

2.3.2 強力な型付け

  • アノテーションによる宣言ですべての設定が可能
    • XMLデプロイメント記述子のほとんどを削減
    • 開発ツールの提供容易性

2.4 Bean

プリミティブ型を含むほとんどのJava型が、Bean型。ライフサイクルコンテキストモデル(CDI仕様) に沿って、インスタンスのライフサイクルがコンテナ管理される、Java EE コンポーネントは Bean。

Bean は、限定子、スコープ、EL名(オプション)、インターセプタバインディング などの属性をもつ。

2.5 CDI 管理 Bean

トップレベルのクラス、以下に該当する場合は、CDI管理Bean

  • JSFの仕様に従っている場合
  • スタティックでないインナークラス
  • 具象クラス、または、@Decoratorがつけられたクラス
  • EJBコンポーネント定義アノテーションがつけられていない。(ejb-jar.xmlでEJBコンポーネントとして定義されていない)
  • 適切なコンストラクタ(パラメータを持たないコンストラクタ、@Injectでアノテートされたコンストラクタ)を持つ

2.6 インジェクト可能なオブジェクトとしてのBean

CDIはコンテナ管理ではない多くのオブジェクトもインジェクト可能

  • ほとんどのJavaクラス
  • セッション Bean
  • Java EE リソース(データソース、JMS)
  • 永続化コンテキスト(JPA EntityManager)
  • Producerフィールド
  • オブジェクトを返すProducerメソッド
  • Webサービスの参照
  • リモート EJBの参照

2.7 限定子の利用

特定のBeanに対していくつかの実装が提供される場合(例えば、あるインターフェースに対して複数の実装クラスが存在する場合)、限定子を利用してどの実装をインジェクトするのかを特定できる

限定子とはBeanに適用できるアノテーションのことで、限定子を定義するには以下を指定する。

  • @Qualifier
  • @Target({METHOD, FIELD, PARAMETER, TYPE}
  • @Retention(RUNTIME)

限定子の宣言例

@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface Informal {}

限定子を付与した実装クラスの例(1)

@Informal
public class InformalGreeting extends Greeting {
    public String greet(String name) {
        return "Hi, " + name + "!";
    }
}

限定子を付与した実装クラスの例(2)

@Default
public class Greeting {
    public String greet(String name) {
        return "Hello, " + name + ".";
    }
}

Beanにインジェクトする場合

限定子を付与しない場合、限定子@Default が暗黙で付与される。

@Inject Greeting greeting;

は、

@Inject @Default Greeting greeting;

と同じ意味となる。

限定子を付与する場合

@Inject @Informal Greeting greeting;

この様にすることで、Greeting インターフェースの実装が複数あり解決出来ない場合に、限定子により解決することが出来るようになる。

2.8 スコープの利用

WebアプリケーションにBeanをインジェクトして利用する場合、ステートを保持する必要がある

事前定義スコープ一覧

スコープ アノテーション 備考
Requet @RequestScoped HTTPリクエスト一回限り
Session @SessionScoped 複数回のHTTPリクエストにまたがる
Application @ApplicationScoped Webアプリケーションのすべてのユーザで共有
Dependent @Dependent
    • デフォルトのスコープ
    • 1オブジェクトは厳密に1クライアントに利用される
    • クライアントBeanと同じスコープを持つ
Conversation @ConversationScoped JSFアプリケーションに含まれ、開発者が境界を制御できる、複数のリクエストにまたがる長い会話

@Dependent を除いて、事前定義されたスコープはコンテキストスコープ。カスタムスコープを定義することも出来る。

スコープはオブジェクトに明確に定義されたライフサイクルコンテキストを提供する。スコープオブジェクトは、必要に応じて自動生成され破棄される。

Java EE コンポーネント(サーブレット、EJB) は、明確に定義されたスコープを持ず、次のいずれかとなる。

  • Singleton : すべてのクライアントでステートを共有
  • Stateless : クライアントが可視のステートを持たない
  • Stateful : クライアントにより明示的に生成と破棄

スコープアノテーションを付与し、CDI管理させる例

@RequestScoped
public class Printer {

    @Inject @Informal Greeting greeting;
    ...
}

2.9 Bean にEL名を付与

ELからCDI管理Beanを利用可能にするには、@Named 限定子を利用する必要がある。

@Named は Bean名(先頭小文字化)でのアクセスを可能にする。

@Named("MyPrinter") のように、名前を指定できる

2.10 Faceletから利用する

プロパティの参照

<h:inputText id="name" value="#{printer.name}"/>

メソッドの呼び出し

<h:commandButton value="Say Hello" action="#{printer.createSalutation}"/>

2.11 Producer メソッドを利用したオブジェクトインジェクション

ProducerメソッドはBeanでないオブジェクトをインジェクトする方法を提供する。

メソッドに対して@Produces アノテーションをつけると、その戻り値の型をインジェクト出来るようになるため、ファクトリークラスを簡単に作成できるようになる。

@Produces @Random int next() {
    return getRandom().nextInt(maxNumber);
}

@Random は上記で出てきた限定子。int (java.lang.Integer と同一視される) だと曖昧であるため、付与することで用途を明確にする

インジェクト例

@Inject @Random Instance randomInt;

2.12 CDIアプリケーションの構成

作成したBeanが、スコープでアノテートされている場合、サーバーがそれを理解するのでBeanアーカイブに追加の設定は不要。

2.12.1 bean.xml

bean.xml は、CDIが利用するオプションのデプロイメント記述子。

アノテーションでの行った設定と、bean.xmlに記述した設定が衝突する場合は、bean.xmlの設定が優先される。

アーカイブのばあい以下のディレクトリに含まれなければならない(CDIが有効にはならない)。

  • Webアプリケーションの場合、WEB-INF 配下
  • EJB,JARの場合、META-INF 配下

2.13 初期化と破棄のためのアノテーション

CDI管理クラスおよびそのスーパークラスの初期化と、破棄の準備のために、以下のコールバックメソッドが準備されている。

  • @PostConstruct :管理Beanの初期化
  • @PostDestroy :管理Beanの破棄準備

ここまで、見た限りでは、SpringのDIを置き換えるに足る機能は持っているのではないかという気がしないでもない。また面白そうな仕様であれこれ試してみたくなる。