minokubaの日記 Twitter

2011-03-02

[]7.2 @AspectJ support

@AspectJアノテーションアスペクトを定義するやり方について説明します。なお@AspectJスタイルは、そもそもAspectJ5で導入されたものです。SpringではAspectJで定義するアノテーションと連携します。AOPランタイムはあくまでSpringのものであり、AspectJとは関係ありません。(APIだけぱくった)実際にAspectJと連携する例については、7.8で説明します。

7.2.1 Enabling @AspectJ Support

@AspectJアノテーションを使うためには、@AspectJアノテーションベースのAOP/自動プロキシ生成の機能を適用する設定をします。まぁ要するにこう書けばいいわけです

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:aop="http://www.springframework.org/schema/aop"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/aop http://www.springframework.org/
      schema/aop/spring-aop-3.0.xsd
      http://www.springframework.org/schema/context http://www.springframework.org/
      schema/context/spring-context-3.0.xsd">

  <aop:aspectj-autoproxy/> ←AspectJアノテーションをもとにプロキシ生成する設定
  
  <context:component-scan base-package="hello.spring.aspect">
    <context:include-filter type="annotation" 
    expression="org.aspectj.lang.annotation.Aspect" /> ←@Aspectをスキャンする
  </context:component-scan>
</beans>

あと、AspectJライブラリとして「aspectjweaver.jar」と「aspectjrt.jar」をクラスパスに設定してください。

7.2.2 Declaring an aspect

上記の設定をすると、@Aspectアノテーションを付与し、アプリケーションコンテキストに定義したBeanが自動的にSpringにより検索され、SpringAOPにセットアップされます。例えばAspectアノテーションを付与したクラス

@Aspect
public class NotVeryUsefulAspect {
}

 <bean id="myAspect" class="org.xyz.NotVeryUsefulAspect"/>

みたいに定義すればよいわけです。そしてこのクラスにPointCut、Advice、Introductionを定義していきます。

アスペクトの自動スキャン

@Aspectを自動スキャンする(Bean登録を明示的にしない)場合は例えば@Componentを付与すると良いです。もしくはcomponent-scanに@Aspectをスキャンする設定を加える。なお、アスペクトに対するアドバイスの設定はできません。自動的に適用対象外となります

7.2.3 Declaring a pointcut

PointCutを定義することで、Aspectを差し込む場所(JoinPoint)を指定できます。SpringAOPはメソッドの実行に対する介入のみ可能なので、Spring管理下にあるBeanのメソッドとのマッチング定義を考えれば良いわけです。SpringAOP(アノテーションスタイル)では、正規表現メソッド名を定義し、これを@Pointcutアノテーションアスペクトに紐付けます。例えばこんな感じです。

・transferというメソッドマッチするすべてのメソッドに対するpointcut「anyOldTransfer」を定義する例。

@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature

正規表現はAspectJ5の表記方に従います。この詳細な仕様は以下を参照してください。

http://www.eclipse.org/aspectj/doc/released/progguide/index.html

7.2.3.1 Supported Pointcut Designators

SpringAOPでは次のAspectJ Pointcut Designatiorsが使えます。

execution呼出先のmethodに対してマッチングをかけます。これが基本です。
within呼出先のクラスで特定します。静的に評価するイメージです。
this呼出先の型を実装するProxyオブジェクトを指定します。実行時に評価するイメージです
target呼出先の型を実装するインスタンスを指定します。実行時に評価します。
インタセプタの引数に渡したい場合に使用するのが主目的
args引数が指定された型のメソッドを引っ掛けます。実行時に評価するイメージです。
(なのでシグニチャ一致ではない)。
インタセプタの引数に渡したい場合に使用するのが主目的
@targettargetと同じだが、型ではなくアノテーションで絞り込む
インタセプタの引数に渡したい場合に使用するのが主目的<
@argsargsと同じだが、型ではなくアノテーションで絞り込む
インタセプタの引数に渡したい場合に使用するのが主目的
@withinwithinと同じだが、型ではなくアノテーションで絞り込む
インタセプタの引数に渡したい場合に使用するのが主目的
@annotationアノテーションが付与されたメソッドをすべて引っ掛けます
beanbean名で特定します。

なお、なおwithin,target,args,@within,@target,@annotation,@argsはFQCNを書くのではなくインタセプタの引数名を指定する書き方もある。こういう感じ。詳細は「7.2.4.6 Advice parameters」を参照。

@Aspect
public class SampleAspect{
    @Around("@annotation(annotation)")
    public String  helo3(ProceedingJoinPoint pjp,HogeAnnotation annotation) throws Throwable{
        System.out.println(annotation.name());
        Object o = pjp.proceed();
        returno.toString();
    }
}

◆制約

SpringAOPは、InvocationHandlerを使うば合うでもCGLIBを使う場合でも、protectedメソッドにはおりこめない。publicメソッドにしか織り込めません。もしprotected/privateメソッドコンストラクタに介入したい場合は、AspectJを使ってください。

◆制約

Bean PCDはSpringAOPでしか使えずAspectJ Weavingでは使えないことに注意してください。あとBean PDCインスタンスに対する(プロキシ型の)介入です。(他のは全てクラスに対するバイトコード操作による介入)。

7.2.3.2 Combining pointcut expressions

pointcutの表記は&&,||,!で組み合わせることができます。またpointcut表現を名前で指定することもできます。例えば、こういうことです。

    @Pointcut("execution(public * *(..))")
    private void anyPublicOperation() {}
    
    @Pointcut("within(com.xyz.someapp.trading..*)")
    private void inTrading() {}
    
    @Pointcut("anyPublicOperation() && inTrading()")
    private void tradingOperation() {} →com.xyz.someapp.tradingパッケージにあるすべてのメソッド

ベストプラクティスとしては、もっと複雑なpointcutを指定する場合は短い名前のコンポーネントを割り当てるといいです。Pointcutを名前で参照する場合はすべてのjavaの可視性に関するルールが適用されます。(継承階層、publicはどこからでもみえる、など)

7.2.3.3 Sharing common pointcut definitions

pointcutの定義を共有したくなりますね。我々のおすすめは、「SystemArchitecture」というアスペクトを定義するのが良いと思います。例えばこんな感じです。

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

  /**Web層のクラスを呼び出すタイミング */
  @Pointcut("within(com.xyz.someapp.web..*)")
  public void inWebLayer() {}

  /**サービス層のクラスを呼び出すタイミング */
  @Pointcut("within(com.xyz.someapp.service..*)")
  public void inServiceLayer() {}

  /**DAO層のクラスを呼び出すタイミング*/
  @Pointcut("within(com.xyz.someapp.dao..*)")
  public void inDataAccessLayer() {}

  /**サービス層のメソッド呼び出し*/
  @Pointcut("execution(* com.xyz.someapp.service.*.*(..))")
  public void businessService() {}
  
  /**DAO層のメソッド呼び出し*/
  @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
  public void dataAccessOperation() {}

}

で、これを例えばこういう感じで参照するわけですよ。

<aop:config>
  <aop:advisor 
      pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
      advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
  <tx:attributes>
    <tx:method name="*" propagation="REQUIRED"/>
  </tx:attributes>
</tx:advice>

7.2.3.4 Examples

SpringAOPでは普通はexecutionを使ってpointcutを定義すると思います。

  execution(
    modifiers-pattern? ret-type-pattern declaring-type-pattern?
    name-pattern(param-pattern) throws-pattern?)

正規表現はこんな感じ

( *は全てを表す)

パラメータのパターンは

() パラメータなし
(..) どんな引数でも良い(引数なしでもいい)
(*) どんな引数でもいいけど引数はひとつ
(*,String) 二つ引数を取り、一つ目の型は自由。

細かくはこちらを。

http://www.eclipse.org/aspectj/doc/released/progguide/semantics-pointcuts.html

以下に例を示す

すべてのpublicメソッドexecution(public * *(..))
setで始まるすべてのメソッドexecution(* set*(..))
AccountServiceインタフェースに定義されたすべてのメソッドexecution(* com.xyz.service.AccountService.*(..))
サービスパッケージのすべてのメソッドexecution(* com.xyz.service.*.*(..))
サービスパッケージもしくはそのサブパッケージのすべてのメソッドexecution(* com.xyz.service..*.*(..))
serviceパッケージ以下にあるすべてのクラスのメソッドwithin(com.xyz.service.*)
サービスパッケージもしくはそのサブパッケージにあるすべてのクラスのメソッドwithin(com.xyz.service..*)
AccountServiceを実装するすべてのメソッドthis(com.xyz.service.AccountService)
AccountServiceを実装するすべてのメソッドtarget(com.xyz.service.AccountService)
Serializableを引数として取るすべてのJoinPointargs(java.io.Serializable)
execution(* *(java.io.Serializable))との違いは、
前者はシグニチャに出てなくても良くて
実行時にSerializableなら対象になる。
後者はシグニチャマッチ。
@Transactionalアノテーションをつけたすべてのオブジェクトメソッド呼出@target(org.springframework.
transaction.annotation.Transactional)
@Transactionalアノテーションをつけたすべてのオブジェクトメソッド呼出@within(org.springframework.
transaction.annotation.Transactional)
@Transactionalアノテーションを付与したすべてのメソッド呼出@annotation(org.springframework.
transaction.annotation.Transactional)
@Classifiedアノテーションが付与されたオブジェクト引数でもらうすべてのメソッド@args(com.xyz.security.Classified)
bean名がtradeServicebean(tradeService)
bean名がServiceで終わる全てのBeanbean(*Service)

7.2.3.5 Writing good pointcuts

Pointcutには三種類あります。

種類による識別例えばget/set/などで絞り込む
scopingによる識別withinなどで絞り込む
コンテキストによる識別this,target,@annotationなど

よくあるポイントカットは最初の二つです。で、性能面で言うとScopingによる識別はとてもはやいです。

7.2.4 Declaring advice

Adviceの宣言。@Before,@After,@Around.

やり方としては、ポイントカット名を指定するやり方と、アドバイスにポイントカットの正規表現を埋め込むやり方がある。

7.2.4.1 Before advice

ポイントカット名を指定する例

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

  @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
  public void doAccessCheck() {
    // ...
  }

}

正規表現を埋め込む例

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

  @Before("execution(* com.xyz.myapp.dao.*.*(..))")
  public void doAccessCheck() {
    // ...
  }

}

7.2.4.2 After returning advice

メソッドが通常にリターンされた後に呼ばれる。@AfterRunninngを付与。returning属性を使うと、アドバイスの中でメソッド戻り値を参照できる。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

  @AfterReturning(
    pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
    returning="retVal")
  public void doAccessCheck(Object retVal) {
    // ...
  }
  
}

7.2.4.3 After throwing advice

例外が発生した場合に呼ばれる。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

  @AfterThrowing(
    pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
    throwing="ex")
  public void doRecoveryActions(DataAccessException ex) {
    // ...
  }

}

throwing属性を使うと発生した例外をメソッド引数で取得することができる

7.2.4.4 After (finally) advice

とにかくメソッド実行が終わったら一番最後に呼ばれる

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

  @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
  public void doReleaseLock() {
    // ...
  }

}

7.2.4.5 Around advice

要するにインタセプタ(フィルタ)方式です。第一引数にはかならずProceedJoinPointを取る必要があります。なおProceedJoinPoint#proceedメソッドに与えた引数はそのままメソッド引数となりますが、これはAspectJの場合とSpringAOPで意味が違うらしいです(詳細分からんかった、とりあえず飛ばし)またアドバイスの戻り値メソッド呼び出し元に返却されます。

7.2.4.6 Advice parameters

Adviceに渡せる引数について。

・JoinPoint

すべてのadviceに対しては、adviceの第一引数といてJoinPointを渡すことができる。なおArowndアドバイスについては、JoinPointのサブクラスであるProcessingJoinPointを渡せる。JoinPointはgetArgs/getThis/getTarget/getSignature/toStringなどといった便利な機能を提供する。

・アドバイスに対する引数の指定(メソッド引数

戻り値や例外の引渡しは、前のXXで説明したので省略。またメソッドに対する引数も同じように指定できる。まぁこんな感じだ

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" + 
        "args(account,..)")
public void validateAccount(Account account) {
  // ...
}

第一引数がAccount型のメソッドに対して上記は介入し、アドバイスの引数として値を取ることができる。定義の仕方としては、以下のようにしてPointcutとAdviceを分けて定義することもできる。(アドバイスで第一引数にAccount取るものと定義し、ポイントカットでこのアドバイスを参照する)

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() &&" + 
          "args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
  // ...
}

・アドバイスに対する引数の指定(アノテーション

引数と同様に、proxy(this),target,アノテーション(@with,@target,@annotation,@args)も取得できる。例えば@Auditableというメソッドアノテーションを付与したメソッドに対して介入する場合は

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && " + 
        "@annotation(auditable)")
public void audit(Auditable auditable) {
  AuditCode code = auditable.value();
  // ...
}

とやってアノテーションを取得できる。

・アドバイスに対する引数の指定(Generics

genericsの型引数でアドバイスを絞り込むことも出来る。

public interface Sample<T> {
  void sampleGenericMethod(T param);
  void sampleGenericCollectionMethod(Collection<T> param);
}

というクラスに対して以下のポイントカットを設定する。

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
  // Advice implementation
}

#+はサブクラスを表す。任意パッケージにあるSampleというインタフェースを実装したsampleGenericMethodに対するポイントカット。上記は、beforeSampleMethodに対する引数がMyTypeのもの、つまりSampleの<T>がMyTypeのものだけが対象になる。ただし、Collection<T> までは大変なので辿りませんごめんなさい。つまり以下のように書いてもちゃんと動きませんよ

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
  // Advice implementation
}

引数名の決定方法

型の引数名をアノテーションJava引数名でマッチングしてるわけですが、実際リフレクションでは引数名取れません。なので、引数名はこういうルールで決めてます。

1.argNamesでしていしたら、この名前が適用

@Before(
   value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
   argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
  AuditCode code = auditable.value();
  // ... use code and bean
}

ただしargNamesにはJoinpointは省略できます。

@Before(
   value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
   argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
  AuditCode code = auditable.value();
  // ... use code, bean, and jp
}

2.argNamesが面倒なら、デバッグオプション付けてコンパイルしてください。クラスのサイズが大きくなったり、デコンパイルされたり、最適化がイマイチになる問題はありますけどね。AspectJ コンパイラコンパイルするなら、デバッグオプションつけなくてもいいです。

3.1と2が該当しない場合は、Springが推測できる範囲でマッピングします。

4.上記マッピングに失敗したらIllegalArguementExceptionが投げられます。

Proceeding with arguments

??

We remarked earlier that we would describe how to write a proceed call with arguments that works consistently across Spring AOP and AspectJ. The solution is simply to ensure that the advice signature binds each of the method parameters in order. For example:

@Around("execution(List<Account> find*(..)) &&" +
        "com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")		
public Object preProcessQueryPattern(ProceedingJoinPoint pjp, String accountHolderNamePattern)
throws Throwable {
  String newPattern = preProcess(accountHolderNamePattern);
  return pjp.proceed(new Object[] {newPattern});
}        

7.2.4.7 Advice ordering

アスペクトにorg.springframework.core.Orderedを実装させるか、アスペクトに@Orderを付与して指定する。Ordered.getValue()の戻り値が小さいものが、優先度こうとみなさえる。また同じアスペクトが2回以上重複してアドバイスされた場合は、実行順序は保証されない。そもそもそう言うのはやめたほうがいいです。

7.2.5 Introductions

これは自分の言葉で書いたほうがいいからそれで書くby minokuba.

要するに、特定のFQCNのパターンを満たすBeanに対して、特定のインタフェースを実装させるアスペクト。実装内容は、別の実装したBeanの内容を差し込む感じ。

#Introduction対象のBean

@Bean
package hello.spring.aspect;
public class Bean{
}

#Mixinするインタフェース

public interface Mixin{
  public void hoge();
}

#Mixin実装

public class DefaultMixin implements Mixin{
  public void hoge(){
    System.out.println("hoge");
  }
}

#Introductionの指定

@Aspect
public class IntroductionAspect{
  @DeclareParents(value="hello.spring.aspect.*+",
  defaultImpl=DefaultMixin.class)
    public static Mixin mixin;
    //面倒なのでここでmainかいた
    public static void main(String args[]) throws Exception{
        ClassPathXmlApplicationContext applicationContext = 
          new ClassPathXmlApplicationContext("hello/spring/aspect/xspring.xml");
        Bean bean = applicationContext.getBean(Bean.class);
        ((Mixin)bean).hoge();
    }
}

なるほどー。

7.2.6 Aspect instantiation models

(This is an advanced topic, so if you are just starting out with AOP you can safely skip it until later.)

だそうなので後回し。

7.2.7 Example

悲観ロックが発生した場合に、ビジネスロジックリトライを行う機能をArowndAdviceで実装する例。ご参考レベル。

http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/aop.html#aop-ataspectj-example

なおAspectのパラメータをbean定義ファイルにbean登録することで実現するパターンはよさそうかと思った。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証