Java EE環境におけるCDIのデフォルト化
Java EE 7 に含まれるCDI1.1より、beans.xmlをアーカイブに含めなくてもCDIが有効となるように仕様が変更されました。この改善点、Java EE 6ユーザにとっては少し困惑する部分があったので、まとめておきます。
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の頃のように何もアノテーションを付与していないクラスをインジェクションするためには、以下のいずれかの対応が必要になります。
<?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>
<?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.xmlのXMLスキーマには以下のようなコメントがあります。
"annotated"の使用を強くお勧めします。
beanディスカバリモードを"all"とした場合、アプリケーションに含まれる
http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/beans_1_1.xsd
全てのアーカイブの型について考慮が必要になります。
beanディスカバリモードを"annotated"とした場合は、Bean定義アノテーションが付与されている型のみ考慮すれば問題ありません。
"none"の場合は、考慮すべき型はありません。
(翻注: noneが設定されるアーカイブではCDIが無効となるため、考慮不要となる)
確かに、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のインジェクション対象からは外したいといった場合に使用できそうです。
まとめ
*1:Context and Dependency Injection for the Java EE platform - CDI1.1仕様書 12.1 Bean achives