2012年4月13日金曜日

CGLIB Proxy で捕捉エリアを拡大


Spring Framework リファレンスの“8.6 Proxying mechanisms”に、Spring AOP はターゲットオブジェクトがインターフェースを実装している時は JDK の Dynamic Proxy、インターフェースを実装していない時は CGLIB Proxy を使うと記されています。

今回は、CGLIB Proxy を有効にして Pointcut の対象を広げます。

準備 - cglib
Spring AOP で流れを追う!』で触れたとおり、cglib(Code Generation Library) サイトから cglib-2.2.2.jar をダウンロードし、/WEB-INF/lib にコピーします。

asm
CGLIB Proxy を利用するためにはこれも必要です。cglib サイトのトップページ、あるいはOW2 Consortium サイトの ASM ページに行き、リンクを辿って asm-3.3.1-bin.zip を持ってきたら、解凍先の lib フォルダーに入っている asm-3.3.1.jar を /WEB-INF/lib にコピーします。

最新の asm-4.0 で試したところ cglib-2.2.2 がうまく動きませんでした。


CGLIB Proxy の有効化
リファレンス“8.6 Proxying mechanisms”に従って[servlet-name]-servlet.xml で以下の設定を行います。

<aop:aspectj-autoproxy proxy-target-class="true"/>


Pointcut の追加
まずは Join Point にしたいメソッド周りの特徴を見極めます。以下は『Bean Validation』で手を加えた AccountController クラスの一部です。

AccountController.java(抜粋)
package wrider.controller;
  :

@Controller
@SessionAttributes({"idCard", "userProfile"})
public class AccountController {
  :
  @RequestMapping(value="/account/login.html", method=RequestMethod.POST)
  public ModelAndView login(@Valid IdCard idCard, BindingResult br) {
  :
  }
  :
  @RequestMapping(value="/account/register.html", method=RequestMethod.POST)
  public ModelAndView register(@Valid UserProfile userProfile, BindingResult br) {
  :
  }
  :
}

クラス定義を見ると AccountController は wrider パッケージにあり、インターフェースを implements していないことがわかります。POST リクエストで呼び出される login()メソッドと register()メソッドは public で、どちらも2つ目の引数として BindingResult 型を受け取っています。この辺の特徴を Pointcut に定義したのが以下のコードです。

WebPointcuts.java(抜粋)
@Aspect
@Component
public class WebPointcuts {
  
  @Pointcut("within(com.scopeandtarget.wrider..*)")
  public void inWriderPackage() {}
  :
  @Pointcut("inWriderPackage() && execution(public * *(*,org.springframework..BindingResult))")
  public void postAction() {}
  
}

これは『Spring AOP で流れを追う!』で作った WebPointcuts クラスに手を加え、新たに postAction というメソッドを追加しています。@Pointcut アノテーションの中身を見ると、「wrider パッケージにある全ての型に定義されたメソッド」を表す inWriderPackage と、「public で2つ目の引数に BindingResult 型を持つメソッド」という条件を "&&" でつないでいます。

Advice の追加
続いて、Pointcut に新しく追加した postAction で呼び出される Advice を追加します。

WebAdvices.java(抜粋)
@Before("com.scopeandtarget.wrider.aop.pointcut.WebPointcuts.postAction() && args(*,param)")
  public void logPrePostRequest(final JoinPoint jp, final Object param) {
    
    BindingResult bindingResult = (BindingResult)param;
    
    String format = "{}.{} will be invoked!";
    outputLogMessage(
        format, 
        jp.getTarget().getClass().getSimpleName(), 
        jp.getSignature().getName());
    
    StringBuffer sb = new StringBuffer();
    
    sb.append("BindingResult has ..");
    sb.append(CoreConstants.LINE_SEPARATOR);
    for (Map.Entry entry : bindingResult.getModel().entrySet()) {
      sb.append(" key: " + entry.getKey());
      sb.append(CoreConstants.LINE_SEPARATOR);
      sb.append(" value: " + entry.getValue().toString());
      sb.append(CoreConstants.LINE_SEPARATOR);
      sb.append(" -----------------------------------");
      sb.append(CoreConstants.LINE_SEPARATOR);
    }
    
    logger.info(sb.toString());
  }

新たに logPrePostRequest メソッドが加わりました。@Before アノテーションを付けているので Before Advice です。インターセプトしたメソッドの 2 番目の引数を final Object 型で受け取り、メソッドの中で BindingResult 型にキャストしています。クラス名とメソッド名は、他の Advice と同じように、引数として受け取った JoinPoint を介して取得後、logger.info() でメッセージ内のプレースホルダーに埋め込んで出力しています。

一方 BindingResult は、取り出した key と value を StringBuffer に append しながらループし、最後に logger.info() で出力するようにしています。尚、改行に ch.qos.logback.core.CoreConstants を使用しているため、/WEB-INF/lib の logback-core-[version].jar をビルドパスに登録しています。


実行!
以上で作業は完了です。実際に動かし、吐き出されたログの一部が以下です。まず、CheckPasswordValidator、続いて CheckEmailValidator の isValid で入力値が検証され、AccountController の login メソッドが呼び出されている様子がわかります。検証結果は全て正常なので、最後にある BindingResult の内容は“0 errors”のみです。

尚、検証エラーがあった際の BindingResult の内容は『Validator と MessageSource』に一例を示しています。

ishtar.log
21:47:04.593 [ishtar] INFO  [http-bio-8080-exec-3] wrider.aop.advice.WebAdvices [WebAdvices.java:93] CheckPasswordValidator.isValid will be invoked! [q1w2e3r4]
21:47:04.593 [ishtar] INFO  [http-bio-8080-exec-3] wrider.aop.advice.WebAdvices [WebAdvices.java:93] CheckPasswordValidator.isValid is on going!
21:47:04.593 [ishtar] INFO  [http-bio-8080-exec-3] wrider.aop.advice.WebAdvices [WebAdvices.java:93] CheckPasswordValidator.isValid completed! result is true
21:47:04.640 [ishtar] INFO  [http-bio-8080-exec-3] wrider.aop.advice.WebAdvices [WebAdvices.java:93] CheckEmailValidator.isValid will be invoked! [wrider@abc.com]
21:47:04.640 [ishtar] INFO  [http-bio-8080-exec-3] wrider.aop.advice.WebAdvices [WebAdvices.java:93] CheckEmailValidator.isValid is on going!
21:47:04.640 [ishtar] INFO  [http-bio-8080-exec-3] wrider.aop.advice.WebAdvices [WebAdvices.java:93] CheckEmailValidator.isValid completed! result is true
21:47:04.640 [ishtar] INFO  [http-bio-8080-exec-3] wrider.aop.advice.WebAdvices [WebAdvices.java:93] AccountController.login will be invoked!
21:47:04.640 [ishtar] INFO  [http-bio-8080-exec-3] wrider.aop.advice.WebAdvices [WebAdvices.java:89] BindingResult has ..
 key: idCard
 value: wrider.model.IdCard@1875a82
 -----------------------------------
 key: org.springframework.validation.BindingResult.idCard
 value: org.springframework.validation.BeanPropertyBindingResult: 0 errors
 -----------------------------------


ログと聞くと一見地味な印象ですが、特にセキュリティやマーケティングでは必要不可欠な要素です。CGLIB Proxy を使うことで Pointcut に設定できる Join Point ―― Spring AOP の場合はメソッド実行のタイミングということですが――の領域を広げることができ、それに伴いログデータを採取できる機会も増えます。BigData の重要性が増しているといわれていますが、例えばシステム上でのユーザーの行動についての、より詳細で豊富なデータを集めたいというようなとき、Spring AOP を試してみるのも悪くないと思いました。

0 件のコメント:

コメントを投稿