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について書くつもりです。

CDIコンテキストの振る舞い

拡張モジュールを作ることでカスタムコンテキストを定義できるというのはCDIの大きな魅力のひとつです。Seam 3のfacesモジュールではViewScopedというカスタムスコープが提供されています。自分でも試しにコンテキストを作ってみたいものです。

ここで問題になるのが、いったいどうやったらカスタムコンテキストを作れるのかということです。もちろん、CDIのContextインタフェースを実装して、コンテナに登録するまでは仕様書からわかるのですが、どうやってコンテキストの開始や終了を制御するのかが不明です。知りたいのはコンテキストを開発するときの作法です。

カスタムコンテキストの挙動

前回のブログではMyApplicationContextというコンテキストを定義して、AfterBeanDiscoveryイベントにそれを登録し、実際にそのカスタムコンテキストを動作させて、ログを出力させてみました。ログを見ると、getScope(), isActive(), get()のようなメソッドが次々と呼び出され、その結果、MyBeanのインスタンスが最終的にアクセスされていました。

MyBeanのインスタンスの違いがわかるようにログを出力すると、次のようなになります。今回はログを取り直す際に、MyBeanのコンストラクタとtoString()メソッドでインスタンスのidがわかるようにコードを修正しています。

08:25:26,312 INFO  [stdout] (http–127.0.0.1-8080-1) CDI: MyBean::constructor=org.tanoseam.beans.MyBean$Proxy$_$$_WeldClientProxy@12fa9ae
08:25:26,358 INFO  [stdout] (http–127.0.0.1-8080-1) CDI: MyApplicationContext::isActive
08:25:26,359 INFO  [stdout] (http–127.0.0.1-8080-1) CDI: MyApplicationContext::get(Contextual<T>)
08:25:26,359 INFO  [stdout] (http–127.0.0.1-8080-1) CDI: Get from Context: instance=null
08:25:26,360 INFO  [stdout] (http–127.0.0.1-8080-1) CDI: MyApplicationContext::get(Contextual<T>, CreationalContext<T>)
08:25:26,360 INFO  [stdout] (http–127.0.0.1-8080-1) CDI: MyBean::constructor=org.tanoseam.beans.MyBean@12edf9a
08:25:26,361 INFO  [stdout] (http–127.0.0.1-8080-1) CDI: MyBean::initialize
08:25:26,361 INFO  [stdout] (http–127.0.0.1-8080-1) CDI: Put to Context: instance=org.tanoseam.beans.MyBean@12edf9a
08:25:26,361 INFO  [stdout] (http–127.0.0.1-8080-1) *************** bean=org.tanoseam.beans.MyBean@12edf9a

シーケンス図

ログに出力されたMyApplicationContext関連の挙動をシーケンス図を描いてみたのが下図になります(初回のMyBean::toString()の呼び出しまで)。

図. コンテキスト関連のシーケンス図

この図はCDIコンテナとコンテキストの関係を整理するために代表的なクラスだけをピックアップしてざっくりと描いたものになります(図中ではプロキシから直接コンテキストに呼び出しがあるように読めますが、実際のシーケンス中にはたくさんのクラスが関係しています)。

図中、4.1と4.2で2回get()が呼ばれていますが、それらは引数の異なる別のメソッドです。4.1のgetでコンテキストにバインドしていないことをチェックした後に、4.2のgetを使ってインスタンスを生成しています。
  • 4.1の方はget(Contextual<T> contextual) で、これはコンテキストにバインドされたBeanインスタンスを取得するためのものです
  • 4.2の方はget(Contextual<T> contextual, CreationalContext<T> creationalContext) で、これはコンテキストにバインドされていない場合は、インスタンスを生成します

このシーケンス図のポイントは次の2点です。

  • インジェクトされたMyBeanのインスタンスはプロキシである
  • プロキシにアクセスがあるとコンテキストにバインドしたMyBeanのインスタンスを取得する(バインドしていなければ、インスタンスを生成する)

Contextはコンテキスト管理をカプセル化する

コンテキストのget()メソッドにアクセスすることでBeanのインスタンスを取得できるということは、コンテキストの実装がインスタンスライフサイクルを制御できるということです。コンテキストの実装がインスタンスを破棄すれば、次回コンテキストにアクセスしたときはコンテキストは新規のインスタンスを生成することになるでしょう。
カスタムコンテキストを作るということは、このようなインスタンスの生成・破棄の独自のスコープを新たに定めるということになります。では、一体、どのタイミングでコンテキストにバインドしたインスタンスを破棄するのでしょうか。その答えは、Context実装が独自に定めることになります。例えば、TransactionContextのようなカスタムコンテキストであれば、実際のアプリケーションサーバー上のトランザクションの境界がコンテキストの終了のタイミングを定めることでしょう。
ここで注意しなければならないのは、Context実装のインスタンスは一つで、それ自身はAfterBeanDiscoveryイベントに登録されたまま変わらないということです。概念としてのコンテキストは複数存在したり、寿命が短く何度も新たに生成されるとしても、個々のコンテキストはContext実装内部で管理されます。そこで、CDI仕様は、コンテナがコンテキストにアクセスするときに利用可能なコンテキストがContext実装内部に現在存在するかをチェックしています。これがContext::isActive()メソッドになります。
まとめると、CDI Contextインタフェースは、コンテキストにバインドするインスタンス全体のライフサイクルを管理するものです。Contextインタフェースで要求しているメソッドはisActive()やget()など数少なく、コンテキストの寿命を管理するのはContextの実装になります。Contextの実装はコンテキストが実現しようとしているスコープの性格に応じて多様な実装があり得るはずです。

AfterBeanDiscoveryイベント:コンテキストの追加

CDI(Contexts and Dependency Injection)仕様の主要なテーマは、文字通り、(1) コンテキストと(2) 依存性注入になります。今回は、前者のカスタムコンテキストを追加する方法について書きます。

CDI仕様では、Request, Conversation, Session, Applicationといったコンテキストが提供されていますが、これに独自のカスタムコンテキストを追加するには、AfterBeanDiscoveryイベントのaddScopeメソッドを使います。このメソッドの引数としてカスタムコンテキストのインスタンスを登録します。

次のCDI拡張モジュールContextRegistrationは、MyApplicationContextというカスタムコンテキストをCDIコンテナに登録する例です。

package org.tanoseam.examples;

import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.Extension;

public class ContextRegistration implements Extension {

public void afterBeanDiscovery(@Observes AfterBeanDiscovery event,
 BeanManager bm) {
 log("ContextRegistration::AfterBeanDiscovery");
 event.addContext(new MyApplicationContext());
 }

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

MyApplicationContextは@MyApplicationScopedというスコープに対応するコンテキストです。CDIのコンテキストはContextインタフェースを実装する必要があります。

このコンテキストの機能としてはこのスコープで生成されたインスタンスを保持するだけの単純な実装になっています。

注意:このサンプルはCDIの挙動を確認するためにログ出力をするシンプルな実装です。本格的な拡張モジュールを開発するときには、コンテキストの用途に応じて排他制御などを十分に考慮してください。

package org.tanoseam.examples;

import javax.enterprise.context.spi.Context;
import javax.enterprise.context.spi.Contextual;
import javax.enterprise.context.spi.CreationalContext;
import java.lang.annotation.Annotation;
import java.util.concurrent.ConcurrentHashMap;

public class MyApplicationContext implements Context {
 ConcurrentHashMap<contextual<?>, Object> instanceMap = new ConcurrentHashMap<contextual<?>, Object>();

@Override
 public Class getScope() {
 log("MyApplicationContext::getScope");
 return MyApplicationScoped.class;
 }

@Override
 public <T> T get(Contextual<T> contextual, CreationalContext<T> creationalContext) {
 log("MyApplicationContext::get(Contextual<T>, CreationalContext<T>)");
 Object instance = instanceMap.get(contextual);
 if (instance != null) {
 log("\tGet from Context: instance=" + instance);
 }
 else {
 if (creationalContext != null) {
 instance = contextual.create(creationalContext);
 }
 log("\tPut to Context: instance=" + instance);
 instanceMap.put(contextual,instance);
 }
 return (T) instance;
 }

@Override
 public <T> T get(Contextual contextual) {
 log("MyApplicationContext::get(Contextual<T>)");
 Object instance = instanceMap.get(contextual);
 log("\tGet from Context: instance=" + instance);
 return (T) instance;
 }

@Override
 public boolean isActive() {
 log("MyApplicationContext::isActive");
 return true;
 }

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

@MyApplicationScopedの定義は次のようになります。

package org.tanoseam.examples;

import javax.enterprise.context.NormalScope;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@NormalScope(passivating = false)
public @interface MyApplicationScoped {
}

@MyApplicationScopedの動作を確認するため、前回のブログで作成したMyBeanにこのスコープアノテーションを設定します。こうしておけば、MyBeanのインジェクションが発生したときに、生成されたMyBeanのインスタンスはMyApplicationContextにバインドされるはずです。

package org.tanoseam.beans;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.tanoseam.examples.MyApplicationScoped;

@MyApplicationScoped
public class MyBean {

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

// ...

CDI拡張モジュールtanoseam-examples-0.1.jarとしては、前回のBeanRegistrationではなく、今回のContextRegistrationを起動するように、あらかじめjavax.enterprise.inject.spi.Extensionファイルの内容をorg.tanoseam.examples.ContextRegistrationとなるように修正しておきます(CDI拡張モジュールの構造についてはここを参考にしてください)。

ここまで準備できたところでContextRegistrationモジュールの動作確認です。前回と同様weld-login.warをJBoss AS7にデプロイし、http://localhost:8080/weld-login/を開いてから、View Usersリンクをクリックします。

17:01:26,365 INFO  [stdout] (MSC service thread 1-1) CDI: ContextRegistration::AfterBeanDiscovery
17:01:26,365 INFO  [stdout] (MSC service thread 1-1) CDI: MyApplicationContext::getScope
17:01:46,126 INFO  [stdout] (http--127.0.0.1-8080-2) CDI: MyBean::constructor
17:01:46,157 INFO  [stdout] (http--127.0.0.1-8080-2) CDI: MyApplicationContext::isActive
17:01:46,158 INFO  [stdout] (http--127.0.0.1-8080-2) CDI: MyApplicationContext::get(Contextual<T>)
17:01:46,158 INFO  [stdout] (http--127.0.0.1-8080-2) CDI:       Get from Context: instance=null17:01:46,159 INFO  [stdout] (http--127.0.0.1-8080-2) CDI: MyApplicationContext::get(Contextual<T>, CreationalContext<T>)
17:01:46,159 INFO  [stdout] (http--127.0.0.1-8080-2) CDI: MyBean::constructor
17:01:46,160 INFO  [stdout] (http--127.0.0.1-8080-2) CDI: MyBean::initialize
17:01:46,160 INFO  [stdout] (http--127.0.0.1-8080-2) CDI:       Put to Context: instance=MyBean String
17:01:46,161 INFO  [stdout] (http--127.0.0.1-8080-2) *************** bean=MyBean String

このログを見ると、MBeanのインスタンスが生成されてMyApplicationContextにバインド(つまりPutされるまで)にコンテナとカスタムコンテキストの間で一定のインタラクションがあることがわかります。

CDIコンテナは、コンテキストのisActiveを呼んでから、get(Contextual<T>)を呼び出しています。そして、get(Contextual<T>,CreationContext<T>)の中でMyBeanの生成とコンテキストへのバインドが行われています。

このようなCDIコンテナ、コンテキスト、Beanの間の呼び出し関係はJavaDocやCDI仕様書からはわかりにくい部分です。次回は、Contextインタフェースの各メソッドの意味を整理し、CDIコンテナとコンテキスト間のインタラクションについて引き続き調べていきます。

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