Java EE 6: Understanding Contexts and Dependency Injection (CDI), Part 2

まず最初にお知らせですが、6月1日より日本においてもサン・マイクロシステムズは日本オラクルに統合されました。私自身もオラクル社におけるコンサルティング・サービスの一員となります。基本的にはこれからもJavaアプリケーション・プラットフォームに関わるコンサルティングを続けていく予定ですので、今後ともよろしくお願いいたします。

それからもう一つ。前回のパート1を書いてからこのエントリを書くまでの間にNetBeans 6.9はRC1、RC2の2つアップデートがリリースされました。6.9ベータではCDI関連のメニューが一部英語のままだった部分が日本語化されていますので、まだ6.9ベータをご使用の方は6.9RC2をお試しください。

さて、Java EE 6仕様に含まれるCDI解説の第2回です。前回のエントリ(パート1)では、Java EE 5のリソース・インジェクションと比較して、主にインジェクション・ポイントの柔軟性、依存関係にあるオブジェクトの提供法の柔軟性について解説しました。また、基本的にCDIにおけるインジェクション・ポイントにおけるオブジェクトの提供方法は必ず一意に解決できなければならないことも述べました。前回までのCDIの例では、インジェクション・ポイントにおける提供オブジェクトの解決がオブジェクトの型だけでできる場合のみを示していました。しかし、実際にはオブジェクトの型だけでインジェクション・オブジェクトを決定することができない場合が存在します。CDI解説の2回目となる今回は、インジェクション・オブジェクトの候補が複数存在する場合に、その中の1つの候補に限定する方法について解説します。

複数のインジェクション・オブジェクトの候補から1つを選択する方法

以下にオブジェクトの型だけではインジェクション・オブジェクトを解決できない典型的な例を示します。

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

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject
    CustomerDAO customerDAO;   // DAO実装1と実装2のどちらかに決められない。
        :
}

// DAOのインタフェース
public interface CustomerDAO {...}

// DAOの実装1
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2
public class XmlCustomerDAOImpl implements CustomerDAO {...}

複数のオブジェクト候補の中から1つの候補をCDIランタイムに選択させ、インジェクションの曖昧さを解決するにはいくつかの方法があります。

  1. カスタムの限定子(@Qualifier)タイプを定義して関係づける
  2. 標準の限定子@Newを用いて、クラス名で関係づける
  3. 標準の限定子@Namedを用いて、名前(文字列)で関係づける
  4. @Alternativeアノーテーションを用いて、beans.xmlから指定する
  5. 上記1)〜4)の組み合わせにより特定する

以下では、それぞれの定義方法について説明します。

1) カスタムの限定子(@Qualifier)タイプを定義して関係づける

限定子(@Qualifier)は、インジェクションの依存関係を決定するために使用するアノーテーションベースのメタデータ定義です。以下の例では、カスタムの限定子@ProductionModeを定義し、インジェクション・ポイントとインジェクション対象クラスの両方に@ProductionModeを付与することで、インジェクションの依存関係を解決しています。

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.inject.Qualifier;

// 限定子 @ProductionMode の定義
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface ProductionMode {}
import com.example.metadata.ProductionMode;
import javax.ejb.Stateless;
import javax.inject.Inject;

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject @ProductionMode
    CustomerDAO customerDAO;   // ← DAO実装1JPACustomerDAOImplがインジェクションされる。
        :
}

// DAOのインタフェース
public interface CustomerDAO {...}

// DAOの実装1(限定子@ProductionModeを付与)
@ProductionMode
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2
public class XmlCustomerDAOImpl implements CustomerDAO {...}

上記のようにカスタム限定子を使用した方法は、アノーテーションの定義を伴うため、実際の開発でカスタム限定子を手書きするのはやや骨の折れる作業になります。しかしながら、NetBeansなどのIDE環境のエディタではコンプリーション(クラス名補完)機能を恩恵を受けることでタイプ・セーフな形でインジェクションの依存関係を定義でき、コンパイル時点でタイプミスによるバグの混入を防ぐことができるという利点があります。

また、CDIサポート機能が追加されたNetBeans 6.9では簡単にカスタム限定子を作成することができるため、カスタム限定子作成の労力を大幅に軽減しているのはうれしい限りです。

nb69_cdi_support3
nb69_cdi_support4

なお、1つのカスタム限定子は異なる複数のインジェクション・ポイントにおける依存性解決のための共通のメタデータとして使用することができます。

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject @ProductionMode
    CustomerDAO customerDAO;

    @Inject @ProductionMode
    CustomerFinder customerFinder;
        :
}

カスタム限定子は引数をとるように定義することもできますので、アプリケーションの中で使用するカスタム限定子が極端に多くなってしまう場合には、引数付きのカスタム限定子を定義し、限定子の数をできるだけ少なくするように工夫することもできます。

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject @Mode(PRODUCTION)  // 引数付き限定子
    CustomerDAO customerDAO;
        :
}

2) 標準の限定子@Newを用いて、クラス名で関係づける

@NewアノーテーションはCDIの標準APIとして用意されている限定子で、カスタム限定子を作成する代わりにインジェクション対象のクラス名を直接指定することでインジェクション候補を限定する方法です。この場合、@Newアノーテーションのメンバにクラス名を指定してインジェクションポイントに付与します。ビーン・コンストラクタやプロデューサには特別な情報を与える必要はありません。以下が限定子@Newを使用した場合の例です。

import javax.enterprise.inject.New;

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject @New(JPACustomerDAOImpl.class)
    CustomerDAO customerDAO;   // ← DAO実装1JPACustomerDAOImplがインジェクションされる。
        :
}

// DAOのインタフェース
public interface CustomerDAO {...}

// DAOの実装1
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2
public class XmlCustomerDAOImpl implements CustomerDAO {...}

@New限定子はカスタム限定子を定義する場合に較べて定義方法が簡単になるのが利点です。また、インジェクション・ポイントを見れば一見してどの実装クラスがインジェクションされるかが分かる点も利点です。しかし、インジェクション元のクラス名を変更する場合は、インジェクション・ポイントにおける@Newアノーテーションのクラス名も同時に修正しなければならない点に注意します。

3) 標準の限定子@Namedを用いて、名前(文字列)で関係づける

CDIにはもう一つ@Namedという標準の限定子が用意されており、オブジェクトに名前を付け、その名前で依存関係を解決する方法も用意されています。以下が@Named限定子を使用した例です。

import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.inject.Named;

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject @Named("jpaDao")
    CustomerDAO customerDAO;   // ← DAO実装1JPACustomerDAOImplがインジェクションされる。
        :
}

// DAOの実装1(ビーン名"jpaDao"を付与)
@Named("jpaDao")
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2(ビーン名"xmlDao"を付与)
@Named("xmlDao")
public class XmlCustomerDAOImpl implements CustomerDAO {...}

@Named限定子を使用する方法は、@Newと同じように手軽に依存関係を定義できる点が利点です。しかし、JSR 299のスペックの中でも述べられているように、(JSF/JSPのELからアクセスされる場合を除いて)@Named限定子はなるべく使用するべきではありません。名前による関連付けはタイプミスによる関連付けのバグをコンパイル時に検出することができないため、実際に動かしてみるまでバグを検出できない可能性があります。

4) @Alternativeアノーテーションを用いて、beans.xmlから指定する

これまでの方法は、依存関係の絞り込みのメタ情報をアノーテーションを用いてソースコードに埋め込むタイプの方法でしたが、外部ファイルに定義する方法も用意されています。依存関係の決定をソースコードの外部に切り出すためには、@Alternativeアノーテーションをインジェクション候補のビーン・クラスやプロデューサ・メソッド(または、プロデューサ・フィールド)に付与します。

import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.enterprise.inject.Alternative;

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject
    CustomerDAO customerDAO;   // ← bean.xmlの定義によって決まる。
        :
}

// DAOのインタフェース
public interface CustomerDAO {...}

// DAOの実装1(@Alternativeを付与)
@Alternative
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2(@Alternativeを付与)
@Alternative
public class XmlCustomerDAOImpl implements CustomerDAO {...}

上記の場合、実際にインジェクションされるクラスを指定するには、デプロイするモジュールに含めるビーン構成ファイルbeans.xmlを使用します。以下は、@Alternativeな候補の中からJPACustomerDAOImplクラスを選択させる場合の例です。

<?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>com.example.dao.JPACustomerDAOImpl</class>
    </alternatives>
</beans>

@Alternativeなビーンはbeans.xmlの<alternatives>タグで明示されなければ選択候補として有効にならない点に注意してください。逆に、beans.xmlで明示された@Alternativeなビーンは他の選択候補を全て無効にします。この性質を利用すると、選択肢が2つしか存在しない場合は2つのうち一方のビーンにのみ@Alternativeアノーテーションを付与すればよいことになります。

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    // beans.xmlに明示がない場合: DAO実装1(JPACustomerDAOImpl)が採用される。
    // beans.xmlに明示がある場合: DAO実装2(XmlCustomerDAOImpl)が採用される。
    @Inject
    CustomerDAO customerDAO;   // ← beans.xmlの定義によって決まる。
        :
}

// DAOの実装1
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2(@Alternativeを付与)
@Alternative
public class XmlCustomerDAOImpl implements CustomerDAO {...}

5) 上記1)〜4)の組み合わせにより特定する

インジェクション候補の中から1つのビーンだけが解決されるようにするためには、場合によっては複数の限定子をインジェクション・ポイントに与えてあげる必要があります。しかし、期待するビーンのクラスやプロデューサに付与されている全ての限定子を指定する必要はありません。候補が1つに絞られるために必要な最低限の限定子をインジェクション・ポイントに与えてあげれば大丈夫です。例えば、CustomerDAOに対して以下のように3つの候補が存在する場合を考えます。

// DAOの実装1
@Named("jpaDao") @ProductionMode
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2
@Named("xmlDao") @ProductionMode
public class XmlCustomerDAOImpl implements CustomerDAO {...}

// DAOの実装3
@Named("csvDao")
@Alternative
public class CsvCustomerDAOImpl implements CustomerDAO {...}

この状態でJPACustomerDAOImplオブジェクトが選択されることを考えると、JPACustomerDAOImplクラスに付与されている限定子は@Named("jpaDao")と@ProductionModeですが、@Named("jpaDao")だけをインジェクション・ポイントに指定するだけでビーンを一意に特定できるため、@ProductionModeは必ずしも指定する必要はありません。

    @Inject @Named("jpaDao")
    CustomerDAO customerDAO;   // ← JPACustomerDAOImplオブジェクトがインジェクションされる。

依存関係の解決にどの方法を使用すべきか?

以上述べてきたように、複数のインジェクション・オブジェクトの候補から1つのビーンを選択する方法は様々な方法がありますが、実際の開発ではどの方法を採用すべきかを考えてみます。

Java EEアプリケーションにおいてCDIの仕組みを使用する代表例は、プレゼンテーション層、ビジネス層、インテクグレーション層などティアをまたがるオブジェクト間の祖結合のためと考えられます。この目的でCDIを使用する場合、アプリケーションが完成に近づいている状態では、ティア間の結合が変更されることはほとんどなく、インジェクションの依存関係はほぼ静的な関係であると考えられます。この場合、もし限定子を付与しなくても候補が1つに決定されるなら最低限の@Injectだけですませるのがいいでしょう。もし、候補が複数存在するなら、なるべく@Named限定子をさけ、カスタム限定子か@New限定子を用いて依存関係を解決するのが望ましいでしょう。@Named限定子をさける理由は、先程述べたようにCDIが持つtype-safeなアプローチの恩恵を得るためです。

また、開発の初期段階では、各ティア毎に独立にコンポーネントの単体テストを実施することが望まれます。この場合、関連する他のティアのクラスはモックのクラスを用意して単体テストをすることが一般的です。例えば、プロダクション用のCustomerDAOの実装を待たずに、CustomerDAOのモックを作成してビジネス層のCustomerManageSessionBeanを単体テストする場合などです。この場合は、@Alternativeとbeans.xmlを使用することが有効です。インジェクション・ポイントの限定子は最終的なプロダクション・モードの組み合わせを定義しておき、単体テスト用のモック・クラスに@Alternativeを付与します。

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    // beans.xmlにモックの明示がない場合: プロダクション用DAO実装が採用される。
    // beans.xmlにモックの明示がある場合: 単体テスト用モックのDAO実装が採用される。
    @Inject
    CustomerDAO customerDAO;   // ← beans.xmlの定義によって決まる。
        :
}

// DAOの実装(プロダクション用)
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装(単体テスト用のモック)
@Alternative
public class MockCustomerDAOImpl extends JPACustomerDAOImpl {...}

こうすれば、モックのDAOを使用してセッション・ビーンの単体テストを実施した後、プロダクション用のDAOとの結合テストに移行する際にソースコードの変更は必要ありません。beans.xmlの置き換えだけで結合テストを実施することができ、またいつでもモック利用の単体テストモードに戻ることができます。

以上、インジェクション候補の絞り込み方法についてまとめてみました。次回は、CDIと従来のリソース・インジェクションとの関係についてと、CDIにおけるもう一つの重要な概念であるスコープについて解説したいと思います。

投稿されたコメント:

コメント
  • HTML文法 不許可
About

Takashi Nishigaya
Principal Consultant
Technology Solution Consulting
Oracle Consulting Services

Search

Categories
Archives
« 8月 2015
      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
     
今日