ProcessAnnotatedTypeイベント:アノテーションの上書き

AnnotatedTypeはCDIにおいてクラスまたはインタフェースのメタデータを管理するためのものです。AnnotatedTypeからアノテーション、コンストラクタ、フィールド、メソッドの情報を得ることができます(下図参照)。CDI拡張モジュールはProcessAnnotatedTypeイベントによってこのAnnotatedTypeを受け取り、そのラッパークラスを作成してCDIコンテナに登録することで、メタデータを上書きできます。


具体例を見ていきましょう。AnnotatedTypeの上書きをする拡張モジュールの例としてWeld Documentation:16.7. Wrapping an AnnotatedTypeがあります。これは、AnnotatedTypeのアノテーションが@Nameを含む場合にvalue()の内容を上書きするというものです。

もともとの@Namedの定義は次のようになっています。

package javax.inject;

@javax.inject.Qualifier
@java.lang.annotation.Documented
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface Named {

 java.lang.String value() default "";
}

@NamedはBeanに名前をつけるものです。UserManagerというBeanクラスがあったとして、このクラスに@Named(value=”userManager”)または@Name(“userManager”)のように指定すると、指定されたクラスのインスタンスは#{userManasger}のようにEL式から参照することが可能になります。

クラスに対して行われる@Namedのvalueの宣言はJavaソースコード上で定義されるものですが、このvalueの値をCDI拡張モジュールの中で書き換えることができます。アノテーションはインタフェースなのでデフォルト値を上書きすることはできません。しかし、CDIコンテナはAnnotatedTypeを使ってクラスのメタデータを管理していますから、これを書き換えることによってCDIコンテナが使うメタデータを書き換えることができるのです。AnnoatedTypeではアノテーションも管理されているので、@Namedのアノテーションを置き換えることによってアノテーションを書き換えることができるのです。

以下は、Weld Documentationのサンプルを若干修正したコードです(修正部分はコメントで示します)。この拡張モジュールではAnnotatedType::getAnnotation()において、取り出したアノテーションが@Nameであれば(前回のブログで紹介した)AnnotationLiteralを使ってオリジナルのアノテーションを上書きします。@Namedのvalue()メソッドを上書きしている点に注目してください。

package org.tanoseam.examples;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import javax.enterprise.inject.spi.Extension;
import javax.enterprise.inject.spi.ProcessAnnotatedType;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.AnnotatedConstructor;
import javax.enterprise.inject.spi.AnnotatedField;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.inject.Named;
import javax.enterprise.event.Observes;
import javax.enterprise.util.AnnotationLiteral;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.lang.StringUtils;

public class QualifiedName implements Extension {

    public  void processAnnotatedType(@Observes ProcessAnnotatedType event) {

        final AnnotatedType at = event.getAnnotatedType();

        log("ProcessAnnotatedType: AnnotatedType" + at);

        AnnotatedType wrapped = new AnnotatedType() {
            Set annotations = new HashSet();

            @Override
            public Set> getConstructors() {
                return at.getConstructors();
            }

            @Override
            public Set> getFields() {
                return at.getFields();
            }

            @Override
            public Class getJavaClass() {
                return at.getJavaClass();
            }

            @Override
            public Set> getMethods() {
              return at.getMethods();
            }

            @Override
            public  X getAnnotation(final Class annType) {
              if (Named.class.equals(annType)) {
                  class NamedLiteral extends AnnotationLiteral
                      implements Named {

                      // value()が繰り返し呼ばれるのでキャッシュする
                      String qualifiedName;

                      @Override
                      public String value() {
                          if (qualifiedName == null) {
                               // getClass()ではなく、getJavaClass()を使うように修正
                               Package pkg = at.getJavaClass().getPackage();
                               String unqualifiedName = at.getAnnotation(
                                  Named.class).value();

                              // valueの値が設定されていない場合の対応
                              if (unqualifiedName.isEmpty()) {
                                  String className =
                                        at.getJavaClass().getSimpleName();
                                  unqualifiedName =
                                        StringUtils.uncapitalize(className);
                              }
                              qualifiedName = pkg.getAnnotation(
                                     Named.class).value()
                                     + '.' + unqualifiedName;
                              log("\t unqualifeidName=" + unqualifiedName);
                              log("\t qualifeidName=" + qualifiedName);
                         }
                         return qualifiedName;
                     }
                  }
                 return (X) new NamedLiteral();
                } else {
                   return at.getAnnotation(annType);
                }
            }
            // getAnnotationへの対応
            @Override
            public Set getAnnotations() {
               if (annotations.isEmpty()) {
                   for (Annotation a : at.getAnnotations()) {
                       annotations.add(getAnnotation(a.annotationType()));
                   }
                }
                return annotations;
             }

            @Override
            public Type getBaseType() {
                return at.getBaseType();
            }

            @Override
            public Set getTypeClosure() {
               return at.getTypeClosure();
            }

            @Override
            public boolean isAnnotationPresent(
               Class annType) {
                   return at.isAnnotationPresent(annType);
            }
         };
         event.setAnnotatedType(wrapped);
    }

    void log(String messages) {
        System.out.println("CDI: " + messages);
    }
}

この拡張モジュールではvalue()の定義を上書きして、package-info.javaに@Nameがついていた場合には”パッケージ名”.”Beanインスタンス名”のようにして参照できるように変更しています。

この拡張モジュールを動作させる準備として、あらかじめパッケージに対して@Nameで名前を宣言しておく必要があります。今回はWeld ディストリビューションのexamples/jsf/loginアプリケーションのアーカイブに上のQualifedName拡張モジュールを含ませてみます。パッケージ名を定義するパッケージとして以下のようなsrc/main/java/org/jboss/weld/examples/login/package-info.javaを定義します。

@Named("tanoseam")
package org.jboss.weld.examples.login;

import javax.inject.Named;

以上でorg.jboss.weld.examples.loginパッケージ配下のクラスで@Namedがついているもの(例えばUserManager)は、EL式としては#{tanoseam.userManger}で参照できるようになります。

JBoss AS 7.1CR1b と Weld 1.1.4 を使って実行した結果を以下に示します。

16:07:52,081 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic@Entity class org.jboss.weld.examples.login.User
16:07:52,085 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic@Named @RequestScoped @Default class org.jboss.weld.examples.login.Credentials
16:07:52,089 INFO [stdout] (MSC service thread 1-7) CDI: unqualifeidName=credentials
16:07:52,089 INFO [stdout] (MSC service thread 1-7) CDI: qualifeidName=tanoseam.credentials
16:07:52,091 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypeabstract interface@Named class org.jboss.weld.examples.login.package-info
16:07:52,091 INFO [stdout] (MSC service thread 1-7) CDI: unqualifeidName=tanoseam
16:07:52,091 INFO [stdout] (MSC service thread 1-7) CDI: qualifeidName=tanoseam.tanoseam
16:07:52,092 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic abstract interface class org.jboss.weld.examples.login.UserManager
16:07:52,095 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic class org.jboss.weld.examples.login.Resources
16:07:52,099 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic@Named @Alternative @RequestScoped @Stateful class org.jboss.weld.examples.login.EJBUserManager
16:07:52,099 INFO [stdout] (MSC service thread 1-7) CDI:     unqualifeidName=userManager
16:07:52,100 INFO [stdout] (MSC service thread 1-7) CDI:     qualifeidName=tanoseam.userManager
16:07:52,104 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic@Named @RequestScoped class org.jboss.weld.examples.login.ManagedBeanUserManager
16:07:52,104 INFO [stdout] (MSC service thread 1-7) CDI:     unqualifeidName=userManager
16:07:52,104 INFO [stdout] (MSC service thread 1-7) CDI:     qualifeidName=tanoseam.userManager
16:07:52,108 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic@Named @SessionScoped class org.jboss.weld.examples.login.Login
16:07:52,109 INFO [stdout] (MSC service thread 1-7) CDI:     unqualifeidName=login
16:07:52,109 INFO [stdout] (MSC service thread 1-7) CDI:     qualifeidName=tanoseam.login
16:07:52,138 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedType class org.tanoseam.examples.CollectBeanInfo$1
16:07:52,141 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedType class org.tanoseam.examples.QualifiedName$1$1NamedLiteral
16:07:52,147 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedType class org.tanoseam.examples.BeanRegistration$1
16:07:52,151 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic class org.tanoseam.examples.MyExtension
16:07:52,152 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic class org.tanoseam.examples.ContextRegistration
16:07:52,153 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic class org.tanoseam.examples.QualifiedName
16:07:52,154 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedType class org.tanoseam.examples.BeanRegistration$1$1
16:07:52,156 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedType class org.tanoseam.examples.BeanRegistration$1$2
16:07:52,158 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic class org.tanoseam.examples.InjectionTime
16:07:52,159 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic class org.tanoseam.examples.CollectBeanInfo
16:07:52,161 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedType class org.tanoseam.examples.MyExtension$1
16:07:52,165 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic class org.tanoseam.examples.ContainerLifecycleEvents
16:07:52,168 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic class org.tanoseam.examples.LifecycleLogger
16:07:52,170 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedType class org.tanoseam.examples.QualifiedName$1
16:07:52,173 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedType class org.tanoseam.examples.LifecycleLogger$1
16:07:52,177 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic class org.tanoseam.examples.MyApplicationContext
16:07:52,180 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedType class org.tanoseam.examples.InjectionTime$1
16:07:52,181 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic class org.tanoseam.examples.BeanRegistration
16:07:52,190 INFO [stdout] (MSC service thread 1-7) CDI: ProcessAnnotatedType: AnnotatedTypepublic class org.tanoseam.beans.MyBean

EJBUserManagerとManagedBeanUserManagerにおいてqualifeidName=tanoseam.userManagerと出力されているところを見てください。Loginサンプルでは、UserManagerインタフェースの実装としてEJBUserManagerとManagedBeanUserManagerの2つがそれぞれ@Named(“userManager”)と宣言されています。EJBUserManagerの方には@Alternativeが宣言されているので、インジェクション時にはManagedBeanUserManagerの方が選択されます。

アノテーションを指定したBeanの検索方法

前回のブログでBeanManagerを紹介しましたが、AnnotationLiteralの使い方の説明が足りなかったと思いますので補足します。

AnnotationLiteralを使うとアノテーションを「もの」として扱える

BeanManagerの使用例として取り上げたBeanManager:: getBeans(Type beanType, Annotation… qualifiers) はメソッド引数として限定子(Qualifier)アノテーションを要求しています。しかし、使用例ではこのメソッド引数としてAnnotationLiteralのインスタンスが指定されていました。

Set<Bean<?>> beans = beanManager.getBeans(Object.class, new AnnotationLiteral<Any>(){});

AnnotationはもともとインタフェースというのがJavaでの定義なので、それをメソッド引数として渡すためにはAnnotationインタフェースを実装した何らかのクラスが必要になります。クラスAnnotationLiteral<T extends Annotation>はCDIによって導入されたものでAnnotationインタフェースの実装をワンライナー(1行プログラム)として書けるようにしたものです。上の例では、アノテーションAnyを引数として渡すためにAnnotationLiteral<Any>のインスタンスを作成しています。

このように、APIを使って実行時にAnnotationを指定するというシーンでAnnotationLiteralが使われます。JavaのアノテーションはそもそもはJavaコード上に@Anyのようにしてメタデータを付加するのに使われるものです。しかし、今回のようにAPIを使ってアノテーションを指定して検索をするような場合にはアノテーションを「もの」として扱える方が便利です。

Instanceを使うとコンテキストからインスタンスを取得できる

AnnotationLiteralを使った別の例を紹介します。CDIにはAPIを使ってコンテキストにバインドしたインスタンスを取得するIntanceというクラスが提供されています。これはCDI拡張モジュール開発者のための機能というよりは、むしろアプリケーション開発向けに提供されている一般的な機能です(詳しくはWeld参照文書を見てください)。

@Inject Instance<PaymentProcessor> paymentProcessorSource;

実際にインスタンスを取得するにはInstanceに対してget()を実行します。get()を実行することで、CDIコンテナは指定されたクラスPaymentProcessorのBeanインスタンスを特定し、その結果を返します。つまり、アプリケーションがインジェクションのタイミングをget()によって制御できるようになります。

PaymentProcessor p = paymentProcessorSource.get();

しかし、コンテキスト上にPaymentProcessortという型の複数のBeanの候補が存在した場合(例えばPaymentProcessorのサブクラスのBeanが複数存在した場合)はCDIコンテナはBeanを1つに特定することができず、結果として例外がスローされます。そこで、次の例のように@Asynchronousという限定子を指定することでユニークがBeanが特定されるようにします。

@Inject @Asynchronous Instance<PaymentProcessor> paymentProcessorSource;

こうすることで例外は避けられるようになりましたが、このようにInstanceの宣言時に限定子を追加していくやり方では、(実行時にContexualなインスタンスを取得するという)Instanceを使う甲斐がありません。Instanceには実行時に検索条件として限定子を指定するselect() というメソッドが提供されていますので、そこでAnnotatlinLiteralを使います。

PaymentProcessor p = paymentProcessorSource
 .select(new AnnotationLiteral<Asynchronous>() {});

BeanManagerとInstanceの例からCDIでBeanの検索をするときにAnnotationLiteralが便利に使えることがわかりました。次回は、CDI拡張モジュールの作り方の話に戻って、AnnotationLiteralを使ってAnnotationの挙動を変更する方法について書くつもりです。

BeanManagerでCDIコンテナにアクセスする

CDI拡張モジュールを作る上でコンテナが管理する情報を取得・操作できたら便利です。今回はそれを可能にするBeanManagerについて紹介します。これは、今までCDI拡張モジュールのインタフェースでたびたび登場していたけど、説明をさぼっていたものです。

BeanManagerとは

BeanManagerは拡張モジュールからコンテナにアクセスするときに使うもので、これを使うことでBean、インターセプタ、デコレータを取得することができます。

BeanManagerを取得するには、Beanのクラス内において

@Inject BeanManager manager;

のようにインジェクションするだけです。

BeanManagerは次のような機能を提供します。

メタデータ関連

  • AnnotatedTypeを生成する
  • InjectionTargetを生成する

イベント関連

  • イベントを発行する

Bean関連

  • Beanを取得する
  • Beanリファレンスを取得する
  • CreationalContextを生成する
  • コンテキストを取得する

アノテーション関連

  • アノテーション定義を取得する
  • インターセプタバインディング定義
  • ステレオタイプ定義
  • 各種アノテーションの判別(限定子か否か、NormalScopeか否か, etc.)

依存性解決関連

  • Beanを特定する
  • デコレータを特定する
  • インターセプタを特定する
  • デコレータを特定する

メソッドの概要を表1にまとめます。正確な定義はJavaDocを参照してください。

表1:CDI 1.0 BeanManager
戻り値 メソッド 概要
AnnotatedType<T> createAnnotatedType(Class<T> type) 指定されたクラスまたはインタフェースのアノテーションを読むために利用できるAnnotatedTypeを取得する
CreationalContext<T> createCreationalContext(Contextual<T> contextual) 指定されたcontextualタイプまたは非contextualオブジェクトのためにcreationalContextを取得する
InjectionTarget<T> createInjectionTarget(AnnotatedType<T> type) 指定されたAnnotatedTypeのためのInjectionTargetを取得する
void fireEvent(Object event, Annotation… qualifiers) イベントを発火し、オブザーバに通知する
Set<Bean<?>> getBeans(String name) EL名レゾリューション規則に従って、指定されたEL名に対応するBeanのセットを返す
Set<Bean<?>> getBeans(Type beanType, Annotation… qualifiers) タイプセーフ・レゾリューション規則に従って、指定されたタイプと限定子に対応するBeanのセットを返す
Context getContext(Class<? extends Annotation> scopeType) 指定されたスコープタイプのアクティブなコンテキストを取得する
javax.el.ELResolver getELResolver() EL名によってBeanを特定するELResolverを返す
Object getInjectableReference(InjectionPoint ij, CreationalContext<?> ctx) 指定されたインジェクションポイントのためにインジェクション可能なリファレンスを取得する
Set<Annotation> getInterceptorBindingDefinition(Class<? extends Annotation> bindingType) 指定されたインターセプタバインディングタイプのためのメタアノテーションのセットを取得する
Bean<?> getPassivationCapableBean(String id) 指定されたIDを持つPassivationCapableBeanを返す
Object getReference(Bean<?> bean, Type beanType, CreationalContext<?> ctx) 指定されたBeanとBeanタイプのためのコンテキスト参照を取得する
Set<Annotation> getStereotypeDefinition(Class<? extends Annotation> stereotype) 指定されたステレオタイプのメタアノテーションを取得する
boolean isInterceptorBinding(Class<? extends Annotation> annotationType) 指定されたアノテーションがインターセプタバインディングか否か
boolean isNormalScope(Class<? extends Annotation> annotationType) 指定されたアノテーションがNormalScopeか否か
boolean isPassivatingScope(Class<? extends Annotation> annotationType) 指定されたアノテーションがPassivatingScopeか否か
boolean isQualifier(Class<? extends Annotation> annotationType) 指定されたアノテーションが限定子か否か
boolean isScope(Class<? extends Annotation> annotationType) 指定されたアノテーションがScopeか否か
boolean isStereotype(Class<? extends Annotation> annotationType) 指定されたアノテーションがステレオタイプか否か
Bean<? extends X> resolve(Set<Bean<? extends X>> beans) 曖昧な依存性解決規則を指定されたBeanのセットに対して適用する
List<Decorator<?>> resolveDecorators(Set<Type> types, Annotation… qualifiers) タイプのセットと限定子のセットのためのデコレータのリストを返す
List<Interceptor<?>> resolveInterceptors(InterceptionType type, Annotation… interceptorBindings) インターセプタバインディングとインターセプションのタイプのためのインターセプタのリストを返す
Set<ObserverMethod<?super T> resolveObserverMethods(T event, Annotation… qualifiers) イベントのためのオブザーバのリストを返す
void validate(InjectionPoint injectionPoint) 指定されたインジェクションポイントの妥当性を検証する
javax.el.ExpressionFactory wrapExpressionFactory(javax.el.ExpressionFactory expressionFactory) 指定されたExpressionFactoryのラッパーを返す。それはMethodExpressionとValueExpressionの生成をExpressionFactoryに移譲する。

コンテナが管理するBeanの取得

BeanManagerを使ってデプロイされたBeanにアクセスしてみましょう。以下のコードではすべてのBeanを集めてプリントします。


 @Inject

 private BeanManager beanManager;

 // ...

 Set<Bean<?>> beans = beanManager.getBeans(Object.class, new AnnotationLiteral<Any>(){});

 for (Bean<?> bean: beans) {

 System.out.println("bean=" + bean.getBeanClass().getSimpleName() +

 ", scope=" + bean.getScope().getSimpleName());

 }

これをWeldのexamples/jsf/login内で実行した結果は次の通り。このブログの過去の記事で作成したMyBeanやMyApplicationContext、それからContextRegistrationのような拡張モジュールまでWeld実装内部ではBeanとして管理されていることがわかります。
22:50:39,042 INFO  [stdout] (http--127.0.0.1-8080-1) bean=InjectionPoint,scope=Dependent
22:50:39,043 INFO  [stdout] (http--127.0.0.1-8080-1) bean=MyBean2,scope=Dependent
22:50:39,043 INFO  [stdout] (http--127.0.0.1-8080-1) bean=Validator,scope=Dependent
22:50:39,043 INFO  [stdout] (http--127.0.0.1-8080-1) bean=ValidatorFactory,scope=Dependent
22:50:39,044 INFO  [stdout] (http--127.0.0.1-8080-1) bean=MyApplicationContext,scope=Dependent
22:50:39,044 INFO  [stdout] (http--127.0.0.1-8080-1) bean=MyBean,scope=MyApplicationScoped
22:50:39,044 INFO  [stdout] (http--127.0.0.1-8080-1) bean=ContextRegistration,scope=ApplicationScoped
22:50:39,045 INFO  [stdout] (http--127.0.0.1-8080-1) bean=ManagedBeanUserManager,scope=RequestScoped
22:50:39,045 INFO  [stdout] (http--127.0.0.1-8080-1) bean=InstanceImpl,scope=Dependent
22:50:39,046 INFO  [stdout] (http--127.0.0.1-8080-1) bean=Resources,scope=Dependent
22:50:39,046 INFO  [stdout] (http--127.0.0.1-8080-1) bean=Conversation,scope=RequestScoped
22:50:39,046 INFO  [stdout] (http--127.0.0.1-8080-1) bean=Principal,scope=Dependent
22:50:39,047 INFO  [stdout] (http--127.0.0.1-8080-1) bean=EventImpl,scope=Dependent
22:50:39,047 INFO  [stdout] (http--127.0.0.1-8080-1) bean=UserTransaction,scope=Dependent
22:50:39,048 INFO  [stdout] (http--127.0.0.1-8080-1) bean=ManagedBeanUserManager,scope=RequestScoped
22:50:39,048 INFO  [stdout] (http--127.0.0.1-8080-1) bean=Login,scope=Dependent
22:50:39,049 INFO  [stdout] (http--127.0.0.1-8080-1) bean=BeanManagerImpl,scope=Dependent
22:50:39,049 INFO  [stdout] (http--127.0.0.1-8080-1) bean=Resources,scope=Dependent
22:50:39,049 INFO  [stdout] (http--127.0.0.1-8080-1) bean=User,scope=Dependent
22:50:39,050 INFO  [stdout] (http--127.0.0.1-8080-1) bean=Login,scope=SessionScoped
22:50:39,050 INFO  [stdout] (http--127.0.0.1-8080-1) bean=Resources,scope=Dependent
22:50:39,050 INFO  [stdout] (http--127.0.0.1-8080-1) bean=Credentials,scope=RequestScoped

BeanリファレンスとCreationalContext (3)

「BeanリファレンスとCreationalContext」というテーマで、過去2回に渡ってCreationalContextの役割を調べてきました。

今回はCreationalContextの破棄についてです。拡張モジュールを作るという流れからだいぶ横道に逸れてしまったので、これでCreationalContextをテーマにしたブログはひとまず区切りとしたいと思います。

Beanインスタンス破棄とCreationalContextとの間の関連

Beanのインスタンスが解放される流れを整理してみましょう。コンテキストが終了してから、それに関連付けられたBeanが破棄されるシーケンスは以下のようになります。

1. AbstractContext::destory()
1-1 Contextual::destroy(T instance, CreationalContext<T> creationalContext)
1-1-1.  <<コンテキストの種別に依存した処理>>
1-1-2.  CreationalContext::release()

CDI仕様書では意外なことにContextインタフェースにdestroyのようなpublicメソッドは定義されていません。Weld1.1.2実装では、org.jboss.weld.context.AbstractContextでdestroy()メソッドが定義されています。

Contextual::destroy()の具体例を見てみましょう。前回に引き続き、再びこのブログのBean<MyBean>の定義を見てください。MyBean::destroy()メソッドの引数としてCreationalContextが引き渡されています(これはcreate()で渡されたものと同じインスタンスである必要があります)。destory() メソッド実装内部ではpreDestroy()やdispose()のメソッドの実行後に、CreationalContextのrelease()が呼び出されています。

public void destroy(MyBean instance,
                    CreationalContext ctx) {
 log("Bean::destroy");
 it.preDestroy(instance);
 it.dispose(instance);
 ctx.release();
}

最後の行のrelease()メソッドは、Beanのすべての依存オブジェクトを破棄します。CreationalContextの実装は、release()が呼び出されたタイミングで、その中に蓄えているインスタンスを解放します。

CDIコンテナはどこでCreationalContextを保持するのか

Beanインスタンスが生成されるのはBeanがインジェクトされるときで、そのライフサイクルはコンテキストによって管理されます。CreationalContextが生成されるのは、トップレベルの(言い換えればRootになる)Beanインスタンスが生成されたとき、と考えられます。そして、コンテキストが終了するときに生成したときに使ったCreationalContextを使ってBeanインスタンスを破棄します。

では、一体、CDIコンテナはどこでCreationalContextのインスタンスを保持するのでしょうか。

Weld実装では、org.jboss.weld.context.api.ContextualInstanceがBeanに関する以下の3点の情報を保持していて、CDIコンテキストがこのContextualInstanceの集まりを管理するという構造になっています。

public interface ContextualInstance {
 public T getInstance(); // コンテキストで管理するインスタンス(例: MyBean)

 public CreationalContext getCreationalContext();

 public Contextual getContextual(); // インスタンスを生成するBean (例: Bean)
}

このContextualInstanceは、CDIのコンテキストのget()にアクセスしたときに、コンテキストにバインドするインスタンスが存在しない場合に生成されます(つまり、これまでBeanインスタンスと漠然と呼んでいたものが実はこのような構造を持って実現されていたということです)。Weld実装ではAbstractContext.javaの125行めがこれに相当します。

さて、ここまでWeldのソースを見てきたおかげでCDIコンテキスト周辺の作りが理解できたような気がします。次回は、CDI拡張モジュールを作る上で重要なBeanManagerについて書こうと思います。

BeanリファレンスとCreationalContext (2)

BeanリファレンスとCreationalContextというテーマの続きです。前回のブログで、Beanインスタンスを生成するBean::create()の中で、(1) 実際にBeanインスタンスを生成するproduce()と、(2) インジェクションを実行するinject()の両方にメソッド引数として同じCreationalContextのインスタンスが渡されていていることを確認しました。このCreationalContextの役割を調べるのが今回のテーマです。

CreationalContextは@Dependentのコンテキスト

CreationalContextはDependent擬似スコープを実現するためにコンテナが提供するコンテキストです。

Dependent擬似スコープとは、Application/Session/Requestなどの通常のコンテキストとは異なる特殊なスコープです。この擬似スコープはCDIでのデフォルトスコープになっていて、明示的にスコープ宣言をしない場合はこのDependentスコープが適用されます。複数の異なるインジェクションポイントから同じBeanインスタンスを共有しないことになっています。

インジェクションが発生するとき、インジェクションポイントにはBeanインスタンスではなく、Beanリファレンスが設定されます。Beanリファレンスの内容は、BeanスコープがNormalScope否かによって変わります。

NormalScope

  • Beanリファレンスとしてクライアントプロキシが生成される。

Dependent擬似スコープ

  • BeanリファレンスとしてBeanインスタンスが生成される。
DependentスコープのBeanインスタンスを生成すると仮定しましょう。Beanインスタンスは他のBeanインスタンスを@Injectによって参照しているしているかもしれません。最初のトップレベルのBeanを生成するときにCreationalContextが作られ、生成するメソッド引数として引き渡されます。

さらに、このトップレベルのBeanインスタンスを作成する途中でインジェクションが発生し、参照する先のインスタンス(これもDependentスコープ)が作成された場合を考えます。そのときに発生するインジェクションでは、inject()の引数として同じCreationalContextが引き渡されます。そして、生成されたBeanインスタンスはこのCreationalContextに次々にpush()されます。

こうして、あるBeanインスタンスを生成する過程で、CreationalContextにBeanインスタンスが次々と蓄積されていきます。再帰的に処理が進み、最終的にトップレベルのBeanのcreate()に制御が返ると、CreatinalContextには依存関係にあるBeanインスタンスが登録されているはずです。

Weld 1.1.2でCreationalContextが使われているところ

CreationalContextの具体的な使い方を知るにはWeldのソースを読むのが良いでしょう。まずCreationalContextにBeanインスタンスをpush()をしているところです。

  • ManagedBean.javaの235行を見るとproduce()メソッド内で生成したBeanインスタンスを引数であるCreationalContextにpush()しています。SessionBeanなど他のBeanでも同様です。

次はCreationalContextにpush()したインスタンスを参照するところです。インジェクションのときにBeanリファレンスを作成するので、BeanManager::getInjectableReference()の実装を調べるとよいでしょう。

BeanManagerImpl::getInjectableReference()は内部でBeanManagerImpl::getReference(InjectionPoint injectionPoint, Bean<?> resolvedBean, CreationalContext<?> creationalContext)を呼び出しています。

  • BeanManagerImpl.javaの765行から777行までのコードでは、メソッド引数のcreationalContextにすでにBeanインスタンスが登録されていたら、クライアントプロキシを生成するのではなく、その代わりにcreationalContextにpush()で登録されたインスタンスを返しています。
ここで、なぜクライアントプロキシではなく、その代わりにBeanインスタンスを返すのかを考えてみます。

CreationalContextのもう一つの役割とは

CreationalContextのもう一つの役割は循環依存関係に関わると考えています(前回のブログを見て)。これはCDI仕様書には明確に説明されていないことなので、以下は私個人の解釈になります。鵜呑みにはしないでくださいね。

具体例をもとに考えてみます。仮に、インジェクションによって A → B → C → B のように依存関係が一巡しているとしたら何が起こるでしょうか。Dependent擬似スコープの場合は、循環依存関係があった場合は、同じBeanインスタンスを再び生成しないという明白なメリットがあります。では、NormalScopeの場合はどうでしょうか。以後、A → B → C → BにおけるそれぞれのBeanはNormalScopeであるとします。

CDIのインジェクションは、この順において、A を起点としてB → Cまではクライアントプロキシを生成するでしょう。そして、 C → Bでの2回めのBをインジェクトしようとしたとき、それはCreationalContextを見ることでBが登録済みのBeanインスタンスであることがわかるので、Cに対して(すでに生成済みの)Beanインスタンスをインジェクトすることでしょう。これによって、クライアントプロキシの生成と、プロキシに付随して発生するコンテキストルックアップを防ぐことができるでしょう。

しかし、気になる点があります。A → B → C → Bの依存関係で、Cに対してBのクライアントプロキシではなくBのBeanインスタンスをインジェクトすることによって別の問題は発生しないのでしょうか。

クライアントプロキシの場合は、動的にコンテキストをルックアップするので、前回のスコープが終了している場合には新たにBeanインスタンスが生成されます。一方でBeanインスタンスをインジェクトしてしまうとこのような動的なインスタンスの変更はできません。例えば、CのスコープがSessionScopedで、BのスコープがRequestScopedの場合、WebアプリケーションからBのインスタンスにアクセスするときに毎回新しく生成されなければなりませんが、それには対応できません。

でも、よくよく考えてみると、そのような状況が発生したときには、A → BにおいてBのプロキシにアクセスすると、Bのスコープが切り替わることでBの新しいBeanインスタンスを返すはずです。これによって、(古いB) → C → Bという循環参照構造はゴミになっていることでしょうから、C→Bの参照が古くなってしまうことは気にする必要はありません。もし、問題が発生するとしてら、Cのインスタンスをどこかのフィールドから直接参照している場合です。

@Injectによって生成したBeanインスタンスを@Injectしている以外のフィールドから参照するということは、すでにコンテナによって削除されたはずのBeanインスタンスを掴んでいる可能性があるということです。CDIではBeanインスタンスの生成・削除はコンテナに任せるのが基本になります。コンテキストによって生成したBeanインスタンスをインジェクション以外の手段でフィールド等から参照するのは避けた方がよさそうです。

次回は、Beanインスタンスの削除とCreationalContextとの間の関係を調べます。

BeanリファレンスとCreationalContext (1)

CreationalContextとは

長くなるので、これから何回かに分けてCreationalContextについて書こうと思います。

インタフェース javax.enterprise.context.spi.CreationalContext はContexual実装によってインスタンスの生成と破棄の間に使われるオペレーションを提供します。このCreationalContextのインスタンスはBeanのインスタンスが生成されるときに、メソッド引数として引き渡されるものです。

public interface CreationalContext<T> {
 public void push(T incompleteInstance);
 public void release();
}

このCreationalContextの役割を理解するため、Beanインスタンスの生成のプロセスを復習してみましょう。このブログのBean<MyBean>の定義を見てください。MyBean::create()メソッドの引数として渡されたCreationalContextは、メソッド実装内部で呼び出されるproduce()やinject()のメソッド引数としても引き渡されています。

public MyBean create(CreationalContext<MyBean> ctx) {
 log("Bean<MyBean>::create");
 MyBean instance = it.produce(ctx);
 it.inject(instance, ctx);
 it.postConstruct(instance);
 return instance;
}

Beanインスタンス生成とCreationalContextとの間の関連

コンテキストへのアクセスからBean(つまりContexual)の生成、インジェクションまでの処理の流れを整理すると次のようになります。

1. Context::get(Contextual<T> contextual, CreationalContext<T> creationalContext)
2. Contexual::create(CreationalContext<T> ctx)
2-1.   InjectionTarget::produce(CreationalContext<T> ctx)
2-1-1   Bean::createInstance(CreationalContext<T> ctx)
2-1-2   CreationalContext::push(T instance)
2-2 InjectionTarget::inject(T instance, CreationalContext<T> ctx)

1: コンテキストに対してget()を呼び出します
2: もしコンテキストに当該Beanが登録されていなければBeanインスタンスを生成します。
2-1: Beanインスタンスを生成するためInjectionTarget::produce()を呼び出します。
2-1-1: produce()の内部で、Beanインスタンスを生成します。
2-1-2: 生成したBeanインスタンスをCreationalContext::push()します。
2-2: Beanインスタンスのフィールドやメソッドでのインジェクションを実行するためInjectionTaget::inject()を呼び出します

上の処理の流れからわかることは、2-1-2でContextにpushしたCreationContextインスタンスがinject()のメソッド引数に渡されることによって、inject()の実装がこれからインジェクトしようとしているBeanインスタンスがすでに生成されたものであるか否かを判断できるということです。

CDI仕様書が言っていること

CDI 1.0仕様書の6.1.1. The CreationalContext interfaceを見るとpush()の説明が次のように書いてあります。

The implementation of Contextual is not required to call push(). However, for certain bean scopes, invocation of push() between instantiation and injection helps the container minimize the use of client proxy objects (which would otherwise be required to allow circular dependencies). 訳:Contextualの実装はpush()を呼び出す必要はありません。しかし、Beanスコープによっては、生成とインジェクションの間でpush()を呼び出すことによって、コンテナがクライアントプロキシオブジェクトを呼び出す回数を最小化することができます(さもなければ、循環した依存関係を認める必要があります)。

これは、inject()実装がCreationalContextを利用することでクライアントプロキシを生成しない場合があるということを暗に言っています。 inject()がやっていることは、@Injectのフィールドやメソッドに対してクライアントプロキシを設定することです(この説明はこの日のブログを見てください)。

クライアントプロキシを生成する手段はBeanManagerの以下のメソッドで提供されています。

  • getInjectableReference(InjectionPoint ij, CreationalContext<?> ctx)

Weld 1.1.2実装では、BeanManagerインタフェースの実装はBeanManagerImplクラスが提供していますので、そのメソッド実装を確認すれば仕様書が意図していることがわかるかもしれません。次回は、実際にソースコードを調べてみましょう。

ProcessInjectionTargetイベント:インジェクションの割り込み

CDI(Contexts and Dependency Injection)仕様でコンテキストと並んで重要なコンセプトはインジェクションです。典型的なインジェクションは次のように実行されます。

  • いつ:対象となるインスタンスが生成されるときに
  • どこで:@Injectが宣言されたフィールドやメソッドに対して
  • だれが:CDIコンテナが
  • なにを:スキャン済みのBeanからその型に合ったBeanを特定し
  • どうやって:コンテキストからBeanインスタンスを取得して注入します

CDI Portable Extensionでは、このインジェクションの機能はInjectionTargetインタフェースによって提供されます。CDIではBeanのcreate()メソッド内でこのInjectionTargetを使ってBeanインスタンスの生成(produce)、破棄(dispose)とインジェクション(inject)を行います。インスタンスに対してpostConstruct, preDestroyのコールバックメソッドを呼び出すメソッドもそれぞれ用意されています。

図. InjectionTargetインタフェース

具体例を見てみましょう。前回のブログでコンテキストの振る舞いについて書きました。次のスタックトレースのMyApplicationContext::get()の箇所を見ると、ManagedBean::create()からManagedBean$ManagedBeanInjectionTarget<T>.produce()が呼び出されています。確かにBeanの実装はInjectionTarget使ってインスタンスを生成しています。

MyBean.() line: 11
NativeConstructorAccessorImpl.newInstance0(Constructor, Object[]) line: not available [native method]
NativeConstructorAccessorImpl.newInstance(Object[]) line: 39
DelegatingConstructorAccessorImpl.newInstance(Object[]) line: 27
Constructor.newInstance(Object...) line: 513
WeldConstructorImpl.newInstance(Object...) line: 239
ConstructorInjectionPoint.newInstance(BeanManagerImpl, CreationalContext) line: 134
ManagedBean.createInstance(CreationalContext) line: 385
ManagedBean$ManagedBeanInjectionTarget.produce(CreationalContext) line: 234
ManagedBean.create(CreationalContext) line: 338 
MyApplicationContext.get(Contextual, CreationalContext) line: 28
ContextBeanInstance.getInstance() line: 99
ProxyMethodHandler.invoke(Object, Method, Method, Object[]) line: 87
MyBean$Proxy$_$$_WeldClientProxy.toString() line: not available

InjectionTargetを実装してProjectInjectionTargetイベントにセットすることでインジェクションの機能を上書きすることができます。これを使ってもともとのインジェクションの機能自体を置き換えることはないと思いますが、インジェクションが発生するタイミングで、その処理をインターセプトして何らかの処理を行うことが可能です。

ProcessInjectionTargetイベント

InjectionTarget実装を設定する先はProcessInjectionTargetイベントです。このイベントは、このブログに書いたようにCDIコンテナのブート時にコンテナによって送信されるイベントになります。

ProcessInjectionTargetイベントは、@Observes ProcessInjectionTarget<T>のようにイベントハンドラを定義しておくと、Beanスキャンの過程でアーカイブに含まれる各Beanごとにイベントを受信できます。

次のコードはインジェクションにかかる時間を計測してログに出力する拡張モジュールのサンプルです。このサンプルはInjectionTarget実装を作成してinject()を上書きし、オリジナルのinject()の前後で時刻を測定することでインジェクションの時間を求めています。

public class InjectionTime implements Extension {

public InjectionTime() {
 log("InjectionTime::constructor");
 }

public  void processInjectionTarget(@Observes ProcessInjectionTarget pit) {
 final AnnotatedType at =pit.getAnnotatedType();
 log("InjectionTime::processInjectionTarget: at=" + at);

final InjectionTarget it = pit.getInjectionTarget();

 InjectionTarget wrapped = new InjectionTarget() {

public void inject(T instance, CreationalContext ctx) {
 log("Begin inject: at=" + at);
 long start = System.currentTimeMillis();
 it.inject(instance, ctx);
 long end = System.currentTimeMillis();
 log("End inject: injectionTime=" + (end - start) + " ms");
 }

public void postConstruct(T instance) {
 it.postConstruct(instance);
 }

public void preDestroy(T instance) {
 it.dispose(instance);
 }

public void dispose(T instance) {
 it.dispose(instance);
 }

public Set getInjectionPoints() {
 return it.getInjectionPoints();
 }

public T produce(CreationalContext ctx) {
 return it.produce(ctx);
 }
 };

pit.setInjectionTarget(wrapped);
 }

この拡張モジュールのログは次のようになります。CDIコンテナによるBeanスキャンのときにProcessInjectionTargetイベントをキャッチしているのがわかります。実際にinject()が呼び出されるのはアプリケーションの実行時になります。

00:31:45,075 INFO  [stdout] (MSC service thread 1-1) CDI: InjectionTime::processInjectionTarget: at=public@MyApplicationScoped class org.tanoseam.beans.MyBean
00:31:45,094 INFO  [stdout] (MSC service thread 1-1) CDI: InjectionTime::processInjectionTarget: at=public class org.jboss.weld.examples.login.Resources
00:31:45,095 INFO  [stdout] (MSC service thread 1-1) CDI: InjectionTime::processInjectionTarget: at=public@SessionScoped @Named class org.jboss.weld.examples.login.Login
00:31:45,097 INFO  [stdout] (MSC service thread 1-1) CDI: InjectionTime::processInjectionTarget: at=public@Default @Named @RequestScoped class org.jboss.weld.examples.login.Credentials
00:31:45,107 INFO  [stdout] (MSC service thread 1-1) CDI: InjectionTime::processInjectionTarget: at=public@Named @RequestScoped class org.jboss.weld.examples.login.ManagedBeanUserManager
00:31:45,112 INFO  [stdout] (MSC service thread 1-1) CDI: InjectionTime::processInjectionTarget: at=public@Entity class org.jboss.weld.examples.login.User
00:31:45,118 INFO  [stdout] (MSC service thread 1-1) CDI: InjectionTime::processInjectionTarget: at=public class org.tanoseam.examples.MyApplicationContext
00:31:51,725 INFO  [stdout] (http--127.0.0.1-8080-3) CDI: Begin inject: at=public@SessionScoped @Named class org.jboss.weld.examples.login.Login
00:31:51,731 INFO  [stdout] (http--127.0.0.1-8080-3) CDI: End inject: injectionTime=5 ms
00:31:51,736 INFO  [stdout] (http--127.0.0.1-8080-3) CDI: Begin inject: at=public@Default @Named @RequestScoped class org.jboss.weld.examples.login.Credentials
00:31:51,737 INFO  [stdout] (http--127.0.0.1-8080-3) CDI: End inject: injectionTime=0 ms
00:31:53,691 INFO  [stdout] (http--127.0.0.1-8080-3) CDI: Begin inject: at=public@Named @RequestScoped class org.jboss.weld.examples.login.ManagedBeanUserManager
00:31:53,696 INFO  [stdout] (http--127.0.0.1-8080-3) CDI: Begin inject: at=public class org.jboss.weld.examples.login.Resources
00:31:53,696 INFO  [stdout] (http--127.0.0.1-8080-3) CDI: End inject: injectionTime=0 ms
00:31:53,706 INFO  [stdout] (http--127.0.0.1-8080-3) CDI: End inject: injectionTime=15 ms

前回と今回のブログで、コンテキストとインジェクションのそれぞれのおおまかな動きが理解できたと思います。でも、シーケンスの細部を見ると、Producer::produce()のパラメーターとして渡されていたCreationalContextがどのような働きをしているかがわかりません(このCreationalContextはContext::get()のパラメーターとしても登場していました)。次回は、インスタンスの生成時に使われるCreationalContextについて書くつもりです。

%d人のブロガーが「いいね」をつけました。