Java EE環境におけるCDIのデフォルト化

Java EE 7 に含まれるCDI1.1より、beans.xmlをアーカイブに含めなくてもCDIが有効となるように仕様が変更されました。この改善点、Java EE 6ユーザにとっては少し困惑する部分があったので、まとめておきます。

CDIとはなんぞやについてはこちらもご参照ください。

CDI1.1からはbeans.xmlがなくてもCDIが有効になる

CDI1.0(Java EE 6)では、WEB-INF/beans.xmlを空ファイルでも良いので含めることで、CDIが有効となり、様々なJava EEコンポーネント(JSFのバッキングBeanやEJBなど)に@Injectで任意のクラスのインスタンスをインジェクションできました。

CDI1.1からは、WARファイルやEARファイルのなかに『bean defining annotationが付与されたクラス』または『セッションBean』が含まれていると、beans.xmlがなくてもCDIが有効*1になります。セッションBeanとは、EJBのステートレスセッションBean - @Stateless とステートフルセッションBean - @Statefull のことです。では、bean definition annotationとはなんでしょうか。

Bean定義アノテーションとは (bean difinition annotation)

CDI1.1仕様書の2.5章によると、スコープアノテーションがBean定義アノテーションであると示されています。スコープアノテーションとは、@Injectでインジェクションしたアノテーションの生存期間を指定するものです。例えば以下のように、Servletに対して、@RequestScopedを付与したBeanをインジェクションしてみます。

@WebServlet("/helloworld")
public class HelloServlet extends HttpServlet {

    @Inject private Greeter greeter;

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException {

        res.getWriter().println("Hello world! : " + greeter.greet());
        res.getWriter().flush();
    }
}

/** グリーターインタフェース */
public interface Greeter {
    public String greet();
}

// JSFのRequestScopedと間違えないように注意
/** インタフェースの実装 */
@javax.enterprise.context.RequestScoped
public class CDIGreeter implements Greeter {
    @Override
    public String greet() {
        return "Context and Dependency Injection!";
    }
}

この時、HelloServletにインジェクションしているCDIGreeterは@RequestScopedが付与されているため、HelloServletを呼び出すたびに新しいインスタンスがインジェクションされます。@RequestScopedは『Servletのserviceメソッド(doGetの呼び出し元)を実行している間』と定義されているためです。スコープアノテーションには以下のようなものがあります。

アノテーション スコープの範囲
@RequestScoped リクエストスコープ。要求を受け付けてから、返すまで。
@SessionScoped セッションスコープ。Httpセッションの間。いわゆるログイン期間。
@ApplicationScoped アプリケーションスコープ。アプリが起動している間ずっと。
@ConversationScoped Conversation.begin()からConversaion.end()を実行するまでの間。リクエストとセッションの間の生存期間で、ブラウザタブごとにスコープを分けたりするのに使う。
@Dependent インジェクション先のライフサイクルに準ずる。例えば、Servletの場合は1つのインスタンスをリクエスト間で使い回しているため、アプリ起動から停止まで。

スコープアノテーションが何も付与されていない場合、@Dependentであると認識されます。

CDI1.0では、あまり@Dependentを明示的に付与する機会がなかったと思います。しかし、CDI1.1ではこの@Dependentを付与しないとうまく動かないケースが出てきました。

beans.xml省略の条件

繰り返しますが、beans.xmlがなくてもインジェクションできるBeanは、『bean defining annotationが付与されたクラス』または『セッションBean』のみです。すなわち、CDI1.0でbeans.xmlをアーカイブに含めていたときのように、何もスコープアノテーションが付与されていないクラスはインジェクションの対象になりません。

前述の例では、CDIGreeterクラスから@RequestScopedを削除すると、HelloServletにインジェクションされません。スコープとしては暗黙的に@Dependentとみなされますが、明示的にスコープアノテーションが付与されていないため、インジェクション対象とならない為です。CDI1.0の頃のように何もアノテーションを付与していないクラスをインジェクションするためには、以下のいずれかの対応が必要になります。

  • 空のbeans.xmlをアーカイブ(WARなど)に含める
  • CDI1.0のスキーマを指定したbeans.xmlをアーカイブに含める
<?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">
</beans>
  • CDI1.1のスキーマを指定したbeans.xmlの場合は、bean-descovery-modeに"all"を設定する
<?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="all">
</beans>

bean-discovery-modeとはなんでしょうか。

bean-discovery-modeの導入

CDI1.1からは、CDI1.1のスキーマを指定してbeans.xmlを定義する場合、にbean-discovery-mode属性を指定する必要があります。NetBeans7.3を使って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">
</beans>

このデフォルトで出力され、かつ推奨設定である bean-discovery-mode="annotated" で私ははまりました。

bean-discovery-modeとは名の通り、どんなクラスをCDIでインジェクションされる側のBeanとするかを定義するアノテーションです。CDI1.1のスキーマでは必須属性です。3種類のモード(all/annotated/none)がありますが、CDI1.0と互換性のある挙動は"all"です。しかし、後述の理由から"annotated"の利用が強く推奨されています。

bean-descovery-mode 説明
all 全てのクラスがインジェクション可能となる。
annotated スコープアノテーションが付与されている、またはセッションBeanのみインジェクション可能となる。
none このアーカイブをCDIの対象としない。

NetBeans7.3は推奨されている通り、"annotated"が付与されたbeans.xmlを自動的に生成します。このため、何もスコープアノテーションが付与されていないクラスはインジェクション対象となりません。CDI1.0の頃のつもりで@Dependentを記述せずに暗黙的に認識されるのを頼りにコードを書くと、インジェクションされずにNullPointerExceptionとなります。@Dependentを明示的に付与したところ、正常にインジェクションされました。

"annotated"を強く推奨している理由

CDI1.1のbeans.xmlXMLスキーマには以下のようなコメントがあります。

"annotated"の使用を強くお勧めします。

beanディスカバリモードを"all"とした場合、アプリケーションに含まれる
全てのアーカイブの型について考慮が必要になります。
beanディスカバリモードを"annotated"とした場合は、Bean定義アノテーションが付与されている型のみ考慮すれば問題ありません。
"none"の場合は、考慮すべき型はありません。
(翻注: noneが設定されるアーカイブではCDIが無効となるため、考慮不要となる)

http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/beans_1_1.xsd

確かに、WEB-INF/lib 配下に空のbeans.xmlを含むライブラリが含まれていた場合、ライブラリ内のクラスもCDI管理Beanの対象となり、思わぬクラスがインジェクションの対象となることがあります。このため、ユーザがスコープアノテーションを付与して、『このクラスはCDIでインジェクションしたいです』と宣言することで、デプロイ時にインジェクション対象が一意に特定できない例外を防止する意図がありそうです。

参考に、GlassFish4(Weld2.0.0)では、インジェクション対象が一意に特定できない場合は、デプロイ時に以下のような例外が発生しました。

org.jboss.weld.exceptions.DeploymentException: WELD-001409 Ambiguous dependencies for type ...

@Ventedの導入

CDI1.1からはbean-descovery-modeをallに設定している場合、CDI管理Beanから対象のクラスを外すために @Vented アノテーションが導入されました。例えば以下のように使います。

@Vented
public class NonInjectionGreeter implements Greeter {
    @Override
    public String greet() {
        return "This beans should not injected!";
    }
}

@Ventedが付与されたクラスは、インジェクションの対象になりません。
特定のインタフェース、この例ではGreeterインタフェースは実装したいが、CDIのインジェクション対象からは外したいといった場合に使用できそうです。

まとめ

  • CDI1.1からは、beans.xmlを含めなくてもCDIによるインジェクションができる。
    • beans.xmlを含めない場合は、インジェクションしたいクラスに@Dependentや@RequestScopedなどのBean定義アノテーションを付与する
  • CDI1.1のbeans.xmlには、bean-discovery-mode属性が追加されている。
    • CDI1.1のスキーマでは必須属性で、all、annotated、noneの3種類。
    • CDI1.0と同じ挙動をするのは、"all"を設定した場合。
    • "annotated"を指定した場合は、Bean定義アノテーションを付与したクラスのみがインジェクションの対象となる。
    • 推奨設定は"annotated"。ライブラリ内クラスまで考慮することを不要とするため。
  • CDI1.0との互換性
    • 空のbean.xmlを含めた場合はbean-descovery-modeは"all"と見なされる。
    • CDI1.0のスキーマを指定した場合も、"all"と見なされる。
  • @Ventedの導入
    • bean-discovery-modeを"all"に設定した場合、インジェクションの対象から外したいときに使う。

*1:Context and Dependency Injection for the Java EE platform - CDI1.1仕様書 12.1 Bean achives