JavaEE7をはじめよう(13) - インジェクション候補が複数ある場合の対処方法
前々回と前回の記事で、CDIの基本的な部分であるスコープとインジェクションについて解説した。
今回は、インジェクション候補のクラスが複数ある場合の様々な対処方法を紹介する。
実装や継承関係がある場合のスコープ指定
実装や継承関係がある場合は、実際にインジェクションしたい具象クラスにスコープを指定する。
例えば、次のようなインターフェースがあるとする。
public interface SomeIf {}
インターフェースや抽象クラスに対してはスコープを指定できないため、SomeIf
を実装する具象クラスに対してスコープを指定する。
@RequestScoped public class SomeBean implements SomeIf {}
インジェクションの指定は、インターフェースや抽象クラスに対しても行える。次のようにSomeIf
に対して@Inject
を指定すると、CDIコンテナはSomeIf
に代入可能な型を探してインジェクションを行う。
public class Logic { // インジェクションされるのは、実装であるSomeBean @Inject private SomeIf bean; }
この例では、SomeIf
を実装しているSomeBean
がスコープ指定されているので、SomeBean
のインスタンスがインジェクションされる。
インターフェースと実装クラスに分けた場合には、この例のように、スコープを実装クラスに指定し、@Inject
をインターフェースに指定するのが一般的である。
インジェクション候補が複数ある場合の対処方法
では、上記のサンプルに、スコープ指定つきの実装クラスをもう1つ追加するとどうなるだろうか。
// もう一つの実装クラス @RequestScoped public class HogeBean implements SomeIf {}
こうなるとCDIコンテナは、SomeIf
で宣言されたインジェクションポイントに代入可能な型が2つあると認識するので、どちらを代入してよいかわからず、デプロイを失敗させる。
このような場合の対処方法は大きく分けて5つある。それぞれについて説明していこう。
方法1:実装クラスに@Inject
を指定する
まずは、インジェクションする型の宣言を実装クラスにする方法である。
public class Logic { @Inject private SomeBean bean1; @Inject private HogeBean bean2; }
複数のサブクラスや実装クラスがある場合には、具象クラスに対してインジェクション指定をする方法がある。上のコード例のようにすると、@Inject
指定とスコープ指定のクラスが一致するので問題はなくなる。
インジェクションしたいインスタンスの型宣言が、java.io.Serializable
のようなマーカーや、プロジェクトルールで決められた共通スーパークラスになっている場合には、具象クラスの型で宣言するとよいだろう。
方法2:標準の限定子(Qualifier)を使用する
2つめは、CDIが提供する標準の限定子を使う方法である。
限定子とは、インジェクション候補が複数ある場合に、どの型を選択するかを指定するためのアノテーションで、@Inject
と並べて指定する。
標準の限定子として、指定したクラスのインスタンスを作る@New
、および文字列でCDI Beanを参照可能にする@Named
が使用できる。
以下が、@New
, @Named
によるインジェクション指定の例である。
public class Logic { @Inject @New(SomeBean.class) private SomeIf bean1; @Inject @Named("hobeBean") private SomeIf bean2; }
@Named
を使う場合はCDI Bean側にも同名のパラメータを指定する必要がある。
@RequestScoped @Named("hobeBean") public class HogeBean implements SomeIf {}
@New
は実装クラス名を直接書いてしまうので、インジェクションするクラスを変更するにはソースコードを修正する必要があることに注意が必要だ。
@Named
は文字列でオブジェクトを指定するため、タイプミスがあったとしてもアプリケーションを実行するまで間違いに気づかなくなり、型安全性を下げてしまうデメリットがある。
@New
と@Named
の本来の使い方
本来@New
と@Named
は複数の実装クラスの中からインジェクション対象を決めるための限定子ではない。以下に、本来の使い方を簡単に説明しておく。
@New
は通常のCDIコンテナで生成されるCDI Beanとは別のCDI Beanを作るために使用する。例えば、セッションスコープと定義したCDI BeanのSessionScopeBean
があるとする。通常このCDI Beanのインスタンスはセッションスコープ中で1つだけになる。しかし@New
を用いると、1つのセッションスコープで複数のSessionScopeBean
インスタンスを持つことができる。
以下がサンプルである。
public class Hoge { @Inject private SessionScopeBean bean1; // bean1 と bean2 は別インスタンスとしてインジェクション @Inject @New private SessionScopeBean bean2; }
ここでは@New
を指定したことにより、bean1
とbean2
フィールドには別のインスタンスが設定される。@New
の属性を省略した場合、bean1
とbean2
フィールドには同じインスタンスが設定される。
@Named
は、JSPやJSF Viewで CDI Bean を参照するのが主な用途である。
CDI Beanに@Named
を指定すると、Viewで使用するEL式から文字列指定で参照できるようになる。
例えば、以下のようにCDI Beanに@Named
アノテーションを指定する。
@Named @ApplicationScoped public class SomeBean { public int add(int a, int b) {return a + b;} }
すると以下のように、JSPなどのEL式から文字列で上記のCDI Beanを参照して、メソッドを実行できる。
なお、@Named
の属性を省略すると、クラス名の先頭1文字を小文字にした名前が暗黙的に設定される。
<span>7が表示される</span> ${someBean.add(3 ,4)}
このように、@New
, @Named
はいずれも本来の用途は異なるため、実装クラスの指定で使用するのは最小限にしたほうがよいだろう。
方法3:限定子(Qualifier)を作る
限定子は自作することも可能だ。
まずは@Qualifier
アノテーションを付与した限定子用のアノテーションを作る。
@Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) public @interface Some {}
@Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) public @interface Hoge {}
この限定子は、次のようにCDI Bean側とインジェクションポイントの両方に設定しておくことで、有効になる。
CDI Beanには次のように指定する。
@Some @RequestScoped public class SomeBean implements SomeIf {}
@Hoge @RequestScoped public class HogeBean implements SomeIf {}
インジェクションポイントの指定方法は次の通り。
public class Logic { @Inject @Some // @Someが設定されたSomeBeanがインジェクションされる private SomeIf bean1; @Inject @Hoge // @Hogeが設定されたHogeBeanがインジェクションされる private SomeIf bean2; }
限定子は複数あるインジェクション候補を静的に制御する機能である。
@New
と比較すると、実装クラスを直接書く必要がなく、実装クラスの管理を一元化できることがメリットである。
限定子は単体で使うことも可能だが、次に解説する Producer と組み合わせることも可能だ。
限定子とProducerを組み合わせた例については、次回の記事で解説する。
方法4:Producer メソッドを使用する
Producerメソッドは、メソッドの戻り値をインジェクション対象にする方法である。
限定子が静的な方法であることに比べ、 Producerメソッドは動的な方法といえる。
サンプルのSomeBean
, HogeBean
からスコープ指定を取り除き、@Produces
アノテーションを付与したメソッドを持つCDI Beanを別途定義する。
@Dependent // Producerのクラス定義にもスコープ指定が必要 public class SomeProducer { private boolean useSome = true; @Produces // Producerメソッドの宣言 @RequestScoped // 戻り値のスコープ。指定が無い場合はDependent public SomeIf create() { if (useSome) { // 何らかの判定処理を行う return new SomeBean(); } else { return new HogeBean(); } } }
上記のようにしておくと、SomeIf
型に対しては、自動的にSomeProducer#create
メソッドの戻り値がインジェクションされる。
public class Logic { // SomeProducer#createメソッドの戻り値がインジェクションされる @Inject private SomeIf bean; }
注意すべきは、Producerメソッドを記述するクラス自体と、Producerメソッドそれぞれにスコープ指定が行える事だろう。
どちらもスコープ指定を省略した場合は、@Dependent
が暗黙的に指定される。
クラス定義にスコープを指定できることは、Producerメソッドを記述するクラスそのものが CDI Bean として定義されていなければならないことを意味する。
どのスコープを指定するかは、Producerメソッドの内容による。
例えば、HTTPリクエストなど、クライアントの情報がメソッドのロジックに必要ならリクエストスコープとする必要があるし、サーバー内の設定ファイルや環境変数などを用いるのなら、クライアントごとにCDI Bean を持つ必要がないのでアプリケーションスコープとすればよい。
Producerメソッドに対するスコープ指定は、インジェクションされるオブジェクトのライフサイクルを決定する。
例えば、上記のcreate
メソッドのスコープを@ApplicationScoped
にすると、インジェクション(つまりcreateメソッドの実行)は1度だけ行われる。
@RequestScoped
とすると、クライアントからのアクセスのたびにインジェクションが行われる。
通常、Producerメソッドの引数は無しとすることが多いが、引数をとることもできる。 メソッド引数はインジェクションと同じ仕組みで CDI コンテナから渡される必要があるので、CDI Beanなどインジェクションが可能なオブジェクトで無ければならない。
以下は、 HttpServletRequest
を例に、メソッド引数にインジェクションを行う例だ。
ちなみに、HttpServletRequest
のインジェクションはJavaEE 7 から可能になっている。
@Produces @RequestScoped public HTTPSetting create(HttpServletRequest req) { if (req.isSecure()) { return new SSL(); } else { return new HTTP(); } }
上記は、以下と同じである。
@Inject HttpServletRequest req; @Produces @RequestScoped public HTTPSetting create() { if (req.isSecure()) { return new SSL(); } else { return new HTTP(); } }
このように Producerメソッドにより、様々な条件で、動的にインスタンスを切り替えることが可能となる。
方法5:Alternativeを使用する
Alternativeはデプロイ時にインスタンスを決定する方法である。
データベースやWebサービスなどを用いた周辺システムへの接続などは、開発時と本番時でURLなどの環境設定が異なったり、開発時は周辺システムが存在しないためにスタブを用いたりすることがよくある。
Alternativeは、環境に応じて異なる設定やスタブの適用などを行いたい場合に有用である。
方法としては、@Alternative
アノテーションをインスタンス切り替え対象のBeanに付与する。
@Alternative @RequestScoped public class SomeBean implements SomeIf {}
@Alternative @RequestScoped public class HogeBean implements SomeIf {}
あとは、どちらのクラスを使用するのかを、beans.xmlに記載すれば、指定したクラスがデプロイ時に選択される。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" bean-discovery-mode="annotated"> <alternatives> <class>example.cdi.bean.HogeBean</class> </alternatives> </beans>
まとめ
以上、インジェクションするインスタンスの様々な解決方法について述べた。汎用性が高いのは、Producerメソッドを使用する方法だろう。
次回はProducerメソッドの効果的な利用方法を解説する。
[前多 賢太郎]