The Java EE 7 Tutorialの54 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メタデータアノテーションの一つを付与します。
| インターセプターのメタデータアノテーション | 説明 |
|---|---|
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
CDIはJava 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インスタンスを使えます。InvocationContextのparametersプロパティはObjectインスタンスの配列で、これはターゲットメソッド引数の定義順に対応しています。たとえば以下のターゲットメソッドでは、PrimaryInterceptorのaround-invokeインターセプターメソッドに渡されるInvocationContextインスタンスのparametersプロパティは、二つのStringオブジェクト(firstNameとlastName)とDateオブジェクトのObject配列になります。
@Interceptors(PrimaryInterceptor.class) public void updateInfo(String firstName, String lastName, Date date) { ... }
InvocationContext.getParametersとInvocationContext.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 { ... }
InvocationContextのcontextDataプロパティに格納されるデータは異なるライフサイクルイベントにまたがっての共有はできません。
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インターセプターメソッドを持つ場合、まず最初にインターセプタークラスのタイムアウトインターセプターが呼ばれ、それからターゲットクラスに定義されたタイムアウトインターセプターが呼ばれます。たとえば、以下のようにPrimaryInterceptorとSecondaryInterceptorクラスが両方ともタイムアウトインターセプターメソッドだとすると、
@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にするか、非static非private finalメソッドにする必要があります*1。
もし非static非privateメソッドにインターセプターバインディングが適用されている場合、メソッドは非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_BEFOREとLIBRARY_BEFOREの間を使用すべきです。このインターセプターが最も高いプライオリティになります。 |
LIBRARY_BEFORE |
1000 | 拡張ライブラリが定義するインターセプターはインターセプターチェーンの早期段階で実行されるべきで、この値はLIBRARY_BEFOREとAPPLICATIONの間を使用すべきです。 |
APPLICATION |
2000 | アプリケーションが定義するインターセプターはAPPLICATIONとLIBRARY_AFTERの間を使用しべきです。 |
LIBRARY_AFTER |
3000 | 拡張ライブラリが定義する低プライオリティインターセプターはLIBRARY_AFTERとPLATFORM_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ステートレスセッションビーンは、文字列の取得と修正を行うgetNameとsetNameの二つのビジネスメソッドからなるシンプルな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
- GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
- FileメニューからOpen Projectを選択
- Open Projectダイアログボックスから以下のディレクトリに移動。
tut-install/examples/ejb*2 interceptorフォルダを選択してOpen Projectをクリック。- Projectsタブで
interceptorプロジェクトを右クリックしてRunを選択。
コンパイル・デプロイ・interceptorが実行され、webブラウザで以下のURLを開きます。
http://localhost:8080/interceptor/ - フォームに名前を入力してSubmitをクリック。
名前がHelloInterceptorクラスで定義されたメソッドインターセプターによって小文字に変換されます。
54.3.1.2 To Run the interceptor Example Using Maven
- GlassFishサーバが実行中なことを確認(Starting and Stopping GlassFish Serverを参照)
- ターミナルで以下に移動:
tut-install/examples/ejb/interceptor/ - 以下のコマンドを実行。
mvn install
このコマンドはアプリケーションをビルドしてパッケージングしてtargetディレクトリにinterceptor.warを生成します。それから、GlassFish ServerにWARファイルをデプロイします。
http://localhost:8080/interceptor/ - フォームに名前を入力してSubmitをクリック。
名前がHelloInterceptorクラスで定義されたメソッドインターセプターによって小文字に変換されます。
関連リンク
- The Java EE 7 Tutorialのテキトー翻訳まとめ - Qiita - Java EE 7 Tutorialのうち、自分がテキトー翻訳したものの一覧
*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