EJB 3.0(Public Draft)入門記 Simplified API Chapter3 その4

Chapter 3 のその4です。Interceptorが出てきました。

InterceptorってAOPの一例だと思うのですが、EJB 3.0ではAOPに対するスタンスというのはいかほどのものなのでしょう。AspectJとかと一緒に使われることなども考慮に入れられているんでしょうか?そういえばChapter 1 や 2 でInterceptorに触れらていないしあんまし大きな位置づけじゃないのか?

3.5 Interceptors

インターセプタはビジネスメソッドの呼び出しをインターセプトするメソッドです。インターセプタメソッドはBeanかBeanに関連付けられたインターセプタクラス上に定義できます。インターセプタクラスとは、Beanクラス上のビジネスメソッドの呼び出しに応じて呼び出されるメソッドをもつクラスだそうです。インターセプタはセッションBeanとメッセージ駆動型BeanとEJB 2.1のエンティティBeanに対して定義できるそうです。
EJB 2.1のエンティティBeanが対象になっているというのは意外でした。

インターセプタクラスはInterceptorアノテーションやInterceptorsアノテーション(複数のインターセプタクラスがある場合)を使用してBeanクラスに示されます。インターセプタメソッドはAroundInvokeアノテーションで示されます。

次のルールがインターセプタに適用されます。

  • インターセプタメソッドの呼び出しは対応するビジネスメソッドと同じトランザクション、しかも同じセキュリティのコンテキスト内で生じる。
  • インターセプタメソッドは実行時例外をスローできる。またビジネスメソッドのthrows節で認められているアプリケーション例外をスローできる。
  • インターセプタはJNDI、JDBC、JMS、他のエンタープライズBean、EntityManagerを呼び出すことができる。
  • インターセプタクラスに対してDependency Injectionがサポートされている。
  • エンタープライズBeanコンポーネントに適用されるプログラミング上の制限がインターセプタにも同様に適用される。

3.5.1 Interceptor Methods

箇条書きにします。

  • インターセプタメソッドはエンタープライズBeanのビジネスメソッドに対して定義できる。ビジネスメソッドにはメッセージ駆動型Beanのメッセージリスナーを含む。
  • インターセプタメソッドはAroundInvokeアノテーションで表す。
  • Beanクラス上もしくは任意のインターセプタクラス上に存在できるのはひとつのAroundInvokeメソッドのみ。
  • AroundInvokeメソッドはビジネスメソッドであってはいけない。

AroundInvokeメソッドはビジネスメソッドでは駄目ということはインターセプタにステートレスセッションBeanを指定できないってことね

3.5.2 Interceptor Classes

箇条書きにします。

  • Beanクラスに対していくてもインターセプタクラスを指定できる。
  • 複数のインターセプタが定義されている場合は実行順序は記載順となる。
  • インターセプタクラスは引数なしのpublicなコンストラクタをもたねばいけない。
  • インターセプタはステートレス。
  • インターセプタは複数のスレッドで共有されるため、インターセプタのライフサイクルは定義されていない。
  • インターセプタのインスタンスが生成されるときにDependecy Injectionが行われる。その際対応するエンタープライズBeanのネーミングコンテキストが使用される。
  • InvocationContextオブジェクトのコンテキストデータを使用することで、Bean上のひとつのビジネスメソッドの呼び出しに対する複数のインターセプタメソッドの呼び出しをまたがって状態を伝えることができる。
  • インターセプタはアノテーションもしくはデプロイメント記述によって静的に設定される。

3.5.3 Multiple Interceptors for a Business Method Invocation

Beanクラスに複数のインターセプタが定義されている場合、Interceptorsアノテーションに定義されている順番でAroundInvokeメソッドが実行されます、って同じような説明がすでに出てますね。もしBeanクラス自身にAroundInvokeが定義されているならばInterceptorsアノテーションに定義されたすべてのインターセプタ実行後に自身のAroundInvokeメソッドが実行されるそうです。

AroundInvokeメソッドは常にInvocationContext.proceed()を呼び出す必要がある、さもなければビジネスメソッドも次のAroundInvokeメソッドも呼び出されない、とあります。InvocationContextはAroundInvokeメソッドにわたってくる引数ですね。

3.5.4 Method Sigunatures

AroundInvokeメソッドのシグネチャです。

public Object (InvocationContext) throws Exception

3.5.5 AroundInvoke methods

InvocationContextはAroundInvokeインターセプタメソッドに必要なメタデータをあらわします。InvocationContextのインターフェイスをコピペ。

public interface InvocationContext {
  public Object getBean();
  public Method getMethod();
  public Object getParameters();
  public void setParameters(Object);
  public EJBContext getEJBContext();
  public java.util.Map getContextData();
  public Object proceed() throws Exception;
}

InvocationContextのインスタンスがひとつのビジネスメソッドに対するそれぞれのAroundInvokeメソッドに渡されるため、インターセプタ間でデータの受け渡しができるそうです。名前からしてgetContextData()がまさにそのためのメソッドですね。

getBean()やgetMethod()はその名前のとおり、インターセプト対象のBeanインスタンスやメソッドを返します。
proceed()は次のインターセプタメソッドかビジネスメソッドを呼び出します。proceed()はメソッド呼び出しの戻り値を返しますが、ビジネスメソッドがvoidを返すときはnullになります。
getEJBContext()も名前があらわすとおりです。AroundInvokeメソッドは対応するBeanクラスとJNDIネームスペースを共有するそうです。あとJNDIへの依存を示すアノテーション(Annotations specifying JNDI dependencies)はインターセプタクラスではなくBeanクラスに適用される、とあるのですが…よくわかりません。Annotations specifying JNDI dependenciesとは@EJBや@Resourceのことだと思いますが、これはインターセプタクラスに指定できないということでしょうか?でも「3.5」のルールの説明のとこでインターセプタクラスに対してDependency Injectionがサポートされているってあったしなぁ。試してみればわかりそうです。
getParameters()やsetParameters(Object[])には特に説明がないですが、そのままの意味合いなんでしょう。

そういえば、ずいぶん前のJ2EE勉強会でひがさんがAOP AllianceのMethodInvocationがもつ情報には足りないものがある。だからS2で拡張せざるを得なかったとおっしゃっていましたがInvocationContextはAOP Allianceの足りないところを満たしているんでしょうか?(AOP AllianceのMethodInvocationには何がどういう理由で足りないのかは忘れました…)

3.5.6 Exceptions

箇条書きしてみます。

  • InvocationContext.proceed()で呼び出されるビジネスメソッドとAroundInvockeメソッドは同じJavaコールスタックで動くため、メソッド間では例外が伝播し、必要であればcatchし例外を変換したりそのまま握りつぶしたりできる。(かなり意訳)
  • AroundInvokeメソッドは実行時例外とビジネスメソッドのthrows節で認められている検査例外をスローできる。
  • AroundInvokeメソッドがproceed()の実行前に例外をスローする場合、他のAroundInvokeメソッドは実行されない。
  • AroundInvokeメソッドは実行時例外をスローするかInvocationContext.getEJBContext().setRollbackOnly()を実行することでトランザクションロールバックさせることができる。このロールバックはInvocationContext.proceed()を呼び出す前後どちらでも発生させることができる。

「AroundInvokeメソッドは実行時例外とビジネスメソッドのthrows節で認められている検査例外をスローできる。」とほぼ同じ文が「3.5」のインターセプタに適用されるルールのとこでも出てきますが、あっちはapplication exceptionでこっちではchecked exceptionと言っています。使い分けの根拠がわからない。言わんとしている意味は同じだと思うけど。


では、インターセプタに関して実験してみます。やってみたことは次のとおりです。

  • 複数のインターセプタを呼ぶ
  • Seasar2のTraceInterceptorと同等のことをしてみる
  • インターセプタ間でデータを共有する
  • インターセプタクラスでDependency Injectionが行われるか確かめる(@Resourceをつかってみる)
  • インターセプタクラスでlookupしてみる

ビジネスインタフェース

public interface Calculator {
  public int add(int x, int y);
  public int subtract(int x, int y);
}

Beanクラス:2つのインターセプタを@Interceptorsアノテーションで指定してます。

@Stateless
@Remote( { Calculator.class })
@Interceptors( { TraceInterceptor.class, CheckDiApplyingInterceptor.class })
public class CalculatorBean implements Calculator {

  public int add(int a, int b) {
    return a + b;
  }

  public int subtract(int a, int b) {
    return a - b;
  }
}

標準出力へトレース出力するインターセプタSeasar2のTraceInterceptorのほぼそのまんまです。ちがっているのはシグニチャとInvocationContext.getContextData()を使って無理やり実行されたインターセプタの数を出力するようにしたことくらい。あとサンプルということで標準出力へ出力するようにしました。

public class TraceInterceptor {
  
  @AroundInvoke
  public Object trace(InvocationContext inv) throws Exception {
    StringBuffer buf = new StringBuffer(100);
    buf.append(inv.getBean().getClass().getName());
    buf.append("#");
    buf.append(inv.getMethod().getName());
    buf.append("(");
    Object[] args = inv.getParameters();
    if (args != null && args.length > 0) {
      for (int i = 0; i < args.length; ++i) {
        buf.append(args[i]);
        buf.append(", ");
      }
      buf.setLength(buf.length() - 2);
    }
    buf.append(")");
    Object ret = null;
    Exception cause = null;
    System.out.println("BEGIN " + buf);
    try {
      inv.getContextData().put("interceptors", 1);
      ret = inv.proceed();
      buf.append(" : ");
      buf.append(ret);
    } catch (Exception e) {
      buf.append(" Exception:");
      buf.append(e);
      cause = e;
    }
    buf.append(" Executed Interceptor(s):");
    buf.append(inv.getContextData().get("interceptors"));
    System.out.println("END " + buf);
    if (cause == null) {
      return ret;
    } else {
      throw cause;
    }
  }
}

インターセプタクラスにDependency Injectionがされるか確かめる目的で作ったインターセプタ

public class CheckDiApplyingInterceptor {

  private DataSource dataSource;

  @Resource(name = "java:/DefaultDS")
  public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
  }

  @AroundInvoke
  public Object check(InvocationContext inv) throws Exception {
    System.out
        .println("setter injection was executed in interceptor class. : "
            + (dataSource != null));
    System.out.println("lookup was executed in interceptor class. : "
        + (inv.getEJBContext().lookup("java:/DefaultDS") != null));
    Integer count = (Integer) inv.getContextData().get("interceptors");
    inv.getContextData().put("interceptors", count + 1);
    return inv.proceed();
  }
}

クライアント

public class Client {
  public static void main(String[] args) throws Exception {
    InitialContext ctx = new InitialContext();
    Calculator calc = (Calculator) ctx.lookup(Calculator.class
        .getName());
    System.out.println(calc.add(100, 400));
    System.out.println(calc.subtract(100, 400));
  }
}

実行結果:ClientでBeanに対して2つのメソッドを実行しましたが、それに対応した出力がおこなわれています。インターセプタクラスに対してセッターインジェクションは行われなかったようです。「Executed Interceptor(s):2」となっているのでインターセプタ間のデータ共有はちゃんとできました。

02:00:13,097 INFO  [STDOUT] BEGIN study.ejb.CalculatorBean#add(100, 400)
02:00:13,097 INFO  [STDOUT] setter injection was executed in interceptor class. : false
02:00:13,097 INFO  [STDOUT] lookup was executed in interceptor class. : true
02:00:13,097 INFO  [STDOUT] END study.ejb.CalculatorBean#add(100, 400) : 500 Executed Interceptor(s):2
02:00:13,118 INFO  [STDOUT] BEGIN study.ejb.CalculatorBean#subtract(100, 400)
02:00:13,118 INFO  [STDOUT] setter injection was executed in interceptor class. : false
02:00:13,118 INFO  [STDOUT] lookup was executed in interceptor class. : true
02:00:13,118 INFO  [STDOUT] END study.ejb.CalculatorBean#subtract(100, 400) : -300 Executed Interceptor(s):2

感想/疑問/気づき など

  • インターセプタクラスに@ResourceアノテーションをつけてDIできるか確かめたのですが、できませんでした。「3.5」のインターセプタのルールのとこで「インターセプタクラスに対してDependency Injectionがサポートされている」とあった意味がわからないです。@EJBや@Resourceが使えないのは不便な気がします。lookupすればいいのかもしれないけど。
  • インターセプタを特定のビジネスメソッドにだけ適用させること(ポイントカットの指定)はできないみたいです。これも不便です。
  • AroundInvokeメソッドのシグネチャはメソッド名以外決まっていますが、インターセプタクラスを使う場合はどのみちひとつのAroundInvokeメソッドしか定義できないのでインターフェースでシグニチャを強制したほうがいいような気がしました。自分で「public Object invoke(InvocationContext inv) throws Exception」のシグニチャをもつインタフェースをつくってそのメソッドに@AroundInvokeアノテーションをつけて、インターセプタはこれを実装するようにしてみましたがうまくいきませんでした。@AroundInvokeをインタフェースのほうにつけても駄目みたいでした。ちなみにJBossではシグネチャが間違っている場合はデプロイ時に怒られます。
  • Beanクラス自身に@AroundInvokeアノテーションをつけたいときってどんなときでしょう。ステートフルセッションBeanを使っているときにクラスの不変条件チェックとかに使える?
  • EJB 3.0 ってビジネスインタフェースだけを用意して実装はAOPでみたいな事はできなさそう。

3.6 Home Interfaces

セッションBeanにもEJB 3.0 のエンティティBeanにもホームインタフェースは不要だそうです。詳しい話は別の箇所で述べるそうなのでここはこれくらい。


この章(Chapter 3)は長かったと思いきや8ページでした。