kagamihogeの日記

kagamihogeの日記です。

The Java EE 7 TutorialのUsing Java EE Interceptorsの章をテキトーに訳した

The Java EE 7 Tutorial54 Using Java EE Interceptorsの章をテキトーに訳した。

54 Using Java EE Interceptors

この章では、メソッドやターゲットクラスのライフサイクルイベントに割り込むインターセプタークラスとメソッドの作成方法について解説します。

以下のトピックを扱います。

  • Overview of Interceptors
  • Using Interceptors
  • The interceptor Example Application

54.1 Overview of Interceptors

インターセプターはJava EEのマネージドクラスと組み合わせて使用するもので、開発者はメソッド実行やライフサイクルイベントに連動して、関連ターゲットクラス(target class)のインターセプターメソッドを実行できます。インターセプターのよくある使い方は、ログ・監査・プロファイリング、です。

インターセプターはEnterprise JavaBeans 3.2と Contexts and Dependency Injection for Java EE 1.1の一部ですが、Interceptors 1.2仕様はJSR 318のmaintenance releaseの一部としてダウンロードが可能で、Enterprise JavaBeans 3.1は https://jcp.org/en/jsr/detail?id=318 で参照可能です。インターセプターは、セッションビーン・メッセージドリブンビーン・CDIのマネージドビーンと共に使用可能です。この場合には、インターセプターのターゲットクラスはビーンクラスになります。

インターセプターはインターセプターメソッド(interceptor method)としてターゲットクラス内部に定義したり、インターセプタークラス(interceptor class)と呼ばれる関連クラスとして定義が可能です。インターセプタークラスにはターゲットクラスのライフサイクルイベントやメソッドと組み合わせて実行するメソッドを持たせます。

インターセプタークラスとメソッドメタデータアノテーションを使用して定義するか、アプリケーションのデプロイメント記述子にインターセプターとターゲットクラスを定義します。

Note: インターセプター定義にデプロイメント記述子を使用するアプリケーションはJava EEサーバ間のポータビリティはありません。

インターセプタークラスやターゲットクラス内のインターセプターメソッドは表 54-の1メタデータアノテーションの一つを付与します。

表 54-1 インターセプターのメタデータアノテーション

インターセプターのメタデータアノテーション 説明
javax.interceptor.AroundConstruct ターゲットクラスのコンストラクタ実行後にコールバックを受けるインターセプターメソッドを指定します。
javax.interceptor.AroundInvoke そのメソッドをインターセプターメソッドに指定します。
javax.interceptor.AroundTimeout そのメソッドEJBタイマーのタイムアウトメソッドにおけるタイムアウトインターセプターに指定します。
javax.annotation.PostConstruct そのメソッドをpost-constructライフサイクルイベントのインターセプターメソッドに指定します。
javax.annotation.PreDestroy そのメソッドをpre-destroyライフサイクルイベントのインターセプターメソッドに指定します。

54.1.1 Interceptor Classes

インターセプタークラスはjavax.interceptor.Interceptorアノテーションを付与しても、しなくても、どちらでも構いません。なお、インターセプタークラスはpublicで引数無しのコンストラクタが必須です。

ターゲットクラスは任意の数のインターセプタークラスを関連付けられます。インターセプタークラスの呼び出し順序はjavax.interceptor.Interceptorsアノテーションにおけるインターセプタークラスの定義順になります。ただし、この順序はデプロイメント記述子でオーバーライド可能です。

インターセプタークラスはDIのターゲットになります。インターセプタークラスのインスタンスが生成される時、関連ターゲットクラスのネーミングコンテキストを使用してDIが行われます。DIは@PostConstructコールバックが実行される前に行われます。

54.1.2 Interceptor Lifecycle

インターセプタークラスは関連ターゲットクラスと同様のライフサイクルを持ちます。ターゲットクラスのインスタンスが生成されるとき、ターゲットクラスに宣言されたインターセプタークラスのインスタンスも生成されます。ターゲットクラスが複数のインターセプタークラスを宣言している場合、ターゲットクラスのインスタンスが生成される時に各クラスのインスタンスが生成されます。ターゲットクラスのインスタンスとすべてのインターセプタークラスのインスタンスは、@PostConstructコールバックが呼び出される前に、完全にインスタンス化されます。また、@PreDestroyコールバックはターゲットクラスとインターセプタークラスのインスタンスが破棄される前に呼び出されます。

54.1.3 Interceptors and CDI

CDIJava EEインターセプターの基本機能に基づいて作られています。インターセプターバインディングタイプを含むCDIインターセプターの詳細についてはUsing Interceptors in CDI Applicationsを参照してください。

54.2 Using Interceptors

インターセプターを定義するには、ターゲットクラス内で表 54-1のインターセプターメタデータアノテーションを使うか、別のインターセプタークラスを作成します。以下のコードはターゲットクラス内で@AroundTimeoutインターセプターメソッドを宣言しています。

@Stateless
public class TimerBean {
    ...
    @Schedule(minute="*/1", hour="*")
    public void automaticTimerMethod() { ... }

    @AroundTimeout
    public void timeoutInterceptorMethod(InvocationContext ctx) { ... }
    ...
}

インターセプタークラスを使用する場合、ターゲットクラスのクラスかメソッドレベルで一つ以上のインターセプターを宣言するのにjavax.interceptor.Interceptorsアノテーションを使用します。以下のコードはクラスレベルでインターセプターを宣言しています。

@Stateless
@Interceptors({PrimaryInterceptor.class, SecondaryInterceptor.class})
public class OrderBean { ... }

以下のコードはメソッドレベルでインターセプタークラスを宣言しています。

@Stateless
public class OrderBean {
    ...
    @Interceptors(OrderInterceptor.class)
    public void placeOrder(Order order) { ... }
    ...
}

54.2.1 Intercepting Method Invocations

@AroundInvokeでマネージドオブジェクト用のインターセプターメソッドを指定します。around-invokeインターセプターはクラスに一つだけ作成できます。around-invokeインターセプターメソッドは以下のような形式を取ります。

@AroundInvoke
visibility Object method-name(InvocationContext) throws Exception { ... }

たとえば、

@AroundInvoke
public void interceptOrder(InvocationContext ctx) { ... }

Around-invokeインターセプターメソッドの可視性は、public, private, protected, packageレベルが可能で、staticやfinalは禁止です。

Around-invokeインターセプターは、インターセプト対象のターゲットメソッドが呼び出すコンポーネントやリソースを呼び出し可能で、ターゲットメソッドと同一のセキュリティとトランザクションコンテキストを持ちます。また、ターゲットメソッドと同一のJava VMコールスタックで実行できます。

Around-invokeインターセプターは実行時例外とターゲットメソッドthrows節の例外をスロー可能です。例外をcatchするか握りつぶし、それからInvocationContext.proceedを呼ぶことで復帰が可能です。

54.2.1.1 Using Multiple Method Interceptors

ターゲットメソッドやクラスに複数のインターセプターを宣言するには@Interceptorsを使います。

@Interceptors({PrimaryInterceptor.class, SecondaryInterceptor.class, 
        LastInterceptor.class})
public void updateInfo(String info) { ... }

@Interceptorsのインターセプターの定義順序がインターセプターが呼び出される順序になります。

デプロイメント記述子に複数のインターセプターを定義することも可能です。デプロイメント記述子のインターセプターの定義順序がインターセプターが呼び出される順序になります。

...
<interceptor-binding>
    <target-name>myapp.OrderBean</target-name>
    <interceptor-class>myapp.PrimaryInterceptor.class</interceptor-class>
    <interceptor-class>myapp.SecondaryInterceptor.class</interceptor-class>
    <interceptor-class>myapp.LastInterceptor.class</interceptor-class>
    <method-name>updateInfo</method-name>
</interceptor-binding>
...

チェーンの次のインターセプターへ明示的に制御を渡すには、InvocationContext.proceedメソッドを呼びます。

データはインターセプターをまたがって共有可能です。

  • 同一のInvocationContextインスタンスが、個々のターゲットメソッドにおけるインターセプターチェーン上のインターセプターそれぞれの入力パラメータとして、渡されます。インターセプターメソッドをまたがるデータの受け渡しにはInvocationContextインスタンスcontextDataプロパティを使用します。contextDataプロパティはjava.util.Map<String, Object>オブジェクトです。contextDataに格納されたデータは、それ以降のインターセプターチェーンのインターセプターメソッドで利用可能です。
  • contextDataに格納されたデータは異なるターゲットクラスのメソッド実行では共有できません。つまり、ターゲットクラスのそれぞれのメソッド実行ごとに、異なるInvocationContextオブジェクトが生成されます。
54.2.1.2 Accessing Target Method Parameters from an Interceptor Class

ターゲットメソッドの引数にアクセスしたり修正するために、around-invokeメソッドに渡されるInvocationContextインスタンスを使えます。InvocationContextparametersプロパティはObjectインスタンスの配列で、これはターゲットメソッド引数の定義順に対応しています。たとえば以下のターゲットメソッドでは、PrimaryInterceptorのaround-invokeインターセプターメソッドに渡されるInvocationContextインスタンスparametersプロパティは、二つのStringオブジェクト(firstNamelastName)とDateオブジェクトのObject配列になります。

@Interceptors(PrimaryInterceptor.class)
public void updateInfo(String firstName, String lastName, Date date) { ... }

InvocationContext.getParametersInvocationContext.setParametersを使用して引数にアクセスしたり修正することが可能です。

54.2.2 Intercepting Lifecycle Callback Events

ライサイクルコールバックイベント(around-construct, post-construct, and pre-destroy)用のインターセプターは、インターセプタークラスかターゲットクラスに定義可能です。ターゲットクラスのコンストラクタ実行をインターセプトするメソッドにはjavax.interceptor.AroundConstructを指定します。post-constructライフクサイクルイベントインターセプターのメソッドにはjavax.annotation.PostConstructを指定します。pre-destroyライフクサイクルイベントインターセプターのメソッドにはjavax.annotation.PreDestroyを指定します。

ターゲットクラス内に定義するライフクサイクルイベントインターセプターは以下の形式を取ります。

void method-name() { ... }

例としては、

@PostConstruct
void initialize() { ... }

インターセプタークラスに定義するライフクサイクルイベントインターセプターは以下の形式を取ります。

void method-name(InvocationContext) { ... }

例としては、

@PreDestroy
void cleanup(InvocationContext ctx) { ... }

ライフクサイクルインターセプターのメソッドの可視性は、public, private, protected, packageレベルが可能で、staticやfinalは禁止です。ライフクサイクルインターセプターは実行時例外はスロー可能ですが、チェック例外はスロー出来ません。

ライフクサイクルインターセプターのメソッドは不明なセキュリティおよびトランザクションコンテキストで呼ばれます。Java EEアプリケーションにポータビリティを持たせるには、ライフサイクルイベントインターセプターのメソッドがセキュリティやトランザクションコンテキストにアクセスするという仮定をすべきではありません。あるクラスのライフサイクルイベント(post-createとpre-destroy)それぞれにつき、一つだけインターセプターメソッドが指定可能です。

54.2.2.1 Using AroundConstruct Interceptor Methods

@AroundConstructメソッドはターゲットクラスのコンストラクタ実行をインターセプトします。@AroundConstructを付与するメソッドはインターセプタークラスかインターセプタークラスの親クラスでだけ定義可能です。ターゲットクラス内では@AroundConstructを使用できません。

@AroundConstructメソッドは、ターゲットクラスに関連付けられているすべてのインターセプターのDIを完了したあとに、呼び出されます。ターゲットクラスが生成されて、すべての@AroundConstructメソッドInvocation.proceedメソッドを呼び出し終えたあとに、ターゲットクラスのコンストラクタインジェクションが実行されます。ターゲットクラスのDIが完了すると、@PostConstructコールバックが呼び出されます。

@AroundConstructメソッドは、Invocation.proceedを呼び出した後は、InvocationContext.getTarget```を呼ぶことで新規作成されたターゲットインスタンスにアクセス可能です。

注意: @AroundConstructメソッドからターゲットインスタンスメソッドを呼ぶことは危険です。その理由は、ターゲットインスタンスのDIが完了していない可能性があるためです。

@AroundConstructメソッドはターゲットインスタンスを生成するためにInvocation.proceedを呼ぶ必要があります。もし@AroundConstructメソッドInvocation.proceedを呼ばない場合、ターゲットインスタンスは生成されません。

54.2.2.2 Using Multiple Lifecycle Callback Interceptors

ターゲットクラスに複数のライフサイクルインターセプターを定義するには、@Interceptors複数のインターセプターを指定します。

@Interceptors({PrimaryInterceptor.class, SecondaryInterceptor.class, 
        LastInterceptor.class})
@Stateless
public class OrderBean { ... }

InvocationContextcontextDataプロパティに格納されるデータは異なるライフサイクルイベントにまたがっての共有はできません。

54.2.3 Intercepting Timeout Events

EJBタイマーサービスのタイムアウトメソッド用のインターセプターを、@AroundTimeoutを使用してターゲットクラスのメソッドもしくはインターセプタークラスに定義できます。一つのクラスには一つだけ@AroundTimeoutを定義できます。

タイムアウトインターセプターは以下の形式をとります。

Object method-name(InvocationContext) throws Exception { ... }

例としては、

@AroundTimeout
protected void timeoutInterceptorMethod(InvocationContext ctx) { ... }

タイムアウトインターセプターメソッドは、public, private, protected, packageを取ることができ、staticもしくはfinalは宣言禁止です。

タイムアウトインターセプターはターゲットのタイムアウトメソッドが呼び出すコンポーネントやリソースを呼ぶことが可能で、インターセプターはターゲットメソッドと同一のトランザクションとセキュリティコンテキストで実行されます。

タイムアウトインターセプターは、InvocationContextインスタンスgetTimerメソッド経由でターゲットタイムアウトメソッドに紐付いているタイマーオブジェクトにアクセスが可能です。

54.2.3.1 Using Multiple Timeout Interceptors

ターゲットクラスに複数タイムアウトインターセプターを定義するには、クラスレベルの@Interceptors@AroundTimeoutインターセプターメソッドを含むインターセプタークラスを指定します。

ターゲットクラスがインターセプタークラスにタイムアウトインターセプターを指定し、ターゲットクラス自身にも@AroundTimeoutインターセプターメソッドを持つ場合、まず最初にインターセプタークラスのタイムアウトインターセプターが呼ばれ、それからターゲットクラスに定義されたタイムアウトインターセプターが呼ばれます。たとえば、以下のようにPrimaryInterceptorSecondaryInterceptorクラスが両方ともタイムアウトインターセプターメソッドだとすると、

@Interceptors({PrimaryInterceptor.class, SecondaryInterceptor.class})
@Stateful
public class OrderBean {
    ...
    @AroundTimeout
    private void last(InvocationContext ctx) { ... }
    ...
}

最初にPrimaryInterceptorタイムアウトインターセプターが呼び出され、次にSecondaryInterceptorが呼び出され、最後にターゲットクラスのlastメソッドが呼び出されます。

54.2.4 Binding Interceptors to Components

インターセプターバインディングタイプはコンポーネントと指定のインターセプターを関連付けるために使用します。インターセプターバインディングタイプは通常、インターセプターのターゲットを指定するカスタムのランタイムアノテーションタイプです。カスタムアノテーション定義にjavax.interceptor.InterceptorBindingを使用し、@Targetに、TYPE(クラスレベルインターセプター)、METHOD(メソッドレベルインターセプター)、CONSTRUCTOR(around-constructインターセプター)、その他有効なターゲット、を一つ以上設定します。

@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
@Inherited
pubic @interface Logged { ... }

インターセプターバインディングタイプは他のインターセプターバインディングタイプにも適用可能です。

@Logged
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
@Inherited
public @interface Secured { ... }

54.2.4.1 Declaring the Interceptor Bindings on an Interceptor Class

インターセプターのクラスにインターセプターバインディングタイプのアノテーションと、クラスとインターセプターバインディングを関連付けるために@Interceptorを付与します。

@Logged
@Interceptor
public class LoggingInterceptor {
    @AroundInvoke
    public Object logInvocation(InvocationContext ctx) throws Exception { ... }
    ...
}

インターセプタークラスは複数のインターセプターバインディングを宣言可能で、一つ以上のインターセプタークラスがインターセプターバインディングタイプを宣言可能です。

インターセプタークラスがライフサイクルコールバックをインターセプトする場合、Target(TYPE)のみにするか、@AroundConstructライフサイクルコールバックの場合にはTarget(CONSTRUCTOR)のみが宣言可能です。

54.2.4.2 Binding a Component to an Interceptor

インターセプターバインディングタイプはターゲットコンポーネントのクラス・メソッドコンストラクタアノテーションを付与します。インターセプターバインディングタイプは@Interceptorと同様なルールを使用して適用します。

@Logged
public class Message {
    ...
    @Secured
    public void getConfidentialMessage() { ... }
    ...
}

コンポーネントがクラスレベルのインターセプターバインディングを持つ場合、コンポーネントは非finalにするか、非staticprivate finalメソッドにする必要があります*1

もし非staticprivateメソッドにインターセプターバインディングが適用されている場合、メソッドは非finalかつコンポーネントクラスは非finalでなければなりません。

54.2.5 Ordering Interceptors

複数のインターセプターが実行される順序は以下のルールによって決定されます。

  • 最初に実行されるのは、デプロイメント記述子に定義されているデフォルトインターセプターです。デフォルトインターセプターは実行順序を指定したり、アノテーションで指定した順序をオーバーライドすることが出来ます。デフォルトインターセプターの実行順序はデプロイメント記述子の定義順になります。
  • @Interceptorsに指定したインターセプタークラスの順序がインターセプタークラスが実行される順序になります。@Interceptors内に指定したインターセプターの@Priority設定は無視されます。
  • インターセプタークラスが親クラスを持つ場合、先にスーパークラスで定義されたインターセプターが実行され、最も上位のスーパークラスから実行されます。
  • インターセプタークラスはjavax.annotation.Priorityでインターセプターメソッドのプライオリティに値を設定可能です。
  • インターセプタークラス内に定義されたインターセプターが実行された後に、ターゲットクラスのコンストラクタ・around-invoke・around-timeoutインターセプターが@Interceptors内のインターセプターと同じ順序で実行されます。
  • ターゲットクラスがスーパークラスを持つ場合、まずスーパークラスに定義されているインターセプターが実行され、最も上位のスーパークラスから実行されます。

@Priorityアノテーションは要素の値にintを取ります。関連インターセプターのプライオリティの高低を設定します。

Note: インターセプターの実行順序を同一値にする場合は実装依存になります。

javax.interceptor.Interceptor.Priorityクラスには表 54-2のプライオリティ定数値が定義されています。

表 54-2 インターセプタープライオリティの定数値

プライオリティ定数値 説明
PLATFORM_BEFORE 0 Java EEプラットフォームが定義するインターセプターで、呼び出しチェーンの早期段階で呼び出されるよう指定するもので、この値はPLATFORM_BEFORELIBRARY_BEFOREの間を使用すべきです。このインターセプターが最も高いプライオリティになります。
LIBRARY_BEFORE 1000 拡張ライブラリが定義するインターセプターはインターセプターチェーンの早期段階で実行されるべきで、この値はLIBRARY_BEFOREAPPLICATIONの間を使用すべきです。
APPLICATION 2000 アプリケーションが定義するインターセプターはAPPLICATIONLIBRARY_AFTERの間を使用しべきです。
LIBRARY_AFTER 3000 拡張ライブラリが定義する低プライオリティインターセプターはLIBRARY_AFTERPLATFORM_AFTERの間を使用すべきです。
PLATFORM_AFTER 4000 Java EEプラットフォームが定義する低プライオリティインターセプターはPLATFORM_AFTERよりも高い値を使用すべきです。

Note: 負数はInterceptors仕様が将来に向けて予約しているため使用すべきではありません。

以下のコードはアプリケーション定義のインターセプターにプライオリティ定数値を使用する方法です。

@Interceptor
@Priority(Interceptor.Priority.APPLICATION+200)
public class MyInterceptor { ... }

54.3 The interceptor Example Application

interceptorサンプルはインターセプタークラスの使用方法を紹介しており、ステートレスセッションビーンで@AroundInvokeインターセプターメソッドを使用しています。

HelloBeanステートレスセッションビーンは、文字列の取得と修正を行うgetNamesetNameの二つのビジネスメソッドからなるシンプルなEJBです。setNameビジネスメソッドにはインターセプタークラスHelloInterceptorを指定している@Interceptorsが付与されています。

@Interceptors(HelloInterceptor.class)
public void setName(String name) {
    this.name = name;
}

HelloInterceptorクラスは@AroundInvokeインターセプターメソッドmodifyGreetingが定義されており、HelloBean.setNameに渡された文字列を小文字に変換します。

@AroundInvoke
public Object modifyGreeting(InvocationContext ctx) throws Exception {
    Object[] parameters = ctx.getParameters();
    String param = (String) parameters[0];
    param = param.toLowerCase();
    parameters[0] = param;
    ctx.setParameters(parameters);
    try {
        return ctx.proceed();
    } catch (Exception e) {
        logger.warning("Error calling ctx.proceed in modifyGreeting()");
        return null;
    }
}

InvocationContext.getParametersメソッドを呼ぶことでObjectの配列にHelloBean.setNameの引数が取得できます。setNameの引数は一つだけなので、配列の要素は最初の一つだけです。文字列を小文字にしてparameters配列に代入して、InvocationContext.setParametersに渡します。セッションビーンに制御を戻すには、InvocationContext.proceedを呼びます。

interceptorユーザーインターフェースJSFで二つのFaceletsビューから構成されます。index.xhtmlには氏名を入力するフォームがあり、response.xhtmlは結果を表示します。

54.3.1 Running the interceptor Example

  1. GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
  2. FileメニューからOpen Projectを選択
  3. Open Projectダイアログボックスから以下のディレクトリに移動。
    tut-install/examples/ejb*2
  4. interceptorフォルダを選択してOpen Projectをクリック。
  5. Projectsタブでinterceptorプロジェクトを右クリックしてRunを選択。
    コンパイル・デプロイ・interceptorが実行され、webブラウザで以下のURLを開きます。
    http://localhost:8080/interceptor/
  6. フォームに名前を入力してSubmitをクリック。
    名前がHelloInterceptorクラスで定義されたメソッドインターセプターによって小文字に変換されます。

54.3.1.2 To Run the interceptor Example Using Maven

  1. GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
  2. ターミナルで以下に移動:
    tut-install/examples/ejb/interceptor/
  3. 以下のコマンドを実行。
    mvn install
    このコマンドはアプリケーションをビルドしてパッケージングしてtargetディレクトリにinterceptor.warを生成します。それから、GlassFish ServerにWARファイルをデプロイします。
    http://localhost:8080/interceptor/
  4. フォームに名前を入力してSubmitをクリック。
    名前がHelloInterceptorクラスで定義されたメソッドインターセプターによって小文字に変換されます。

関連リンク

*1:it must not be final or have any non-static, non-private final methods. 上手く訳せず。it must ... haveで良いのか?

*2:https://java.net/projects/javaeetutorial/sources/svn/show/trunk/examples/ejb