エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

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を指定したことにより、bean1bean2フィールドには別のインスタンスが設定される。@Newの属性を省略した場合、bean1bean2フィールドには同じインスタンスが設定される。

@Namedは、JSPJSF 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メソッドの効果的な利用方法を解説する。

[前多 賢太郎]