本エントリではJavaEEの中でも屈指の便利機能、CDI(Contexts and Dependency Injection)を紹介します。
なお、本エントリではJavaEE7に搭載されているCDI1.1を前提とします。JavaEE6はCDI1.0となり、本エントリのコードに一部動作しなくなる箇所がありますのでご注意ください。
CDIとは
CDIとは、JavaEE6から採用された、JavaEEにおけるDI機能のことです。DIについての詳しいことはこちらのエントリをご覧いただくとして、本エントリではCDIの特筆すべき機能に焦点を当てることにします。
CDIの真髄・スコープ管理
CDIの真髄はインスタンスの存命期間、すなわちスコープを管理してくれることです。
Webアプリケーションを作っていると「このオブジェクトの持つ情報をしばらくの間保持したままにしておきたい」と思うことがあります。例えば次のようなケースです。
設定ファイルの内容
Webアプリ起動中は不変な情報であり、一度ファイルから読み込んだ内容をメモリにキャッシュして読み取り専用情報として使いたい。いわゆるシングルトンとして扱いたい。
DBに格納されたコードテーブル(コード値をラベル文字列に変換するテーブル)の内容
変更される可能性があるが、だからと言って頻繁に使う処理だから毎回DBにアクセスするのは遅すぎる・・・リクエストの先頭でまとめてDBから読み込んでリクエスト中のみ有効なキャッシュとしたい。
CDIがない場合
CDIなしに前述のような要求を実現するのは不可能ではありませんが、とても間違えやすく、また変更しにくいプログラムが必要です。試しにリクエスト単位で動作するカウンターを持つクラスを作ってみましょう。
まずはServletです。
@WebServlet(urlPatterns = "/cdi-sample") public class CDISampleServlet extends HttpServlet { @Override protected void doGet(final HttpServletRequest pReq, final HttpServletResponse pResp) throws ServletException, IOException { ScopedService.Factory.get(pReq).countUp(); ScopedService.Factory.get(pReq).countUp(); pResp.setContentType("text/plain"); pResp.getWriter().println("Counter Value -> " + ScopedService.Factory.get(pReq).getCounter()); } }
次にServletから使うServiceです。
public class ScopedService { private int counter = 0; public void countUp() { this.counter++; } public int getCounter() { return this.counter; } public static class Factory { public static ScopedService get(final HttpServletRequest pReq) { final String KEY = ScopedService.class.getName(); ScopedService ret = (ScopedService) pReq.getAttribute(KEY); if (ret == null) { ret = new ScopedService(); pReq.setAttribute(KEY, ret); } return ret; } } }
いかがでしょうか?確かにこれで「リクエスト単位で状態を保持する」という要求を実現できたのですが、かなりいや〜な感じの実装になっていますね。
- ScopedService.Factory.get()メソッドの実装が煩雑。そもそもこんなメソッドを作らないといけないのが手間
- もしスコープに変更が入ったら・・・例えばシングルトンに変更する場合、マルチスレッドに気を払いながらScopedService.Factory.get()メソッドの中を修正する必要がある
これをCDIを使って書き換えてみましょう。
CDIがある場合
まずServletです。
@WebServlet(urlPatterns = "/cdi-sample") public class CDISampleServlet extends HttpServlet { @javax.inject.Inject ScopedService scopedService; @Override protected void doGet(final HttpServletRequest pReq, final HttpServletResponse pResp) throws ServletException, IOException { this.scopedService.countUp(); this.scopedService.countUp(); pResp.setContentType("text/plain"); pResp.getWriter().println("Counter Value -> " + this.scopedService.getCounter()); } }
@Injectアノテーションが最大のポイントです。このアノテーションが付いているインスタンス変数には、CDIが適切なオブジェクトをDIしてくれます。
次にServletから使うServiceです。
@javax.enterprise.context.RequestScoped public class ScopedService { private int counter = 0; public void countUp() { this.counter++; } public int getCounter() { return this.counter; } }
こちらは@RequestScopedアノテーションが最大のポイントです。このアノテーションが付いているクラスのオブジェクトは、CDIにより同じリクエスト内では使い回されます。つまりこのクラスのオブジェクトはリクエストスコープになるわけです。
そしてアノテーションが付いている以外は、ごくごく普通のクラスになりました。
スコープを変更する
CDIのすごいところは、アノテーションを変えるだけでスコープを簡単に変えることができる点です。先ほどのScopedServiceをリクエストスコープからアプリケーションスコープ(=シングルトン、Webアプリ中に1つしかないオブジェクト)に変更してみます。
@javax.enterprise.context.ApplicationScoped public class ScopedService { private int counter = 0; public void countUp() { this.counter++; } public int getCounter() { return this.counter; } }
全コードを掲載するのはバカバカしいのですが、あえて掲載しました。変わったのは先頭のアノテーションのみです。
たったこれだけでスコープが変わりました。Servlet側は何も変更していません。すごいですね。
少し複雑な話・スコープの短いオブジェクトのDIがなぜ可能か
さて、CDIの凄さを知るために、少し複雑な話をします。
これまでの例では、何気なくServletにリクエストスコープのServiceをDIしていました。Servletはアプリケーションスコープで動作します。そこにリクエストスコープのオブジェクトをDIして、正しく動作するのでしょうか?
結論から言うと、もちろん正しく動作します。CDIはこのようなケース、つまり自身よりスコープの短いオブジェクトをDIするケースでは、「プロキシ」というオブジェクトを生成し、その中で適切に処理対象を振り分けるのです。
裏でこれだけのことをやってのけるCDIですが、使う側はアノテーションを付けるだけ!
驚異の高機能をごく簡単な記述で使う・・・CDIってすごいですね。
@Injectの別の付け方
ここまで@Injectアノテーションはインスタンス変数に付けていましたが、コンストラクタやsetterに付けることも出来ます。この場合、DIしたいオブジェクト引数に取るようにします。次の例はコンストラクタに付ける場合のサンプルです。
@WebServlet(urlPatterns = "/cdi-sample") public class CDISampleServlet extends HttpServlet { private final ScopedService scopedService; @javax.inject.Inject public CDISampleServlet(final ScopedService pScopedService) { this.scopedService = pScopedService; } @Override protected void doGet(final HttpServletRequest pReq, final HttpServletResponse pResp) throws ServletException, IOException { this.scopedService.countUp(); this.scopedService.countUp(); pResp.setContentType("text/plain"); pResp.getWriter().println("Counter Value -> " + this.scopedService.getCounter()); } }
こちらの方がテストしやすくなるため、より望ましいと思います。
CDIの美点
スコープ制御が不要に
これまで見てきた通り、スコープ制御のための複雑なコードが不要になります。
クラスの実装がシンプルに
スコープ制御のコードがなくなり、クラスには本来の処理のみを記述すればよくなります。これによりコードがシンプルになります。
テスタビリティが向上
これはCDIというよりはDIの美点です。
@Injectを付けておくと、実行時に適切なオブジェクトを生成/注入(=インジェクション)してくれるのがDIの機能ですが、これによりnewを書かずに済み、状況に応じたオブジェクトの差し替えが可能になります。このためテスト用のモックを適用しやすくなり、テスタビリティが向上します。
この辺りの話は「newの連鎖」という言葉で解説されているWebページがありますので参考にしてください。
CDIのその他の機能
CDIには他にも便利な機能が多数あります。中でも有用な2つの機能をご紹介します。
AOPインターセプター
メソッドの前後に任意の処理を織り込むことをAOP、織り込む処理のことをインターセプターと言います。CDIでは簡単にインターセプターを作ることができます。
インターセプターを作るには次の2つのモジュールを作成します。
- アノテーション:メソッドにどのインターセプターを適用するか、というマーカーとなるアノテーションを作ります。
- インターセプタークラス:メソッドの前後に織り込む処理を実装します。
ここではメソッドの名前と実行後の戻り値を、コンソールに表示するインターセプターを作ってみます。
マーカーとなるアノテーション
package javaee.sandbox.interceptor; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.interceptor.InterceptorBinding; @InterceptorBinding @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface MethodPrint { // nodef }
マーカーとなるアノテーションはほとんどん場合このパターンで実装できます。
インターセプタークラス
package javaee.sandbox.interceptor; import javax.enterprise.context.Dependent; import javax.interceptor.AroundInvoke; import javax.interceptor.Interceptor; import javax.interceptor.InvocationContext; @Interceptor @MethodPrint @Dependent public class MethodPrintInterceptor { @AroundInvoke Object intercept(final InvocationContext pContext) throws Exception { System.out.println(pContext.getMethod().getName() + "メソッド実行開始."); final Object ret = pContext.proceed(); System.out.println(pContext.getMethod().getName() + "メソッド実行終了. 戻り値 -> " + ret); return ret; } }
型宣言に付いた2つのアノテーション、@Interceptorと先に作ったマーカーとなるアノテーション(@MethodPrint)がポイントです。@Interceptorにより、このクラスがインターセプタークラスであると宣言しています。また@MethodPrintにより、どのマーカーアノテーションに反応するのかを示しています。
織り込む処理はメソッドとして実装します。次のようなルールで作成します。
- @AroundInvokeアノテーションを付与
- 名前は何でもい
- 戻り値はObject型
- 引数はInvocationContext型1つのみ
- 本来のメソッドを実行するにはInvocationContext#proceed()メソッドを実行
ここでは、本来のメソッドの実行前後にコンソール表示を行っています。また、InvocationContext#proceed()メソッドの戻り値は本来のメソッドの戻り値です。場合によってはこの戻り値を加工して差し替える、などということも可能です。
beans.xmlに追記
インターセプターを使うにはbeans.xmlにクラス名を書いておく必要があります。beans.xmlはWEB-INFディレクトリ直下に起きます。内容は以下のようにします。
<?xml version="1.0" encoding="UTF-8"?> <beans bean-discovery-mode="annotated" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"> <interceptors> <class>javaee.sandbox.interceptor.MethodPrintInterceptor</class> </interceptors> </beans>
<class>タグにインターセプタークラスのフルネームを書きます。
インターセプタークラスを使う
インターセプターで処理を織り込みたいメソッドにマーカーとなるアノテーション(ここでは@MethodPring)を付与するだけで使えます。例を見てみましょう。
import javaee.sandbox.interceptor.MethodPrint; import javax.enterprise.context.ApplicationScoped; @ApplicationScoped public class ScopedService { private int counter = 0; public void countUp() { this.counter++; } @MethodPrint public int getCounter() { return this.counter; } }
たったこれだけです。簡単ですね。このクラスを実行すると、コンソールに次のように表示されます。
getCounterメソッド実行開始. getCounterメソッド実行終了. 戻り値 -> 4
@PostConstruct
この機能は便利・・・と言うより知らないと困るかもしれません。CDIで管理されるオブジェクトの初期化タイミングで何か処理をしたい、というのはよくあることです。例えば設定ファイルの中身をロードしてキャッシュしておく、というケースです。しかし、コンストラクタでは初期化できないことが多いです。コンストラクタの中ではDI対象のフィールドが初期化されていないことが多いからです。例えば下記のようなケースです。
@ApplicationScoped public class NonInitializationService { @Inject ConfigurationLoader loader; private final Map<String, String> configurationCache; public NonInitializationService() { // 下記はNG. // コンストラクタの中では this.service が null なので. this.configurationCache = this.loader.loadConfiguration(); } }
このような場合に使えるのが@PostConstructアノテーションです。このアノテーションを付けたメソッドは、全てのDIが完了した後に呼び出されますので、初期化処理には最適です。先ほどの例は次のように書き直すとうまく動作します。
@ApplicationScoped public class InitializationService { @Inject ConfigurationLoader loader; private Map<String, String> configurationCache; @PostConstruct void postConstruct() { this.configurationCache = this.loader.loadConfiguration(); } }
詳しい文献
本エントリではCDIを凝縮したエッセンスをご紹介しました。さらに詳しい情報を得たい方のために文献をご紹介しておきます。
Oracleによるチュートリアル(英語)
こちらのページへどうぞ。
日本語での説明
Qiitaの記事「JavaEE使い方メモ(CDI)」が最も詳しいと思われます。
まとめ
CDIのもたらすインパクトは大きいものです。アノテーションさえ付ければスコープ管理のための余計なコーディングが必要なく、コアな実装に集中することができます。結果、テストのしやすさや変更のしやすさ、コードの見通しの良さなど、様々な恩恵が得られるはずです。
うまく使って楽にWebアプリケーションを開発しましょう。