JavaEE7をはじめよう(最終回) - CDI 拡張機能による独自スコープの作成
今回は Java EE 7 連載の最終回として、CDI の拡張機能を紹介する。
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 内に該当ファイルを作成する方法でも実現可能である。)
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. フラッシュスコープアノテーションの作成
最初に、フラッシュスコープであることを示すアノテーションとして@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つだ。
FlashScoped
アノテーションが付いたクラスを CDI Bean として登録すること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
アノテーション経由で受信することで、任意の処理を定義する。
ここで受信するイベントは以下の2つである。
BeforeBeanDiscovery
- CDI コンテナがクラスパス内の Bean を検索する前のイベントである。ここで、FlashScoped
アノテーションも検索対象に追加している。AfterBeanDiscovery
- Bean の検索が終わった後のイベントである。ここで、Context
実装を追加している。
上記の他にどのようなライフサイクルイベントがあるかは、javax.enterprise.inject.spi
の Javadoc や参考資料を参照いただきたい。
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
メソッドでは引数のContextual
(Bean
にキャスト可能)からどのクラスに対する 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 ファイルの場所は以下の通りである。
サンプルアプリケーションの稼動の都合上、Maven からも取得可能となっている。
参考資料
Chapter 16. Portable extensions
インジェクション時に任意の処理を行うなど、様々なサンプルが提示されている。なるほど!ザ・Weld | Exploring CDI extensions
CDIの拡張機能に関する日本語情報が充実しており、大変参考になった。
連載まとめ
さて、長らく続けてきた Java EE 7 の連載も、今回が最終回である。
この連載では、これまで以下の要素について概要を紹介してきた。
上記の他にも、Java EE 7 は以下のような技術を含んでいる。
- JSF (Java Server Faces)
- jBatch
- EJB
- JTA
連載当初は Java EE 7 に関する日本語の参考情報はほとんどなかったが、現在では参考書籍もそろってきた。 紹介できなかった技術についてはそちらを参照してほしい。
Java EE 7 は過去の J2EE と比較して、効率よくかつ簡単に開発できるようになっている。 また、標準仕様ということもあり商用サポートも期待できるため、 Java アプリケーションを構築したい場合は有力な選択肢になるだろう。
[前多 賢太郎]