見習いプログラミング日記

Javaを中心に色々なことを考えてみます。目指せ本物のプログラマ。

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についての詳細は過去の記事にまとめていますが、簡単にいうと、以下の場合にどのクラスをインジェクション対象としてスキャンするかの仕様が見直されました。

  • warやearにbeans.xmlを含めなかった場合
  • またはbeans.xmlbean-discovery-mode="annotated"を明示していた場合

@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-RSCDIを組み合わせる場合はこの案でもなんとかなりますが、フィールドにフォーム入力値などの状態を持つ事が多い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がおすすめです。

何故この変更が盛り込まれたのか

2013年のGlassFish Advant Calanderで書いた記事に詳細をまとめていますが、GuavaなどのCDI1.1が存在する前からあったライブラリが、CDI1.1に準拠したAPサーバ上で動かなくなったことが@Singletonをスキャン対象から外した要因です。

スキャン対象の拡大

@Singletonがスキャン対象から外れた修正以外として、CDI1.2では新たなスキャン対象の追加も行われています。

ステレオタイプについては前述の通りです。特に@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 APサーバの準拠状況

OSS製品では、JavaEE7準拠の代表的なサーバは既にCDI1.2に対応しています。

  • GlassFish4.1 (2014/9/9 リリース)
  • WildFly8.2.0 Final (2014/11/20 リリース)

WebLogicやJBossEAP等の商用サーバは、まだJavaEE7準拠版がリリースされていないため何とも言えませんが、恐らくリリース時にはCDI1.2に対応させてくると思います。

まとめ

  • CDI1.2 マイナーバージョンアップが2014/4に行われた
    • beans.xmlがない場合のスキャン対象Beanが変更
      • @javax.inject.Singletonはスキャン対象から除外された
      • @Interceptor/@Decorator/@Stereotypeがスキャン対象に追加された
  • この修正はJavaEE7準拠サーバにも反映されている
    • GlassFish4.1、WildFly8.2.0はCDI1.2に対応済み