2012年4月10日火曜日

Spring AOP で流れを追う!


アプリケーションを開発していると作りこんだクラスやメソッドに関して「入出力はどうなっているか」とか、「そもそも呼ばれているのか」といったことが気になることがあります。そんなとき Spring Framework の Spring AOP(Aspect Oriented Programming)が重宝します。今回は、Spring AOP を使い、前回の『Bean Validation』で作ったバリデーターの動きを追跡してみたいと思います。

Pointcut, Join point, Advice
Spring AOP の目玉は、Pointcut(ポイントカット) の記述言語として AspectJ を採用していることです。Spring AOP の詳しい用語解説は、リファレンスの“8.1.1 AOP concepts”に書かれていますが、Pointcutとは要するに、プログラムの中で共通の特徴を持つ(いくつかの)場所(Join Point)に、何らかの処理(Advice)を差し込むための条件です。Spring AOP ではそうした条件の記述に AspectJ が利用できるということです。

準備 - AspectJ
というわけで AspectJ を準備します。The AspectJ Project サイトから aspectj-[version].jar をダウンロードし、同サイトの FAQ ページにある“2 Quick Start”に従って、適当な場所にインストールします。因みに次のように打ち込めばインストーラーが起動します。

java -jar aspectj-[version].jar

すると [インストール先フォルダー]/lib に必要な jar が入っているので、これらを /WEB-INF/lib にコピーし、ビルドパスに追加します。

準備 - SLF4J
これは次回のための準備としていれておきます。SLF4J(Simple Logging Facade for Java) サイトから slf4j-[version] の圧縮ファイルを持ってきて解凍後、slf4j-api-[version].jar と slf4j-simple-[version].jar を上記と同様、ビルドパスに登録します。


その他
aopalliance.jar
古いファイルですが、これが無いと AOP を有効にして立ち上げようとした時に怒られます。AOP Alliance サイトからリンクを辿って AOP Alliance フォルダーに行き、aopalliance.zip をダウンロードします。解凍後 /WEB-INF/lib にコピーします。

cglib-2.2.2.jar
8.1.3 AOP Proxies に書いてありますが、Spring AOP はインターフェースを実装していないクラスに対しては CGLIB(Code Generation Library) Proxy を使うそうです。将来的に必要となるかもしれないので、これも CGLIB サイトから持ってきて /WEB-INF/lib にコピーしておきます。


AOP Proxy の有効化
applicationContext.xml [Servlet-name]-servlet.xml に以下の一行を追加して AOP Proxy を有効化します。

<aop:aspectj-autoproxy/>


Pointcut の定義
一通りの準備が整ったところで、Pointcut の定義に取り掛かります。まずは Join Point にしたいメソッドの特徴の見極めです。以下のコードは、パスワードのバリデーションを行う CheckPasswordValidator クラスです。

CheckPasswordValidator
前回のコードに少し手を加えています。正規表現でチェックする部分を wrider.utils.AbstractRegexpUtils という抽象クラスに持たせ、CheckPasswordValidator はこれを extends しています。また、有効文字数をアノテーションの引数 min と max で指定できるようにしています。後者に伴い CheckPassword.java も少し変わりましたが、コードは AbstractRegexpUtils.java と共に割愛させていただきます。
package wrider.validator;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.springframework.util.StringUtils;

import wrider.annotation.CheckPassword;
import wrider.utils.AbstractRegexpUtils;

public class CheckPasswordValidator extends AbstractRegexpUtils
  implements ConstraintValidator {

  private static final String BASE_PATTERN = "^[a-zA-Z0-9]";
  private int max;
  private int min;
  
  public void initialize(CheckPassword constraintAnnotation) {
    max = constraintAnnotation.max();
    min = constraintAnnotation.min();
  }
  
  public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
    if (!StringUtils.hasLength(object)) {
      return true;
    }
    else {
      final String PASSWORD_PATTERN = BASE_PATTERN + "{" + min + "," + max + "}$";
      return super.patternMatching(PASSWORD_PATTERN, object);
    }
  }
  
}

上記クラスは javax.validation.ConstraintValidator インターフェースの実装クラスで、wrider.validator パッケージにあり、boolean 型の値を返す isValid メソッドを持っています。同パッケージには、メールアドレスのバリデーションを行う CheckEmail クラスもあり、同様の特徴を持っています。

そこで、これらのクラスの isValid メソッドを Join point とするよう Pointcut を定義したのが次のコードです。

WebPointcuts.java
package wrider.aop.pointcut;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class WebPointcuts {
  
  @Pointcut("within(wrider..*)")
  public void inWriderPackage() {}
  
  @Pointcut("execution(public boolean isValid(..))")
  public void doValidate() {}
  
  @Pointcut("inWriderPackage() && doValidate()")
  public void fieldValidation() {}
  
}

クラス定義を @Aspect と @Component でアノテートしています。これにより「component-scan と Stereotypeアノテーション」で書いたように[servlet-name]-servlet.xml で <context:component-scan /> が有効になっていれば、Spring Framework が自動検出してくれます。

クラス定義の中にはいくつかの空のメソッドがあり、それぞれに @Pointcut アノテーションが付いています。最初のメソッドは「wrider パッケージ内のすべての型におけるメソッドの実行」と定義した inWriderPackage、次が「public で、boolean 型の返り値と任意の引数を持つ isValid メソッドの実行」を対象とした doValidate です。そして最後の fieldValidation は、上記二つを同時に満たす Pointcut の定義です。

このようにプログラムの「aspect(相、特徴)」に着目した記述ができるのが AspectJ です。

Advice の定義
Advice には大別して Join Point の直前で実行する Before Advice、Join Point 終了後に実行する After Advice、Join Point が呼び出された辺りで実行する Around Advice があります。

WebAdvices.java
以下のコードでは 3 つの Advice が定義しています。いずれも Pointcut に“fieldValidation”を指定し、文字列を連結して作ったメッセージを System.out.println() でコンソールに出力している点は共通していますが、@Before, @AfterReturning, @Around の違いに応じて、返り値や引数の扱いを変えています。

@Before - Before Advice
実行直前にインターセプトしたメソッドの最初の引数を、final Object 型の引数(param)として受け取るよう指定しています。jp.getTarget().getClass().getSimpleName() でクラス名、jp.getSignature().getName() でそのクラスのメソッド名を取得しています。そしてメッセージには「実行前」ということで“will be invoked!”の文字列を含めています。
@Before("wrider.aop.pointcut.WebPointcuts.fieldValidation() && args(param,*)")
  public void logFieldValidationOccured(final JoinPoint jp, final Object param) {
    
    Signature sig = jp.getSignature();
    String cn = jp.getTarget().getClass().getSimpleName();
    String buf = cn + "." + sig.getName() + " will be invoked! [" + param.toString() + "] ";
    
    System.out.println(buf);
    
  }

@AfterReturning - AfterReturning Advice
メソッド実行後の返り値を final Object 型の引数(retVal)で受け取り、それをそのまま return しています。インターセプトしたクラス名、メソッド名の取得は上記と同じです。
@AfterReturning(
      pointcut="wrider.aop.pointcut.WebPointcuts.fieldValidation()", 
      returning="retVal")
  public Object logFieldValidationFinished(final JoinPoint jp, final Object retVal) {
  :
    return retVal;
  }

@Around - Around Advice
ProceedingJoinPoint インターフェースの proceed() メソッドを使って、進行中の状態を return しています。クラス名、メソッド名の取得は上の 2 つと異なり、Around Advice で使用できる ProceedingJoinPoint から取得しています。
@Around("wrider.aop.pointcut.WebPointcuts.fieldValidation()")
  public Object logFieldValidationOnGoing(final ProceedingJoinPoint pjp)  throws Throwable {
  :
    return pjp.proceed();
  }

全体のコードです。
package wrider.aop.advice;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.AfterReturning;

import org.springframework.stereotype.Component;

@Aspect
@Component
public class WebAdvices {
  
  @Before("wrider.aop.pointcut.WebPointcuts.fieldValidation() && args(param,*)")
  public void logFieldValidationOccured(final JoinPoint jp, final Object param) {
    
    Signature sig = jp.getSignature();
    String cn = jp.getTarget().getClass().getSimpleName();
    String buf = cn + "." + sig.getName() + " will be invoked! [" + param.toString() + "] ";
    
    System.out.println(buf);
    
  }
  
  @AfterReturning(
      pointcut="wrider.aop.pointcut.WebPointcuts.fieldValidation()", 
      returning="retVal")
  public Object logFieldValidationFinished(final JoinPoint jp, final Object retVal) {
    
    Signature sig = jp.getSignature();
    String cn = jp.getTarget().getClass().getSimpleName();
    String buf = cn + "." + sig.getName() + " completed! result is " + retVal.toString();
    
    System.out.println(buf);
    
    return retVal;
  }
  
  @Around("wrider.aop.pointcut.WebPointcuts.fieldValidation()")
  public Object logFieldValidationOnGoing(final ProceedingJoinPoint pjp)  throws Throwable {
    
    Signature sig = pjp.getSignature();
    String cn = pjp.getTarget().getClass().getSimpleName();
    String buf = cn + "." + sig.getName() + " is on going!";
    
    System.out.println(buf);
    
    return pjp.proceed();
  }
}

実行!
では試して見ましょう。今まで再三使ってきた login.html にアクセスし、エラーとなる文字列を入力した結果が以下のコンソール画面です。青字が各 Advice の出力です。
1: makeIdCard has been invoked!
2: makeUserProfile has been invoked!
3: login[GET] has been invoked!
CheckEmailValidator.isValid will be invoked! [wrider] 
CheckEmailValidator.isValid is on going!
CheckEmailValidator.isValid completed! result is false
CheckPasswordValidator.isValid will be invoked! [123] 
CheckPasswordValidator.isValid is on going!
CheckPasswordValidator.isValid completed! result is false
4: login[POST] has been invoked!
got email: wrider

CheckEmailValidator に着目すると

 ~ will be invoked [wrider]
  ↓
 ~ is on going!
  ↓
 ~ completed! result is false

というメッセージの流れから“Before”→“Around”→“AfterReturning”という順番で Advice が呼び出されていることがわかります。また、CheckEmailValidator が「wrider」という文字列の検証で「false」を返している様子もわかります。

次回の予告(かも?)
最後に、SLF4J を使って各 Advice を書き換えた場合のコンソール出力を載せておきます。

SLF4Jによるコンソール出力
10969 [http-bio-8080-exec-3] INFO wrider.aop.advice.WebAdvices - CheckEmailValidator.isValid will be invoked! [wrider] 
10969 [http-bio-8080-exec-3] INFO wrider.aop.advice.WebAdvices - CheckEmailValidator.isValid is on going!
10969 [http-bio-8080-exec-3] INFO wrider.aop.advice.WebAdvices - CheckEmailValidator.isValid completed! result is false

1 件のコメント:

  1. MGM Resorts to Open New Casino Hotel in Reno | DRMCD
    The resort will be 파주 출장안마 the first 양산 출장샵 hotel in the iconic Strip to reopen its 포천 출장샵 iconic Reno-Tahoe 진주 출장안마 casino, 구리 출장샵 with several notable changes being made in

    返信削除