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

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

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 で使用可能だ。

インターセプタークラスの定義

次に、ログ出力のような共通的な処理を行うインターセプターを定義する。 特別なクラスを継承する必要はなく、アノテーションを設定すればよい。

  • クラス宣言で用いるアノテーション

    • @Interceptor - 必須。インターセプターであることを表す。
    • @Priority - 任意だが設定した方がよい。複数のインターセプターの実行順を示す。
    • バインド用アノテーション - 任意。インターセプターを実行するクラスを定義するアノテーション。詳細は後述する。
  • メソッド宣言で用いるアノテーション

    • @AroundInvoke - 任意。対象クラスのメソッドの実行前後に何らかの処理を行う。
    • @AroundConstruct - 任意。対象クラスのコンストラクター実行時に何らかの処理を行う。
    • @AroundTimeout - 任意。EJBタイムアウトメソッドに対して何らかの処理を行う。

今回は、@AroundInvokeを中心に説明する。@AroundConstruct, @AroundTimeoutの説明は割愛するが、@AroundInvokeと内容は基本的に同じである。

@Aroundxxxを設定するメソッドの名前は何でもよいが、引数にInvocationContextを指定し、戻り値をObjectにする必要がある。

InvocationContextCDI が提供する API で、対象クラスが本来実行しようとしていたメソッドを示す。InvocationContextproceedメソッドを呼べば、本来のメソッドを実行することができる。このため、事前に行いたい処理はproceedの呼び出しの前に記述する必要がある。

またproceedObject型の値を返す。これが本来のメソッドの戻り値なので、通常インターセプターメソッドはこの値を戻り値にする必要がある。(無視して任意の値を返すと、本来のメソッドの戻り値を握りつぶすことになる。)

また、今回のサンプルでは対応していないが、メソッドの引数の情報にもアクセス可能なので、引数の値を変更したりすることもできる。

ログ出力を行うサンプルコード

以下に、ログ出力を行うサンプルを示す。

@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@TransactionalJavadoc に規定されている。

よって、先ほどのインターセプターは@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 について解説する。

[前多 賢太郎]