CDI(旧名WebBeans)入門 その2

http://d.hatena.ne.jp/shin/20100105/p1 の続き

今回はスコープ(インスタンスの寿命の管理)のお話。

まずはわかりやすくするためにコモンアノテーションをつける。

これはJava EE 5(つまりGlassfish V2とかTomcat 6とかの旧世代でも動く)やJava SE 6で定義されているもので、DIコンテナ等スコープを管理する場合実装されているべきものです。

EJB3でもSpringでもJSFでもServletでもフィルタでもリスナーでも対応していますね。ちなみにSpringは3.0でJSR-330対応しているのでこの連載で書いてることが注入方法だけはそのまま使えるはず。

カウンタ保持クラスを以下のようにしてみる。

package beans;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;


public class Counter {
    private int count = 0;

    public int getCount(){
        return ++this.count;
    }

    @PostConstruct
    private void init(){
        System.out.println("初期化おわったー");
    }
    @PreDestroy
    private void destroy(){
        System.out.println("破棄するよー");
    }
}

新しくプロジェクト作り直したのでメソッド名やパッケージが追加されたり多少変わっているが基本中身はそのまま。



実行結果は以下のとおり。紛らわしいがwebbeansってのがプロジェクト名にしてある。

情報: Loading application webbeans at /webbeans
情報: webbeans was successfully deployed in 1,203 milliseconds.
情報: 初期化おわったー
情報: サーバーのシャットダウンが開始されました
情報: 破棄するよー

アプリケーションが稼働中はインスタンスがずっと保持されるServletに注入されているのでシャットダウンのタイミングで呼ばれたのがわかる。ちなみに初回アクセスまではインスタンスは作られないみたい。遅延で作られているようです。


あと、たぶんアンデプロイでも呼ばれるはず。というわけでアンデプロイしてみた。

情報: Loading application webbeans at /webbeans
情報: webbeans was successfully deployed in 1,094 milliseconds.
情報: 初期化おわったー
情報: 破棄するよー

ちゃんと呼ばれてるね。


さて、ここまでは前置き、そしてこれから本題に入る。
では一般的なリクエストスコープを見てみよう。

〜略〜
import javax.enterprise.context.RequestScoped;

@RequestScoped
public class Counter {
〜略〜

実行してみる。2回ほど表示。

情報: Loading application webbeans at /webbeans
情報: webbeans was successfully deployed in 1,109 milliseconds.
情報: 初期化おわったー
情報: 破棄するよー
情報: 初期化おわったー
情報: 破棄するよー

見てわかるとおりインスタンスそのものが注入されているわけではなく、EJB等と同じように参照を持っているだけのようだ。したがってサーブレットのような寿命が長い場合ものに注入しても問題はない。

すばらしい。


続いてセッションスコープ。

〜略〜
import javax.enterprise.context.SessionScoped;

@SessionScoped
public class Counter implements Serializable{
〜略〜

ポイントはシリアライズされる可能性があるためにその指定を入れてること。これがないとデプロイ時にはねられる。


実行。ブラウザ2つ開いてみた。

情報: Loading application webbeans at /webbeans
情報: webbeans was successfully deployed in 1,109 milliseconds.
情報: 初期化おわったー
情報: 初期化おわったー
情報: サーバーのシャットダウンが開始されました
情報: 破棄するよー
情報: 破棄するよー

シャットダウン時に破棄されたようだ。ちなみにGlassfishは再デプロイ時にセッション維持することもできるので、生成や破棄のタイミングが意図しないように表示されても混乱しないように。ちゃんとどういうモードで設定しているかを見分けるのだ。


では次にアプリケーションスコープ。アプリケーションが有効な間ずっと保持し続けるのでサーブレットに注入した場合はスコープなしとの違いはわからない。

というわけで、大きいスコープから狭いスコープを注入してみる。具体的にはアプリケーションスコープにセッションスコープを注入。
サーブレットから呼び出されるクラス。

package beans;

import java.io.Serializable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

@ApplicationScoped
public class Counter implements Serializable{

    @Inject
    CounterHolder ch;

    public String getCount(){
        return this.ch.next() + "回目";
    }

    @PostConstruct
    private void init(){
        System.out.println("初期化おわったー");
    }
    @PreDestroy
    private void destroy(){
        System.out.println("破棄するよー");
    }
}


実際にカウンタを保持するクラス

@SessionScoped
public class CounterHolder implements Serializable{
    private int count = 0;

    public int next(){
        return ++count;
    }

    @PostConstruct
    private void init(){
        System.out.println("ほるだーの初期化おわったー");
    }
    @PreDestroy
    private void destroy(){
        System.out.println("ほるだーの破棄するよー");
    }

}


実行っ!

情報: Loading application webbeans at /webbeans
情報: webbeans was successfully deployed in 1,094 milliseconds.
情報: 初期化おわったー
情報: ほるだーの初期化おわったー
情報: ほるだーの初期化おわったー
情報: サーバーのシャットダウンが開始されました
情報: ほるだーの破棄するよー
情報: ほるだーの破棄するよー
情報: 破棄するよー

ブラウザ2つ開いています。セッションはそのときに生成されているのがわかるでしょうか。でもアプリケーションスコープは最初と最後だけです。


ではもっとわかりやすくなるようにリクエストスコープをセッションスコープのクラスに注入してみましょう。
セッション保持側

〜略〜
@SessionScoped
public class CounterHolder implements Serializable{

    @Inject
    RequestScopeHoge hoge;

    private int count = 0;

    public int next(){
        hoge.toString();//だみーでよんでみる

        return ++count;
    }
〜略〜

リクエスト保持側

package beans;

import java.io.Serializable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.context.RequestScoped;

@RequestScoped
public class RequestScopeHoge implements Serializable{

    @PostConstruct
    private void init(){
        System.out.println("りくえすとの初期化おわったー");
    }
    @PreDestroy
    private void destroy(){
        System.out.println("りくえすとの破棄するよー");
    }
}

実行!

情報: Loading application webbeans at /webbeans
情報: webbeans was successfully deployed in 1,093 milliseconds.
情報: 初期化おわったー
情報: ほるだーの初期化おわったー
情報: りくえすとの初期化おわったー
情報: りくえすとの破棄するよー
情報: りくえすとの初期化おわったー
情報: りくえすとの破棄するよー
情報: ほるだーの初期化おわったー
情報: りくえすとの初期化おわったー
情報: りくえすとの破棄するよー
情報: りくえすとの初期化おわったー
情報: りくえすとの破棄するよー
情報: サーバーのシャットダウンが開始されました
情報: ほるだーの破棄するよー
情報: ほるだーの破棄するよー
情報: 破棄するよー

一つ目のブラウザで2回アクセス、その後2つ目のブラウザで2回アクセス、シャットダウンをしています。

サーブレット>アプリケーションスコープ>セッションスコープ>リクエストスコープ
という順番で呼ばれていくのがわかると思います。

セッションスコープのクラスのnext()メソッドにてダミーでメソッドを読んでいますが、これがないとインスタンスの生成がされません。これは実行時に解決されるためで、JTA等のようにスレッドローカル等で処理されているのでしょう。EJBの注入のようにあくまでも参照のみを持つのです。


広いスコープ狭いスコープを意識せずにつなぐことができる、これがCDIのすごさです。

つまり、フロントエンド等はViewに関するロジックのためのものはシングルトンで、画面に表示する箇所(つまり入力パラメータも)やリクエスト固有のViewヘルパー的なものはリクエストスコープ、セッションスコープのものはそのまま表示用にもってきてもいいし、リクエストを経由して間接的にアクセスさせてもいい、といったことが可能です。つまり、Struts 1.xのアクションクラスやT2Frameworkのページクラス等のようなものと相性がいいかもしれません。

Servlet 3.0の非同期API時にリクエストスコープ周りがどうなるかは調べないといけないですけど、なかなか面白いのではないでしょうか。

あ、あと

@Singleton

ってのもあります。会話スコープはとりあえず今回は無視で。


コモンアノテーションがついたメソッドをprivateにしてるとか、わざとスコープの広いほうに注入させてるとか、おいらのいやらしさがにじみでてますね。いつもなら日本語メソッドやってますが、これをしていないだけましと思ってますけど。

そろそろCDIのよさがわかってきたころかなと思います。