JavaEE7をはじめよう(18) - CDI インターセプターとステレオタイプ
以前の記事 で、 Java EE7から追加された @Transactional
による宣言的トランザクションについて解説した。その中で、glassfish4.1 では、ロールバック時に根本原因がわからないという不具合があることを述べた。
また、その対策として例外ログを出力するインターセプターを作成する方法を紹介した。
今回は、ログ出力を題材にCDIのインターセプターの実装を行う。
インターセプターとは
インターセプターとは、メソッドの実行前後にログを出すなど、どのような処理にも共通で実行させたい処理を1箇所に定義し、実行時に共通処理を各メソッドに組み込む仕組みである。 Java EE ではアノテーションによってインターセプターを定義して組み込むことができる。
なお、以前に紹介した @PreDestroy
,@PostConstruct
もインターセプターの一種であり、こちらはそれぞれのクラスに固有で設定するものだった。今回紹介するのは共通処理をインターセプター用のクラスに定義し、各クラスに組み込むものである。
ロガーの作成
まず、ログ出力のための仕組みを用意する。ログは任意のライブラリを用いてかまわないが、今回はJava標準のjava.util.logging
を用いる。
ロガーを提供する方法として、以前の記事で紹介した Producer を使うと、ロガーの設定や取得方法を一元化できるので便利だ。
@Dependent public class LoggerFactory { @Produces @Dependent public Logger getLogger(InjectionPoint ip) { return Logger.getLogger(ip.getMember().getDeclaringClass() .getPackage().getName()); } }
getLogger
メソッドが引数として受け取っているInjectionPoint
クラスは、CDI が提供する API である。
CDI の内部動作に関連するクラスであり、Producer メソッドの引数に指定できる。
InjectionPoint
は名前の通り、@Inject
アノテーションを記述したインジェクションポイントに関する情報を保持するオブジェクトで、インジェクション対象のクラスやフィールドの情報などが格納される。
上記のコードでは、そこからインジェクション対象のクラスのパッケージ名を取得してロガー名に設定している。 この例では単にコンソールにログを出すだけだが、ファイルへの出力やフォーマットなどを変更する場合は、このクラスを修正すればよい。
ロガーを使用する側では、@Inject Logger logger
というインジェクションを記述するだけでよい。このロガーは全ての CDI Bean で使用可能だ。
インターセプタークラスの定義
次に、ログ出力のような共通的な処理を行うインターセプターを定義する。 特別なクラスを継承する必要はなく、アノテーションを設定すればよい。
今回は、@AroundInvoke
を中心に説明する。@AroundConstruct
, @AroundTimeout
の説明は割愛するが、@AroundInvoke
と内容は基本的に同じである。
@Aroundxxx
を設定するメソッドの名前は何でもよいが、引数にInvocationContext
を指定し、戻り値をObject
にする必要がある。
InvocationContext
は CDI が提供する API で、対象クラスが本来実行しようとしていたメソッドを示す。InvocationContext
のproceed
メソッドを呼べば、本来のメソッドを実行することができる。このため、事前に行いたい処理はproceed
の呼び出しの前に記述する必要がある。
またproceed
はObject
型の値を返す。これが本来のメソッドの戻り値なので、通常インターセプターメソッドはこの値を戻り値にする必要がある。(無視して任意の値を返すと、本来のメソッドの戻り値を握りつぶすことになる。)
また、今回のサンプルでは対応していないが、メソッドの引数の情報にもアクセス可能なので、引数の値を変更したりすることもできる。
ログ出力を行うサンプルコード
以下に、ログ出力を行うサンプルを示す。
@Interceptor // インターセプターの宣言 @Dependent @WithLog // バインド用アノテーション @Priority(Interceptor.Priority.APPLICATION) // 優先度 public class WithLogInterceptor { // プロデューサー経由でロガー取得 @Inject private Logger logger; // アプリケーション名の取得 @Resource(lookup="java:app/AppName") String appName; /** * インターセプターのメソッド * @param ic 実行コンテキスト - 本来実行される処理。 * @return 本来実行される処理の戻り値 * @throws Exception 何らかの例外 */ @AroundInvoke public Object invoke(InvocationContext ic) throws Exception { // ターゲットは、CDIのクライアントプロキシなので、スーパークラスを取得。 String classAndMethod = ic.getTarget().getClass() .getSuperclass().getName() + "#" + ic.getMethod().getName(); // メソッド開始前のログ logger.info(() -> appName + ":" + classAndMethod + " start."); Object ret = null; try { // メソッドの実行 System.out.println("call by interceptor"); ret = ic.proceed(); } catch(Exception e) { // 例外のログを出したら、例外はそのまま再スローする。 // トランザクションインターセプターの内部で処理されるので、 // ここでは根本例外が出る。 logger.log(Level.SEVERE, appName, e); throw e; } // メソッド終了後のログ logger.info(() -> appName + ":" + classAndMethod + " end."); return ret; }
メソッド内で例外が発生した場合は、例外のスタックトレースをログ出力して再スローする。
次節の「インターセプターの優先度」の節で述べるが、このインターセプターは、@Transactional
で提供される宣言的トランザクションのインターセプターより後に動いているため、ここで捕捉した例外は、まだ@Transactional
で握りつぶされる前の根本例外である。
インターセプターの優先度
インターセプターは複数動作する可能性があるため、実行順を決める必要がある。
@Priority
アノテーションはインターセプターに優先度を与えるアノテーションである。
優先度は@Priority
の属性に整数値を与えることで設定され、少ないほうが優先して実行される。
また、以下の通り目安となる定数が設定されている。
定数 | 値 | 内容 |
---|---|---|
Interceptor.Priority.PLATFORM_BEFORE | 0 | プラットフォーム(サーバー)が初期設定などで早期に実行する優先度 |
Interceptor.Priority.LIBRARY_BEFORE | 1000 | 拡張ライブラリが早期に実行する優先度 |
Interceptor.Priority.APPLICATION | 2000 | アプリケーションなどが実行してもよい標準の優先度 |
Interceptor.Priority.LIBRARY_AFTER | 3000 | 拡張ライブラリなどで後処理などで実行する優先度 |
Interceptor.Priority.PLATFORM_AFTER | 4000 | プラットフォームが後処理などで実行する優先度 |
通常であれば、Interceptor.Priority.APPLICATION
前後の値を使用すればよく、先ほどのインターセプターもそれにならっている。
@Transactional
で提供されるトランザクションインターセプターの優先度は、Interceptor.Priority.PLATFORM_BEFORE+200
と@Transactional
の Javadoc に規定されている。
よって、先ほどのインターセプターは@Transactional
より後に動くことになり、根本例外の取得が可能となっている。
インターセプターの定義
Java EE6 までは、インターセプターは beans.xml
に定義を書いておく必要があったが、Java EE7からbeans.xml
に定義する必要はなくなった。
インターセプターの使用方法
続いて、定義したインターセプターを利用する方法を見ていく。利用方法は2通りある。
方法1:@Interceptors
の使用
インターセプターを利用したいクラス(またはメソッド)に、@Interceptors
アノテーションを設定し、属性にインターセプターのクラスを指定する方法である。
@RequestScoped @Transactional @Interceptors(WithLogInterceptor.class) public class SampleService { //割愛
ただこの方法には、影に隠れているインターセプターのクラス名を直接書いてしまうという欠点がある。
方法2:バインド用アノテーションの使用
もう一つの方法として、バインド用のアノテーションを使用する方法がある。
前述のWithLogIntercepter
のクラス宣言では@WithLog
というアノテーションを指定していた。
このアノテーションは自作のもので、以下のような定義になっている。
@Inherited @InterceptorBinding // インターセプターのバインド @Retention(RUNTIME) @Target({METHOD, TYPE}) public @interface WithLog { }
ポイントは、@InterceptorBinding
アノテーションが付与してあることで、これにより@WithLog
アノテーションをインターセプターの利用を宣言するアノテーションとして使用することができる。
この方法を使えば、@Interceptors
の代わりに、以下のように定義することができる。
@RequestScoped @Transactional @WithLog public class SampleService { //割愛
この定義により、@WithLog
アノテーションが付与されたインターセプタークラス(WithLogInterceptor
)を組み込むことが可能となる。
(余談だが、@Transactional
も同様のバインド用アノテーションである。)
ステレオタイプ
この前の「インターセプターの使用方法」の節で示したSampleService
クラスは、今回のインターセプターが増えたことにより、アノテーションが3つになってしまった。
インターセプターが増えるたびにクラスに付与するアノテーションが増えるのは煩雑な上に、定義漏れが発生する可能性もある。
そのような時のために、よく使用するアノテーションの組み合わせをまとめる仕組みとして、ステレオタイプがある。
ステレオタイプも@Stereotype
を付与したアノテーションとして作成する必要がある。
@Retention(RUNTIME) @Target({TYPE}) @Stereotype // ステレオタイプ宣言 // 以下が各クラスに付与するCDI関係のアノテーション @Transactional @RequestScoped @WithLog public @interface Service { }
これにより、クラスに個別に付与していたアノテーション群を、上記の@Service
アノテーションに集約できる。
こうすることで、前述のクラスに付与するアノテーションは以下のように簡略化できる。
@Service public SampleService { //省略
CDIを使うと、アノテーションがどんどん増えていくが、ステレオタイプを用いることで、アノテーションの宣言の手間を減らすことができるだろう。
(定義ファイルなどで、特定のクラスなどに一律でインターセプターを定義する方法は、現状では提供されていない。CDIは良くも悪くもアノテーションで何でもやろうとする仕組みのようだ。)
まとめ
今回は、インターセプターの定義方法について紹介した。横断的に行う必要のある処理に対しては、インターセプターの利用を検討するとよいだろう。
またあわせて、CDIを使うことで増えていくアノテーション宣言をまとめるステレオタイプについて紹介した。
設計を進めていくと、どのような責務でクラスを作るかが見えてくるはずで、それらの責務に対して指定するアノテーションもパターン化できるはずだ。(ちなみに今回の例では、ビジネスロジックを実行するクラスは、リクエストスコープで、トランザクション起点で、ログを出力するというのがパターンになっている。)
このような場合に、パターン化したアノテーションをステレオタイプとして定義しておくと、実装工程で楽になるだろう。
今回までで CDI の基本的な内容について解説した。 是非 CDI を活用して Java EE アプリケーションを設計してみて欲しい。
次回から、Java EE 7 で新しく追加された仕様である WebSocket について解説する。
[前多 賢太郎]