CDI1.2によるbean-discovery-modeの見直し
2014年4月にCDI仕様のマイナーアップデート CDI1.1 -> CDI1.2 *1が行われ、@java.inject.Singletonが付与されたクラスはデフォルトではインジェクションできるBean対象から外れました。
後方互換性のない変更であるため、以下にまとめます。
default bean-discovery-modeの見直し
CDI1.1 (Java EE7) から導入されたbean-discovery-modeについての詳細は過去の記事にまとめていますが、簡単にいうと、以下の場合にどのクラスをインジェクション対象としてスキャンするかの仕様が見直されました。
@java.inject.Singletonがスキャン対象から除外された
冒頭に書いたとおり、@javax.inject.Singleton
はデフォルトのスキャン対象から除外されたため、以下のようなコードはCDI1.2よりデプロイ時にエラーになります。
@Path("/echo") @RequestScoped public class EchoResource { @Inject EchoService service; @GET public String echo(@QueryParam("name") String name) { return service.echo(name); } } @java.inject.Singleton public class EchoService { public String echo(String name) { return "Hello " + name; } }
CDI1.2に準拠しているWildFly8.2.0で試すと、デプロイ時に以下のような例外メッセージが出力されます。@Singletonがスキャン対象から外されたため、依存性の解決例外になっています。
2015-01-11 16:12:54,944 ERROR [org.jboss.msc.service.fail] (MSC service thread 1-1) MSC000001: Failed to start service jboss.deployment.unit."CDITest.war".WeldStartService: org.j boss.msc.service.StartException in service jboss.deployment.unit."CDITest.war".WeldStartService: Failed to start service ... Caused by: org.jboss.weld.exceptions.DeploymentException: WELD-001408: Unsatisfied dependencies for type EchoService with qualifiers @Default
シングルトンをインジェクションしたい場合の対処
思い当たる範囲で、4つの対処があります。
対処案1. インジェクション先を@Singletonにする
インジェクション先のクラスのスコープを@Singleton
にすることで、依存先のServiceクラスに『インジェクション先にスコープを合わせる』ことを示す@Dependent
を付与すると、Serviceはシングルトンになります。
この案が使えるのは、インジェクション先(例ではJAX-RSエンドポイント)が状態を持たない場合のみです。以下のように、フィールドに状態を持っていた場合、@Singletonでは最初のリクエスト時にしかパラメータが設定されません。
@Path("/echo") @javax.inject.Singleton public class EchoResource { @Inject EchoService service; // ↓ 最初の1回目のリクエストしかパラメータが反映されない @QueryParam("name") String name; @GET public String echo() { return service.echo(name); } } @Dependent public class EchoService { public String echo(String name) { return "Hello " + name; } }
JAX-RSとCDIを組み合わせる場合はこの案でもなんとかなりますが、フィールドにフォーム入力値などの状態を持つ事が多いJSFと組み合わせる場合は対処案1は使えません。
追記: 書いてはみましたが、Serviceをシングルトンにしたいだけなのに、JAX-RSエンドポイントを変更するこの案はあんまりお勧めできません。
対処案2. @Stereotypeを使う
CDI1.2より@Steraotype
によって自作したアノテーションが付与されていた場合、デフォルトのスキャン対象となっているため、この仕組みを利用します。
ステレオタイプとは、スコープや@Transactionalなど、Beanの機能を示すアノテーションをまとめる仕組みです。JSF2ユーザは、一度は@Model
を使ったことがあると思いますが、@ModelのJavadocを見ると、@Stereotypeによって、@RequestScopedと@Namedを組み合わせたアノテーションであるとわかります。
@Singleton単体ではスキャン対象となりませんが、@Stereotypeが付与されたアノテーションが付与されたクラスはスキャン対象となるため、以下のように@Singletonをラップしたアノテーションを作ります。
@Singleton @Stereotype @Target(TYPE) @Retention(RUNTIME) public @interface Service { }
作成したアノテーションを@Singletonの代わりに付与します。Spring Framework風に@Serviceにしてみました。
@Service public class EchoService { public String echo(String name) { return "Hello " + name; } }
対処案1と比べて一手間掛かりますが、デメリットは特にありません。
対処案3. @ApplicationScopedを使う
リクエスト毎にインスタンス生成させない方法として、アプリケーションスコープを使う案もあります。今まではアプリケーションで共有のコンフィグ情報やキャッシュなどを持たせる印象が強いアプリケーションスコープですが、1つのインスタンスを共有する観点ではシングルトンと同じように使えます。
@ApplicationScoped public class EchoService { public String echo(String name) { return "Hello " + name; } }
対処案4. ServiceはEJBで作る
スコープに困ったら、Serviceクラスのようなトランザクション境界となるクラスはJavaEE6時代と同様にEJBで作るのもありだと思います。
@Stateless public class EchoService { public String echo(String name) { return "Hello " + name; } }
個人的には@ApplicationScopedはアプリケーション全体の状態を持たせる場合に使いたいため、案2の@Stereotypeを使う案が好きです。シンプルに使う場合は案3の@ApplicationScopedがおすすめです。
スキャン対象の拡大
@Singletonがスキャン対象から外れた修正以外として、CDI1.2では新たなスキャン対象の追加も行われています。
- @Interceptor と @Decorator
- @Stereotypeを持つアノテーションが付与されたクラス
ステレオタイプについては前述の通りです。特に@Interceptorが地味に有効で、インターセプタ実装クラスにスコープアノテーションを付ける必要がなくなりました。
@InterceptorBinding @Target({TYPE,METHOD}) @Retention(RUNTIME) public @interface Logging { } // @Dependent <<== CDI1.2からはスコープがなくてもスキャンされる @Interceptor @Logging @Priority(APPLICATION + 10) public class LoggingInterceptor { @AroundInvoke public Object log(InvocationContext ic) throws Exception { System.out.println("logging ..."); return ic.proceed(); } } @Service public class EchoService { @Logging public String echo(String name) { return "Hello " + name; } }
インターセプタのスコープは?と言われても違和感があったので、嬉しい修正です。
まとめ
- CDI1.2 マイナーバージョンアップが2014/4に行われた
- beans.xmlがない場合のスキャン対象Beanが変更
- @javax.inject.Singletonはスキャン対象から除外された
- @Interceptor/@Decorator/@Stereotypeがスキャン対象に追加された
- beans.xmlがない場合のスキャン対象Beanが変更
- この修正はJavaEE7準拠サーバにも反映されている
- GlassFish4.1、WildFly8.2.0はCDI1.2に対応済み