AbstractContextを使ったCDIコンテキストの実装 (2)

前回AbstractContextの概要について調べたので、すぐにそれを使ったコンテキストを作ってみたいところですが、まだ細部について理解できていないところもあると思います。今回は、実際に新規にコンテキストを作って見る前にコンテキストが提供すべき機能について調べてみようと思います。

ActiveContextの使用例

DeltaSpikeにおいてActiveContextのテストコードの一部としてDummyContextが作成されています。すでにActiveContextがコンテキストの基本機能を提供しているので、それを継承するDummyContextはすごくシンプルです。

public class DummyContext extends AbstractContext
{
    private boolean active = true;
    private boolean concurrent;
    private ContextualStorage storage = null;
    private BeanManager beanManager;

    public DummyContext(BeanManager beanManager, boolean concurrent)
    {
        super(beanManager);
        this.concurrent = concurrent;
        this.beanManager = beanManager;
    }

    @Override
    protected ContextualStorage getContextualStorage(boolean createIfNotExists)
    {
        if (storage == null && createIfNotExists)
        {
            storage = new ContextualStorage(beanManager, concurrent, isPassivatingScope());
        }

        return storage;
    }

    @Override
    public Class getScope()
    {
        return DummyScoped.class;
    }

    @Override
    public boolean isActive()
    {
        return active;
    }
}

このDummyContextは一旦作成された後はBeanを保持し続ける作りになっています。そのコンストラクタは DummyContext(BeanManager beanManager, boolean concurrent) となっているので、第二引数にtrueを渡すことでマルチスレッドに対応させることもできます。 ここで注目したいのは DummyContext の最後のメソッドである isActive() です。これは常に true を返せば良いものでは無いと思います。どんなとき true を返し、どんなとき false を返すのか。ちょっと、isActive() について調べてみましょう。

アクティブコンテキストとは何か

CDIコンテキストにはアクティブが否かを調べるための isActive() というメソッドがあります。コンテキストはどんなときにアクティブで、どんなときにアクティブでないのか。CDI 1.0仕様書 6.2 には isActive() について次のように記されています。「カレントスレッド」というのがミソです。

At a particular point in the execution of the program a context object may be active with respect to the current thread. When a context object is active the isActive() method returns true. Otherwise, we say that the context object isinactive and the isActive() method returns false.

訳:プログラムの実行中のある特定の点で、コンテキストはカレントスレッドについてアクティブになることができる。コンテキストはisActive() メソッドが true を返すときはアクティブである。さもなければ、そのコンテキストオブジェクトはインアクティブで、isActive() メソッドは false を返す。

さらに、CDI 1.0 仕様書 6.5.1 には次のようにも書かれています。

From time to time, the container must obtain an active context object for a certain scope type. The container must search for an active instance of Context associated with the scope type. (中略). If there is exactly one active instance of Context associated with the scope type, we say that the scope is active.

訳:コンテナはスコープに対応するアクティブコンテキストを保持しなければならないときがある。コンテナはスコープに関連したコンテキストのアクティブなインスタンスを検索しなければならない。(中略)そのスコープタイプに関連するコンテキストのアクティブインスタンスが一つしか存在しないのであれば、そのスコープはアクティブであるという。

また、CDI 1.0仕様書 6.7 には次のように組み込みコンテキストのアクティブな様子についても書かれています。

The built-in context object is active during servlet, web service and EJB invocations, or in the case of the conversation context object, for JSF requests.

訳:サーブレット、Webサービス、EJBの呼び出しの間、または、conversationコンテキストについてはJSFリクエストの実行中は、組み込みコンテキストはアクティブである。

以上から、リクエストの種類はさて置き、リクエストを実行中の状態を管理するためのものがアクティブコンテキストと言えそうです。

組み込みコンテキストがアクティブか否かの判断

では次に、組み込みコンテキストについてアクティブコンテキストの実装について考えてみましょう。

RequestコンテキストやConversationコンテキストの場合、HTTPリクエストの実行に対応するのはスレッドですから、スレッドごとにBeanを保持するための独立した領域を用意することになります。これは、JavaであればThreadLocalが自然な実装になるでしょう。Conversationコンテキストが複数のリクエストをまたがって存在する場合(long-running conversation)は、Conversation ContextはHTTP Session内に保存され、リクエストパラメタとして渡ってくるConversation ID (cid) によって識別されるはずです。cidに対応したConversation Contextが存在しない場合はisActive() はfalseを返すでしょう。

一方で、SessionコンテキストやApplicationコンテキストのように複数のスレッドで共有されるコンテキストについては、スレッドごとに独立した領域を用意する必要はありません。このようなタイプのコンテキストについては isActive() は多くの場合はtrueを返すでしょう。

いつ始まって、いつ終わるのか

Beanのインジェクションが起こるタイミングで、コンテキストにそのBeanがすでに登録されているか否かがチェックされます。その際、コンテキストがアクティブでなければContextNotActiveExceptionがスローされます。しかし、そのコンテキストが初期化・破棄されるタイミングはコンテキストの種類によって様々です。

組み込みコンテキストの場合は、コンテナはRequestコンテキストやSessionコンテキストはServletListenerによってそれぞれのコンテキストが初期化・破棄されるタイミングを知ることができます。ViewScopedコンテキストの場合は、初期化のタイミングは特になく、isActive() でUIViewRootが取得できるか否かでアクティブかどうかを決めています。破棄のタイミングは、JSFコンテナからのViewが破棄されるイベント(PreDestroyViewMapEvent)を受信したときです。

これらのコンテキストでは、HTTPリクエストとRequestコンテキストのように、Java EEの世界にコンテキストと対応するオブジェクトが存在していて、コンテキストとその実体のライフサイクルが同期するように風に作られています。したがって、組み込みコンテキストの初期化・破棄のタイミングはその実体が初期化・破棄されるタイミングでコンテキストに通知されるようになっています。

今回調べた範囲でコンテキストと実体の間の対応関係が理解できました。コンテキストがアクティブというのは、カレントスレッドがこの実体にアクセスできるということだと考えると納得ができます。また、コンテキストと実体の間の呼び出し関係は実装依存(密結合)になるため、両者の間は標準的なAPIとして一般化されていないのだと思います。そして、そこがコンテキストの初期化と破棄を理解する上でわかりにくくしている一因のように思います。

AbstractContextを使ったCDIコンテキストの実装 (1)

DeltaSpike 0.4-incubatingでは、現在、JSF関連のモジュールが開発中されているところです。最近になって、@ViewScopedスコープのコンテキスト実装がコミットされています(DELTASPIKE-266)ので、今回はこれの実装方法について調べてみましょう。

@ViewScopedとは

@ViewScopedはJSF 2.0で導入されたスコープでJSFのViewにBeanを格納できるものです(正確に言えば、UIViewRoot.getViewMap()で取り出すことができるマップに格納されます)。これを使うと、JSFの画面が有効な間だけ存在するようなBeanを管理できまので、Ajaxアプリケーションが画面ごとに保持するデータをキャッシュとして格納するのに利用できます。

JSF 2.0の仕様では、@ViewScopeに格納されるオブジェクトはCDI Beanではなく、@ManagedBeanとなっています。このため、@ViewScopedで宣言したBeanをCDIの@Namedと組み合わせて使うことはできないという制限があります(Stack Overflowの記事を参照のこと)。DeltaSpikeで実装している@ViewScopeはこの制限を取り除いた、CDIで実装したスコープになります。

CDIカスタムコンテキストの作り方

CDI Extension SPIを使ってカスタムコンテキストを実装するためには、CDIのContextインタフェースを満たすクラスを実装し、このコンテキストをコンテナに登録するようなCDI拡張を作る必要があります。一般的なコンテキスト実装の方法はこのブログで過去に書いた記事にありますので、参考にしてください。

@ViewScoped用のコンテキストを作るには次のようにすれば良いでしょう。

  • @ViewScopedに対応するコンテキストを実装する
  • そのコンテキストに格納するBeanはUIViewRoot.getViewMap()で取得するマップに格納する
  • このViewが破壊されるタイミングでコンテキストを破棄する(それに伴い、コンテキストに格納されたすべてのBeanを破棄する)

AbstractContext 抽象クラス

DeltaSpikeでは@ViewScopedを実装するにあたり、まずAbstractContextというカスタムコンテキスト実装のための抽象クラスを定義し(DELTASPIKE-274)、それを拡張する形でViewScopedContextクラスを実装しています。

AbstractContextにはカスタムクラスを作成するためのロジックがまとめられていて、そのサブクラスはBeanを管理するための詳細を提供するようになっています。AbstractContextを継承したViewScopedClassはコンテキストに格納するBeanを保存するためのContextualStorageを返すgetContextualStorage()を実装します。ContextualStorageは内部でContextualInstanceInfoのマップを保持します。ContextualInstanceInfoはBeanとCreationalContextから構成されます。なぜ、BeanとCreationalContextを一緒に管理する必要があるかについては以下の記事の「CDIコンテナはどこでCreationalContextを保持するのか」に書きましたのでご覧下さい。

さて、AbstractContextの構造と使い方がわかったので、次回は実際にこのクラスを使ってカスタムコンテキストを作ってみようと思います。