CDI の注入および限定子の操作
執筆: Andy Gibson
JSR-299 で指定されている CDI (Contexts and Dependency Injection) は Java EE 6 の不可欠な部分であり、サーブレット、エンタープライズ Bean、および JavaBeans が、アプリケーションのライフサイクル内で明確なスコープを持って存在できるようにするためのアーキテクチャーを提供します。また、CDI サービスによって、EJB セッション Bean や JSF (JavaServer Faces) 管理対象 Bean などの Java EE コンポーネントが注入可能になり、イベントの起動や監視による疎結合方式の対話が可能になります。
このチュートリアルは、Andy Gibson 氏によって投稿された「CDI 入門パート 2 – 注入 」というタイトルのブログをベースにしています。ここでは、クラスやインタフェースをほかのクラスに注入 するために、どのように CDI 注入を使用できるかについて示します。また、特定の注入ポイントに注入するクラスの型を指定できるようにするために、CDI 限定子 をコードに適用する方法についても示します。
NetBeans IDE は、Contexts and Dependency Injection を組み込みでサポートしています (これには、プロジェクト作成時に beans.xml CDI 構成ファイルを生成するオプション、注釈のためのエディタおよびナビゲーションサポート、一般的に使用される CDI アーティファクトを作成するための各種ウィザードなどが含まれています)。
このチュートリアルを完了するには、次のソフトウェアとリソースが必要です。
注:
NetBeans IDE の Java バンドル版には、Java EE 6 準拠のコンテナである GlassFish Server Open Source Edition 3.x も含まれています。
CDI のサポートは、パッチ 1 を適用すれば NetBeans 6.8 でも使用できます。
このチュートリアルのサンプルソリューションプロジェクトをダウンロードできます: cdiDemo2.zip
注入 (Injection): CDI の「I」
CDI は、コンテキストおよび依存関係を注入するための API です。Seam および Spring では多くの場合、Bean に名前を付けて、その名前で注入ポイントにバインドすることで依存関係が機能します。「Contexts and Dependency Injection および JSF 2.0 入門 」を終えてからこのチュートリアルを進めている場合、これまでは @Named 注釈を使用して Bean の名前を定義したときに、JSF ページから名前で管理対象 Bean を参照するだけでした。@Named 注釈の主な役割は、アプリケーション内の EL 文を解決できるように Bean を定義することです。この解決は、通常は JSF EL リゾルバによって行われます。注入は名前を使用して実行することもできますが、この方法は CDI の注入を機能させる本来の方法ではありません。CDI には、注入ポイントおよび注入ポイントに注入する Bean を表すためのさまざまな方法が提供されています。
次の例では、ItemDao インタフェースを実装するクラスから項目の一覧を取得する ItemProcessor を作成します。別のクラスに Bean を注入 できるようにする方法を示すため、CDI の @Inject 注釈を利用します。次の図は、この課題で構築するシナリオを図解しています。
DAO はデータアクセスオブジェクト (Data Access Object ) を表します。
まず、cdiDemo.zip ファイル (上記の必要なリソースの一覧表 を参照) からサンプルのスタートプロジェクトを抽出します。「ファイル」>「プロジェクトを開く」(Ctrl-Shift-O、Mac の場合は ⌘-Shift-O) を選択してから、コンピュータ上のこのプロジェクトの場所を選択することで、IDE でプロジェクトを開きます。
新しい Item クラスを作成して、exercise2 という名前の新しいパッケージに格納します。「新規ファイル」( ) ボタンをクリックするか、Ctrl-N (Mac の場合は ⌘-N) を押してファイルウィザードを開きます。
「Java」カテゴリから「Java クラス」を選択します。「次へ」をクリックします。
クラス名として「Item 」、パッケージとして「exercise2 」と入力します (ウィザードの完了時に新しいパッケージが作成されます)。
「完了」をクリックします。新しいクラスおよびパッケージが生成され、エディタで Item クラスが開きます。
POJO (Plain old Java objects) である Item に value および limit プロパティーを作成して、toString() メソッドを実装します。次の内容をクラスに追加します。
public class Item {
private int value;
private int limit;
@Override
public String toString() {
return super.toString() + String.format(" [Value=%d, Limit=%d]", value,limit);
}
}
取得メソッドおよび設定メソッドをクラスに追加します。これを行うには、カーソルがクラス定義の間 (クラスの中括弧の間) にあることを確認してからエディタ内で右クリックし、「コードを挿入」を選択します (Alt-Insert、Mac の場合は Ctrl-I)。取得メソッドおよび設定メソッドを選択します。
「Item」チェックボックスを選択します。これで、クラスに含まれているプロパティーがすべて選択されます。
「生成」をクリックします。クラスの取得メソッドと設定メソッドが生成されます。
public class Item {
private int value;
private int limit;
public int getLimit() {
return limit;
}
public void setLimit(int limit) {
this.limit = limit;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
@Override
public String toString() {
return super.toString() + String.format(" [Value=%d, Limit=%d]", value, limit);
}
}
value と limit の両方の引数を取るコンストラクタを作成します。これにも IDE の支援機能を使用できます。クラス定義内で Ctrl- スペースキーを押して、「Item(int value, int limit) - 生成」オプションを選択します。
次のコンストラクタがクラスに追加されます。
public class Item {
public Item(int value, int limit) {
this.value = value;
this.limit = limit;
}
private int value;
private int limit;
...
ItemDao インタフェースを作成して、Item オブジェクトの一覧を取得する方法を定義します。このテストアプリケーションでは複数の実装を使用することを予定しているため、インタフェースへのコードを作成します。
「新規ファイル」( ) ボタンをクリックするか、Ctrl-N (Mac の場合は ⌘-N) を押してファイルウィザードを開きます。
「Java」カテゴリから「Java インタフェース」を選択します。「次へ」をクリックします。
クラス名として「ItemDao 」、パッケージとして「exercise2 」と入力します。
「完了」をクリックします。新しいインタフェースが生成され、エディタで開かれます。
Item オブジェクトの List を返す fetchItems() という名前のメソッドを追加します。
public interface ItemDao {
List<Item> fetchItems();
}
(エディタのヒントを使用して java.util.List のインポート文を追加します。)
ItemProcessor クラスを作成します。これは主クラスであり、ここに Bean を注入したり、ここからプロセスを実行したりします。今のところは DAO から始めて、プロセッサ Bean にこれを注入する方法を見てみましょう。
「新規ファイル」( ) ボタンをクリックするか、Ctrl-N (Mac の場合は ⌘-N) を押してファイルウィザードを開きます。
「Java」カテゴリから「Java クラス」を選択します。「次へ」をクリックします。
クラス名として「ItemProcessor 」、パッケージとして「exercise2 」と入力します。「完了」をクリックします。
新しいクラスが生成され、エディタで開かれます。
次のようにクラスを修正します。
@Named
@RequestScoped
public class ItemProcessor {
private ItemDao itemDao;
public void execute() {
List<Item> items = itemDao.fetchItems();
for (Item item : items) {
System.out.println("Found item " + item);
}
}
}
インポートを修正します。エディタを右クリックして「インポートを修正」を選択するか、Ctrl-Shift-I (Mac の場合は ⌘-Shift-I) を押します。
「了解」をクリックします。次のクラスのインポート文が必要になります。
java.util.List
javax.inject.Named
javax.enterprise.context.RequestScoped
項目の一覧を作成して、決まった項目の一覧を返すだけの簡単な DAO から始めます。
「プロジェクト」ウィンドウで「exercise2」パッケージノードを右クリックし、「新規」>「Java クラス」を選択します。「新規 Java クラス」ウィザードで、クラス名を「DefaultItemDao」にします。「完了」をクリックします。
エディタで、DefaultItemDao に ItemDao インタフェースを実装させて、fetchItems() を実装します。
public class DefaultItemDao implements ItemDao {
@Override
public List<Item> fetchItems() {
List<Item> results = new ArrayList<Item>();
results.add(new Item(34, 7));
results.add(new Item(4, 37));
results.add(new Item(24, 19));
results.add(new Item(89, 32));
return results;
}
}
Ctrl-Shift-I (Mac の場合は ⌘-Shift-I) を押して、java.util.List および java.util.ArrayList のインポート文を追加します。
ItemProcessor クラスに切り替えます (Ctrl-Tab キーを押します)。ItemProcessor に DefaultItemDao を注入するために、ItemDao フィールドに javax.inject.Inject 注釈を追加して、このフィールドが注入ポイントであることを示します。
import javax.inject.Inject;
...
@Named
@RequestScoped
public class ItemProcessor {
@Inject
private ItemDao itemDao;
...
}
エディタのコード補完サポートを利用して、クラスに @Inject 注釈およびインポート文を追加します。たとえば、「@Inj」と入力してから Ctrl- スペースキーを押します。
最後に、ItemProcessor で execute() メソッドを呼び出すための何らかの方法が必要です。これは SE 環境なら実行できますが、今のところは JSF ページ内にとどめておきます。execute() メソッドを呼び出すボタンを含む process.xhtml という名前の新しいページを作成します。
「新規ファイル」( ) ボタンをクリックするか、Ctrl-N (Mac の場合は ⌘-N) を押してファイルウィザードを開きます。
「JavaServer Faces」カテゴリを選択し、「JSF ページ」を選択します。「次へ」をクリックします。
ファイル名として「process 」と入力してから「完了」をクリックします。
新しい process.xhtml ファイルで、ItemProcessor.execute() メソッドに接続されたボタンを追加します。EL を使用する場合、管理対象 Bean のデフォルト名は、クラス名の最初の文字を小文字にした名前 (つまり itemProcessor) になります。
<h:body>
<h:form>
<h:commandButton action="#{itemProcessor.execute}" value="Execute"/>
</h:form>
</h:body>
プロジェクトを実行する前に、process.xhtml ファイルをプロジェクトの Web 配備記述子の新しい開始ページに設定します。
IDE の「ファイルに移動」ダイアログを使用すると、すばやく web.xml を開けます。IDE のメインメニューで「ナビゲート」>「ファイルに移動」(Alt-Shift-O、Mac の場合は Ctrl-Shift-O) を選択してから「web」と入力します。
「了解」をクリックします。web.xml ファイルの「XML」ビューで、次のように変更します。
<welcome-file-list>
<welcome-file>faces/process.xhtml </welcome-file>
</welcome-file-list>
IDE のメインツールバーにある「プロジェクトを実行」( ) ボタンをクリックします。プロジェクトがコンパイルされて GlassFish に配備され、process.xhtml ファイルがブラウザで開きます。
ページに表示されている「Execute」ボタンをクリックします。IDE に戻って GlassFish のサーバーログを調べます。サーバーログは、「出力」ウィンドウ (Ctrl-4、Mac の場合は ⌘-4) の「GlassFish Server」タブの下に表示されます。ボタンをクリックすると、デフォルト DAO 実装による項目がログに一覧表示されます。
ログを消去するには、「出力」ウィンドウで右クリックして「消去」を選択 (Ctrl-L、Mac の場合は ⌘-L) します。上記の画像では、「Execute」ボタンをクリックする直前にログを消去しています。
ItemDao インタフェースを実装するクラスを作成しました。アプリケーションが配備されたときに、モジュールの管理対象 Bean は (モジュールの beans.xml ファイルのために) CDI 実装によって処理されました。ここで使用した @Inject 注釈は、そのフィールドに管理対象 Bean を注入することを指定します。注入可能 Bean について把握していることは、この Bean が ItemDao またはこのインタフェースのサブタイプを実装する必要があることだけです。この場合、DefaultItemDao クラスは条件を完全に満たしています。
注入された可能性のある ItemDao の実装が複数ある場合はどうなるでしょうか。CDI はどの実装を選択すべきかを判断できないため、配備時エラーが発生します。これを解決するには、CDI 限定子を使用する必要があります。限定子については次の節で詳しく説明します。
限定子の操作
CDI 限定子は、クラスレベルで適用してクラスがどの種類の Bean なのかを示したり、(特に) フィールドレベルで適用してその場所でどの種類の Bean が注入される必要があるかを示したりできる注釈です。
ここで構築しているアプリケーションに限定子が必要なことを示すために、やはり ItemDao インタフェースを実装するもう 1 つの DAO クラスをアプリケーションに追加してみましょう。次の図は、この課題で構築しているシナリオを図解しています。CDI は、注入ポイントでどの Bean 実装が使用されるべきかを判断できなければいけません。2 つの ItemDao の実装があるため、Demo という名前の限定子を作成することでこれを解決できます。そのあと、使用する Bean と ItemProcessor の注入ポイントの両方に、@Demo 注釈で「タグ」を付けます。
次の手順を実行します。
「プロジェクト」ウィンドウで「exercise2」パッケージを右クリックし、「新規」>「Java クラス」を選択します。
「新規 Java クラス」ウィザードで、新しいクラス名を「AnotherItemDao 」にしてから「完了」をクリックします。新しいクラスが生成され、エディタで開かれます。
クラスを次のように変更して、ItemDao インタフェースを実装し、インタフェースの fetchItems() メソッドを定義します。
public class AnotherItemDao implements ItemDao {
@Override
public List<Item> fetchItems() {
List<Item> results = new ArrayList<Item>();
results.add(new Item(99, 9));
return results;
}
}
java.util.List および java.util.ArrayList のインポート文を必ず追加するようにしてください。これを行うには、エディタを右クリックして「インポートを修正」を選択するか、Ctrl-Shift-I (Mac の場合は ⌘-Shift-I) を押します。
これで ItemDao を実装するクラスが 2 つになったため、どの Bean を注入すべきかがわからなくなりました。
「プロジェクトを実行」 ( ) ボタンをクリックして、プロジェクトを実行します。今度はプロジェクトの配備に失敗します。
「保存時に配備」がデフォルトで有効になっていて、IDE がプロジェクトを自動的に配備するため、ファイルを保存するだけで済む可能性があります。
出力ウィンドウ (Ctrl-4、Mac の場合は ⌘-4) でサーバーログを調べます。次のようなエラーメッセージが表示されています。
Caused by: org.jboss.weld.DeploymentException: Injection point has ambiguous dependencies.
Injection point: field exercise2.ItemProcessor.itemDao;
Qualifiers: [@javax.enterprise.inject.Default()];
Possible dependencies: [exercise2.DefaultItemDao, exercise2.AnotherItemDao]
「出力」ウィンドウでテキストを複数行に折り返すには、右クリックして「テキストを折り返す」を選択します。これにより、水平方向にスクロールさせる必要がなくなります。
CDI の実装である Weld によって示されたあいまいな依存関係のエラーは、指定された注入ポイントに使用する Bean を決定できないということを意味しています。Weld の CDI 注入に関して起こる可能性のあるエラーは、ほとんどが配備時に報告され、パッシベーション (非活性化) 可能な Bean に Serializable 実装が欠落していないかどうかといったエラーについても報告されます。
1 つのクラス型のみに一致させることによって、ItemProcessor の itemDao フィールドを、AnotherItemDao 実装型と DefaultItemDao 実装型のうちの一致する方に指定できました。しかし、このようにするとインタフェースへのコードを作成するメリットがなくなり、フィールドの型を変えずに実装を変更することが難しくなります。より良い解決策として、代わりに CDI 限定子に目を向けてみましょう。
CDI が注入ポイントを調べて、注入する適切な Bean を探す際、クラスの型だけでなくすべての限定子も考慮されます。知らないうちに、@Any というデフォルトの限定子をすでに 1 つ使用しました。ここで使用している DefaultItemDao 実装のほか、ItemProcessor の注入ポイントにも適用できる @Demo 限定子を作成しましょう。
IDE には、CDI 限定子を生成できるウィザードがあります。
「新規ファイル」( ) ボタンをクリックするか、Ctrl-N (Mac の場合は ⌘-N) を押してファイルウィザードを開きます。
「コンテキストと依存関係の注入」カテゴリから「限定子の種類」を選択します。「次へ」をクリックします。
クラス名として「Demo 」、パッケージとして「exercise2 」と入力します。
「完了」をクリックします。新しい Demo 限定子がエディタで開きます。
package exercise2;
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;
/**
*
* @author nbuser
*/
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Demo {
}
次に、この限定子をクラスレベルでデフォルトの DAO 実装に追加します。
エディタで DefaultItemDao に切り替え (Ctrl-Tab キーを押します)、クラス定義の上に「@Demo」と入力します。
@Demo
public class DefaultItemDao implements ItemDao {
@Override
public List<Item> fetchItems() {
List<Item> results = new ArrayList<Item>();
results.add(new Item(34, 7));
results.add(new Item(4, 37));
results.add(new Item(24, 19));
results.add(new Item(89, 32));
return results;
}
}
「@」を入力したあとで Ctrl- スペースキーを押して、コード補完の候補を呼び出します。エディタは Demo 限定子を認識して、コード補完のオプションとして @Demo を表示します。
「プロジェクトを実行」 ( ) ボタンをクリックして、プロジェクトを実行します。プロジェクトがエラーなく構築および配備されます。
注: この変更では、変更を増分的に配備する代わりに、アプリケーションを再配備するためにプロジェクトを明示的に実行する必要がある場合があります。
ブラウザで「Execute」ボタンをクリックしてから IDE に戻り、「出力」ウィンドウでサーバーログを調べます。次のような出力が表示されます。
INFO: Found item
exercise2.Item
@
1ef62a93
[Value=99, Limit=9]
出力には、AnotherItemDao クラスの項目が一覧表示されます。ItemProcessor の注入ポイントではなく、DefaultItemDao 実装に注釈を付けたことを思い出してください。@Demo 限定子をデフォルトの DAO 実装に追加することで、型と限定子の両方で一致するようになるため、ほかの実装が注入ポイントに、より一致するようになりました。DefaultItemDao にある Demo 限定子は注入ポイントにはないため、あまり適切ではありません。
次に、ItemProcessor の注入ポイントに @Demo 注釈を追加します。
エディタで ItemProcessor に切り替え (Ctrl-Tab キーを押します)、次のように変更します。
@Named
@RequestScoped
public class ItemProcessor {
@Inject @Demo
private ItemDao itemDao;
public void execute() {
List<Item> items = itemDao.fetchItems();
for (Item item : items) {
System.out.println("Found item " + item);
}
}
}
ブラウザで「Execute」ボタンをクリックしてから IDE に戻り、「出力」ウィンドウでサーバーログを調べます。デフォルトの実装 (DefaultItemDao) による出力がふたたび表示されます。
INFO: Found item
exercise2.Item
@
7b3640f1
[Value=34, Limit=7]
INFO: Found item
exercise2.Item
@
26e1cd69
[Value=4, Limit=37]
INFO: Found item
exercise2.Item
@
3274bc70
[Value=24, Limit=19]
INFO: Found item
exercise2.Item
@
dff76f1
[Value=89, Limit=32]
これは、現時点では型と限定子の両方 をベースにしてマッチングを行っており、正しい型と @Demo 注釈の両方が当てはまる Bean は DefaultItemDao だけであるためです。
代わりの注入方法
注入されるクラスの注入ポイントを定義するには複数の方法があります。これまでは、注入されるオブジェクトを参照するフィールドに注釈を付けました。フィールド注入のために取得メソッドや設定メソッドを提供する必要はありません。final フィールドを持つ不変の管理対象 Bean を作成する場合、@Inject 注釈でコンストラクタに注釈を付けることで、コンストラクタで注入を使用できます。そのあとで、コンストラクタパラメータに任意の注釈を適用して、注入する Bean を限定できます (注入対象の Bean を限定できるように、各パラメータには型があります)。Bean は注入ポイントが定義されたコンストラクタを 1 つしか持てませんが、複数のコンストラクタを実装することは可能です。
@Named
@RequestScoped
public class ItemProcessor {
private final ItemDao itemDao;
@Inject
public ItemProcessor(@Demo ItemDao itemDao) {
this.itemDao = itemDao;
}
}
注入される Bean に渡すことができる初期化メソッドを呼び出すこともできます。
@Named
@RequestScoped
public class ItemProcessor {
private ItemDao itemDao;
@Inject
public void setItemDao(@Demo ItemDao itemDao) {
this.itemDao = itemDao;
}
}
上記の場合では初期化用に設定メソッドを使用しましたが、任意のメソッドを作成して、メソッド呼び出しの中で任意の数の Bean の初期化用に使用できます。1 つの Bean に複数の初期化メソッドを持たせることもできます。
@Inject
public void initBeans(@Demo ItemDao itemDao, @SomeQualifier SomeType someBean) {
this.itemDao = itemDao;
this.bean = someBean;
}
注入ポイントがどのように定義されているかにかかわらず、Bean のマッチングにも同じ規則が適用されます。CDI は、型と限定子をベースにもっとも適切に一致するものを探そうとします。そして注入ポイントとして一致する Bean が複数ある場合や、一致する Bean がない場合は、配備時に失敗します。
関連項目
この Contexts and Dependency Injection についてのシリーズの次回に続きます:
CDI および Java EE の詳細については、次のリソースを参照してください。