エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

JavaEE7をはじめよう(最終回) - CDI 拡張機能による独自スコープの作成

今回は Java EE 7 連載の最終回として、CDI拡張機能を紹介する。

CDI は拡張ポイントを提供して独自の拡張ができるようにしている。具体的に可能なのは以下のようなことである。

  • 独自スコープを追加する
  • CDI Beanでないクラスを CDI Bean として登録し、インジェクション可能にする
  • CDI の振る舞いを変更する

フラッシュスコープとは

今回は、フラッシュスコープという独自スコープの作成を題材にして、CDI拡張機能を紹介する。

ここでいうフラッシュスコープとは、最初の HTTP リクエスト終了後から、次の HTTP リクエスト終了までが生存期間となるスコープである。

主な用途は、リダイレクトを行う画面遷移でリダイレクト後もデータを保持しておくことである。

例えば、データベースの更新を行う URL の/updateと、データの表示を行う URL の/searchがあるとする。 データベースの更新後に最新データを表示させたい場合、/updateの応答として/searchにリダイレクトさせるようにすれば、/updateでデータの再表示を行う処理が不要になる。

ブラウザ側から見ると、データ更新の処理は1回の画面遷移に見えるし、URLが/searchになるので、リロードや戻るボタンによって、再度更新処理が動くこともなくなるので有用だ。 このような画面遷移方式を、PRG (POST-REDIRECT-GET) パターンと呼ぶ。

PRG パターンで問題となるのは、最初の POST でメッセージなどを設定して、リダイレクト後の画面にそのメッセージを表示したい場合である。 ここで、リクエストスコープにメッセージを設定してしまうと、PRG パターンでは2回リクエストを行うため、メッセージが消失する。 かといって、セッションスコープを使用するのもデータを削除する必要があるため、面倒である。

フラッシュスコープはこのような問題を解消する仕組みで、Ruby on Rails や、 Spring, JSF などで実装されている。 (ちなみにJSF のフラッシュスコープは独自の実装であり、アノテーションによる定義を行わない。)

今回の記事では、CDI 拡張機能を利用して、Java EE 全般で使用可能な、アノテーションベースのフラッシュスコープを作成する。

CDI 拡張のためのAPI

CDI の拡張ポイントに関する API の多くは、javax.enterprise.inject.spiパッケージにある。

拡張機能は jar ファイルとして提供し、jar 内のMETA-INFなどに拡張用の定義ファイルを置くことができる。 拡張機能の利用側は、jar ファイルをクラスパスに含めるだけで利用できる。 (再利用を考えないのであれば、war ファイル内を jar 化せず、war 内に該当ファイルを作成する方法でも実現可能である。)

CDIを拡張するためのAPI のうち主要なものを示す。

  • javax.enterprise.inject.spi.BeanManager - CDI Bean の取得、検索、登録などの各種処理を行う主要なクラス。
  • javax.enterprise.inject.spi.Bean - CDI Bean の情報を持つ。CDIアノテーションの設定の参照や、 Bean のインスタンス生成が可能。
  • javax.enterprise.inject.spi.Extension - CDI 拡張ポイントを示すインターフェース。このインターフェースを実装したクラスが起点となって、拡張機能が開始される。
  • javax.enterprise.context.spi.Context - カスタムスコープに応じて実装する必要のあるインターフェース。カスタムスコープを付与した CDI Bean をどのタイミングで有効とするか、どのように取得するかを制御する。

拡張ポイント(Extension)の作成

以降では、独自スコープの作成方法を次の順で説明する。

  1. フラッシュスコープアノテーションの作成
  2. Extension実装クラスの作成
  3. サービス定義ファイルの作成
  4. Context実装クラスの作成
  5. Bean インスタンスを保持する仕組みの作成

1. フラッシュスコープアノテーションの作成

最初に、フラッシュスコープであることを示すアノテーションとして@FlashScopedを作成する。

@Scope
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE
       , ElementType.FIELD, ElementType.METHOD})
public @interface FlashScoped {}

ここでは@Scopeアノテーションを付与することで、スコープ用のアノテーションであることを示している。

2. Extension実装クラスの作成

次に、javax.enterprise.inject.spi.Extensionを実装したクラスを作成する。これにより、CDI コンテナの初期処理の各ライフサイクルで、任意の処理を実行できるようになる。

サービス定義ファイルに指定したExtensionの実装オブジェクトは CDI コンテナによって生成される。 その後、 CDI コンテナが行う様々な処理の特定のタイミングで、Extensionの実装オブジェクトに定義した処理を呼び出すことで、CDI の機能を拡張できる。

今回行いたいのは以下の2つだ。

  1. FlashScopedアノテーションが付いたクラスを CDI Bean として登録すること
  2. FlashScopedアノテーションが付いた CDI Bean を取り扱うContext実装クラスを登録すること

a.では、自作アノテーションを付与したクラスを、CDI Bean として扱うように CDI コンテナに設定する。これにより、@Injectを付与した別の Bean のフィールドに注入が可能になる。

b.では、自作アノテーションを付与した CDI Bean のライフサイクルを設定する。a.によって CDI Bean として登録できたとしても、その Bean オブジェクトをいつ生成し、いつ破棄するかというライフサイクルがわからない。Context実装クラスの役割は、CDI Bean のライフサイクルを設定することである。このように、CDI のスコープはアノテーションContext実装クラスによって決まる。

コードは以下のようになる。

public class FlashScopeExtension implements Extension {
    
    public void beforeBeanDiscovery(
            @Observes BeforeBeanDiscovery event,
             BeanManager beanManager) {

        // a. FlashScopedアノテーションをCDI Bean の収集対象とする。
        event.addScope(FlashScoped.class, true, true);
    }

    /**
     * After bean discovery.
     *
     * @param event the event.
     * @param beanManager  beanManger.
     */
    public void afterBeanDiscovery(
            @Observes AfterBeanDiscovery event,
            BeanManager beanManager) {  
         
        // b. FlashScoped Bean を扱うコンテキスト実装クラスを登録
        event.addContext(new FlashScopeContext());
    }
}

Extensionインターフェースにはメソッドが定義されていない。 任意の処理を定義するためには、上記のように、任意のメソッドの引数に@Observesを付与したライフサイクルイベントの引数を設定する。

CDI コンテナは、初期処理の各ライフサイクルでライフサイクルに応じたイベントを発行する。 Extension実装クラスはライフサイクルイベントを@Observesアノテーション経由で受信することで、任意の処理を定義する。

これは 以前の記事で説明した CDI イベントである。

ここで受信するイベントは以下の2つである。

  • BeforeBeanDiscovery - CDI コンテナがクラスパス内の Bean を検索する前のイベントである。ここで、FlashScopedアノテーションも検索対象に追加している。
  • AfterBeanDiscovery - Bean の検索が終わった後のイベントである。ここで、Context実装を追加している。

上記の他にどのようなライフサイクルイベントがあるかは、javax.enterprise.inject.spiJavadoc や参考資料を参照いただきたい。

3. サービス定義ファイルの作成

上記で作成したExtension実装を有効とするためには、サービス定義ファイルを作成する必要がある。 方式は決まっており、 jar ファイル内のMETA-INF/servicesディレクトリにjavax.enterprise.inject.spi.Extensionという名前のファイルを作成し、そこにExtension実装クラスの完全修飾名を書いておく。

このようにしておくと、各 jar ファイルが読込まれるときに、上記のファイルに記載されたクラスがロードされる。

また、META-INF配下にbeans.xmlを置くことができる。beans.xmlを置くことで、この jar ファイル内のみの CDI の設定を行うことが可能だ。

4. Context実装クラスの作成

Extension実装クラスのafterBeanDiscoveryメソッドで登録したFlashScopeContextクラスのコードを次に示す。

public class FlashScopeContext implements Context,Seriarizable
    
    // 1. FlashScopedアノテーションを取り扱う事を示す。
    @Override
    public Class<? extends Annotation> getScope() {
        return  FlashScoped.class;
    }
    
    // リクエストスレッドに対して常に有効である事を示す。
    @Override
    public boolean isActive() {
        return true;
    }

    /** 
     * 2. CDI Beanの初回取得時に呼ばれる。
     *
     * @param cntxtl CDI Bean(どのBeanの取得要求かがわかる)
     * @param cc bean CDI Beanの生成情報 
     * @return CDI Beanのインスタンス
     */
    @Override
    public <T> T get(Contextual<T> cntxtl, CreationalContext<T> cc) {
        Bean<T> bean = (Bean<T>) cntxtl;
        FlashContextBeanStore beanHolder = getBeanStore();
        if (beanHolder.containsBean(bean)) {
            return beanHolder.getBean(bean).getInstance();
        }
    // Beanのインスタンスを生成し、
        // インスタンスをストアに保存した後、呼び出し元に返す。
        T t = bean.create(cc);
        beanHolder.putBean(bean, cc, t);
        
        return t;
    }

    /**
     * 3. 一度取得したことのあるCDI Beanの取得時に呼ばれる。
     * 
     * @param cntxtl bean CDI Bean
     * @return CDI Beanのインスタンス
     */
    @Override
    public <T> T get(Contextual<T> cntxtl) {
        Bean<T> bean = (Bean<T>) cntxtl;
        // Bean をストアから取得して返却する。
        FlashContextBeanStore beanHolder = getBeanStore();
        if (beanHolder.containsBean(bean)) {
            return beanHolder.getBean(bean).getInstance();
        }
        return null;
    }

    // 4 Beanのストアの取得
    private FlashContextBeanStore getBeanStore() {
        return BeanStoreHolder.get();
    }
    
}

Context実装クラスでは、getScopeメソッドによってどのアノテーションに対して有効かを示し、getメソッドで CDI Bean の取得方法を提供する。大事なのは、getメソッドである。

getメソッドでは引数のContextualBeanにキャスト可能)からどのクラスに対する Bean の取得要求かがわかる。 初回の取得であればインスタンスを生成して返してしまえばよいのだが、その時にそのインスタンスを保持して、次の取得要求で同じインスタンスを返さないと、インスタンスの状態が維持できない。 インスタンスの保持方法については、CDI では規定していないので、独自に考える必要がある。

5. Bean インスタンスを保持する仕組みの作成

CDI Bean インスタンスを保持する仕組みとしては以下の候補が考えられる。

  • ThredLocal - スレッドごとに固有の情報を持てる。
  • HTTPSession - セッションごとに固有の情報を持てる。
  • 任意の実装によるシングルトンなど - JVM 単位に固有の情報を持てる。

フラッシュスコープはリクエストをまたぐので、HTTPSessionに保持するのが自然であろう。

ここでは、セッションに保持する Bean の保存先として、FlashContextBeanStore というクラスを定義した。

public class FlashContextBeanStore {
    
    private final Map<Class, FlashInstance> store 
        = new ConcurrentHashMap<>();
    
    public <T> FlashInstance<T> getBean(Bean<T> bean) {
        return store.get(bean.getBeanClass());
    }
    // 省略

中身は、ClassをキーとするMapであり、Bean のインスタンスのクラスを見れば、インスタンス化されているかどうかがわかるようになっている。

Mapの値はFlashInstanceというクラスで、以下のような定義とした。

public static class FlashInstance<T> {
    
    // 状態
    /** 1回目のリクエストはリダイレクトであったか */
    private boolean sendRedirect;
    /** 2回目のリクエストが終わったか */
    private boolean endRedirect;
    
    private Bean<T> bean;
    private CreationalContext<T> ctx;
    private T instance;
    
    /**
     * リクエスト終了後に、状態を更新する。
     * @param callSendRedirect リダイレクトが行われれたか 
     */
    public void mark(boolean callSendRedirect) {
        // 初回のリクエストでリダイレクトの場合、sendRedirectを更新
        if (!sendRedirect && callSendRedirect) {
            sendRedirect = true;
            return;
        }
        // リダイレクト後のリクエストの場合、endRedirectを更新
        if (sendRedirect) { 
            endRedirect = true;
        }
    }
    
    /**
     * このBeanが破棄可能かを判定する。
     *
     * 破棄が可能なのは、リダイレクトのリクエストが終わったか、
     * 1回目のリクエストがリダイレクトでなかったかのどちらかである。
     * @return ok - true
     */
    public boolean isDestory() {
        return (sendRedirect && endRedirect)
                || (!sendRedirect && !endRedirect);
    }
    
    public T getInstance(){
        return instance;
    }
}

インスタンスに関する情報の他、リダイレクトが行われたかどうかなどの状態を持ち、その状態を変更するメソッドを持つ。

では、そのメソッドはどこで呼ぶべきだろうか。

HTTP リクエストが終了するタイミングで呼ぶ必要があることから、どのような HTTP リクエストに対しても動作するようにFilterで行うのが適切と判断した。

@WebFilter(urlPatterns = "/*")
public class FlashContextFilter implements Filter{

    @Override
    public void doFilter(ServletRequest request,
            ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        // a.リクエストからセッションを取得し、BeanStoreを取得する。
        // BeanStoreが無い場合は生成する。
        HttpServletRequest req = (HttpServletRequest) request;
        HttpSession session = req.getSession();
        String key = FlashContextBeanStore.class.getCanonicalName();
        FlashContextBeanStore beanStore = 
            (FlashContextBeanStore)session.getAttribute(key);
        
        if (beanStore == null) {
            beanStore = new FlashContextBeanStore();
            session.setAttribute(key, beanStore);
        }
        // b.ThreadLocal経由で ContextにもBeanStoreを渡す。
        BeanStoreHolder.set(beanStore);
        try {
            chain.doFilter(request, response);
        } finally {
            BeanStoreHolder.remove();
        }
        
        // c.レスポンスを取得し、BeanStoreの状態を更新する。
        // 削除可能となったインスタンスは破棄する。
        HttpServletResponse res = (HttpServletResponse) response;
        
        for(FlashContextBeanStore.FlashInstance i :
                beanStore.getBeans().values()) {
            
            i.mark(res.getStatus() == HttpServletResponse.SC_FOUND);

            if (i.isDestory()) {
                beanStore.destroyBean(i);
            }
        }
    }

a.では、セッションへのFlashContextBeanStoreの取得と生成を行っている。

b.では、取得したBeanStoreをContext実装クラスで扱えるように、ThreadLocal経由で引き渡している。

c.では、リクエスト終了後、FlashContextBeanStoreインスタンスの状態を更新して、削除可能なインスタンスを削除している。

Bean の保持や破棄の方法は、どのようなスコープを定義するかで大きく異なるが、参考になれば幸いである。

実行サンプル

以前のCDI のトランザクションの記事で紹介したサンプルは、PRG パターンに沿って実現している。表示しているメッセージは、今回紹介した@FlashScopedスコープで実現している。

Bean の定義には、以下のように@FlashScopedをつけるだけだ。 それだけで、あとは@Injectionによってインジェクション可能となる。

@FlashScoped
@Named
public class FlashMessage implements Serializable{
    
    private String message;
    
    //getter/setter
}

ソースファイル

ソースファイルの全体は、以下の github で公開している。

GitHub - enterprisegeeks/cdi-flash-scope: Provides FlashScoped CDI Bean on CDI 1.1

また、jar ファイルの場所は以下の通りである。

https://github.com/enterprisegeeks/cdi-flash-scope/tree/mvn-repo/com/github/enterprisegeeks/cdi-flash-scope

サンプルアプリケーションの稼動の都合上、Maven からも取得可能となっている。

参考資料

連載まとめ

さて、長らく続けてきた Java EE 7 の連載も、今回が最終回である。

この連載では、これまで以下の要素について概要を紹介してきた。

  • JPA
  • CDI
  • WebSocket
  • Bean Validation
  • Concurrency Utilities

上記の他にも、Java EE 7 は以下のような技術を含んでいる。

連載当初は Java EE 7 に関する日本語の参考情報はほとんどなかったが、現在では参考書籍もそろってきた。 紹介できなかった技術についてはそちらを参照してほしい。

Java EE 7 は過去の J2EE と比較して、効率よくかつ簡単に開発できるようになっている。 また、標準仕様ということもあり商用サポートも期待できるため、 Java アプリケーションを構築したい場合は有力な選択肢になるだろう。

[前多 賢太郎]