ラベル AOP の投稿を表示しています。 すべての投稿を表示
ラベル AOP の投稿を表示しています。 すべての投稿を表示

2012年4月27日金曜日

トランザクションを“Declarative”に管理する

データベースにアクセスする際の“boilerplate(決まり文句)”にはうんざりさせられます。しかし、begin, commit/rollback といった決まり文句は、トランザクションの境界線をはっきりさせるために必要です。また、例外を捕まえて rollback するために try {..} catch {..}を使います。これもまた面倒です。

Declarative Transaction Management
Spring Framework はそうした面倒を緩和してくれる仕掛けを持っています。“Declarative Transaction Management(宣言的トランザクション管理)”もその一つです。これを使うと、「どの例外が発生したら rollback するか(declarative rollback rules)」とか「ダーティリードやファントムリードを許すか(isolation level)」とか「現在のトランザクションをサスペンドして、新しいトランザクションを生成するか(transaction propagation)」といったことを“宣言”できます。

宣言は XML で記述する方法と、@Transactional アノテーションの属性として記述する方法があります。今回、もちろん @Transactional は使いますが、種々の設定は XML で行うことにします。

その前に『Hibernate でデータアクセス(3)』で作成した hibernate.cfg.xml から、以下の行を削除しておきます。

<property name="hibernate.current_session_context_class">thread</property>

というか、JTA を使わないのであればこの項目は厳禁みたいです。以下のような書き込みが初歩的な相談事として巷に溢れていました。

宣言
リファレンス“11.5.1 Understanding the Spring Framework's declarative transaction implementation”に、Spring の宣言的トランザクションは AOP Proxy を介して実現されているとあります。実際、AOP と同様に AdvicePointcut を定義して、それらを Advisor で結び付けるというのが基本になります。

では、11.5.2 Example of declarative transaction implementation 以降の例と説明に従いながらトランザクションを宣言していきます。

名前空間
まずは、<tx:advice/> などの要素を利用するための名前空間を追加します。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    :
  xmlns:tx="http://www.springframework.org/schema/tx"
  xmlns:aop="http://www.springframework.org/schema/aop"
    :
  xsi:schemaLocation="
    :
  http://www.springframework.org/schema/tx 
  http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
  http://www.springframework.org/schema/aop 
  http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
    :

<tx:advice/>
この advice にトランザクションに対する各種属性を記述していきます。トランザクション属性を適用したいメソッド名を <tx:method/> タグの name で指定します。デフォルト設定のままでいい場合は、その他の記述は必要ありません。例えば <tx:method name="*"/> みたいな感じです。
read-only
トランザクションがリードオンリーの場合は true にします。
timeout
トランザクションがタイムアウトするまでの時間を秒数で指定します。
rollback-for
ロールバックの切欠となる例外クラスを指定します。複数の場合はカンマ区切り。@Transactional アノテーションの説明には、Throwable のサブクラスでなければならないと記述されています。FQCN(完全修飾クラス名)で書いた方が無難かも。
no-rollback-for
rollback-for の逆の意味です。
propagation
トランザクションの伝播(propagation)に関する設定です。11.5.7 Transaction propagationREQUIRED, REQUIRES_NEW, NESTED の動きが解説されています。

また、IBM developerWorks の記事“Transaction strategies: Understanding transaction pitfalls”は、propagation と他の設定を組み合わせた際の動作を知る参考になると思います。同じ read-only + propagation.REQUIRED でも JDBC と JPA では内部の動きが違うんですね。

因みにこの記事にもある Unit of Work の考え方については Hibernate Core リファレンス“13.1.1. Unit of work”が参考になります。
isolation:
トランザクションの分離(isolation)レベルに関する設定です。コミット前のデータでもかまわない場合は READ_UNCOMMITTED, 最低限コミットされていなければならない場合は READ_COMMITTED, トランザクション内での繰り返しリードでデータが変わると困る場合は REPEATABLE_READ, 他のトランザクションの影響は絶対に許さない場合は SERIALIZABLE, その辺のことはデータソースに丸投げで構わない場合は DEFAULT という感じになると思います。ロックやパフォーマンスにも関わる部分なので慎重に考えたい項目です。

以上を勘案して次のように設定してみました。「change」で始まるメソッドでは、全ての例外が rollback のトリガーとなるよう設定していますが、もっと条件を細かくしてもいいかもしれません。
:
  <bean id="txManager"
    class="org.springframework.orm.hibernate4.HibernateTransactionManager"
    p:sessionFactory-ref="sessionFactory">
  </bean>
  
  <tx:advice id="noRollBackTxAdvice" transaction-manager="txManager">
    <tx:attributes>
      <tx:method name="get*" read-only="true" isolation="READ_UNCOMMITTED" propagation="REQUIRED"/>
    </tx:attributes>
  </tx:advice>
  <tx:advice id="rollBackTxAdvice" transaction-manager="txManager">
    <tx:attributes>
      <tx:method name="change*" read-only="false" isolation="READ_COMMITTED" propagation="REQUIRES_NEW"
        rollback-for="java.lang.Throwable"/> 
      <tx:method name="*"/>
    </tx:attributes>
  </tx:advice>
    :

<aop:pointcut/>
pointcut と次の advisor は <aop:config/> 内に定義します。記述方法は AOP の規則に則ります。

以下のコードでは更新系トランザクションメソッドの pointcut に「wriderChangeOperation」、参照系に「wriderGetOperation」、ストアドプロシージャを呼び出すメソッドの pointcut に「wriderMakeOperation」という ID を割り当てています。

<aop:config>
    <aop:pointcut 
      expression="execution(* wrider.service.*.get*(..))" 
      id="wriderGetOperation"/>
    <aop:pointcut 
      expression="execution(* wrider.service.*.make*(..))" 
      id="wriderMakeOperation"/>
    <aop:pointcut 
      expression="execution(* wrider.service.*.change*(..))" 
      id="wriderChangeOperation"/>

<aop:advisor/>
advisor で上記 advice と pointcut を紐付けます。

<aop:advisor advice-ref="noRollBackTxAdvice" pointcut-ref="wriderGetOperation"/>
    <aop:advisor advice-ref="rollBackTxAdvice" pointcut-ref="wriderMakeOperation"/>
    <aop:advisor advice-ref="rollBackTxAdvice" pointcut-ref="wriderChangeOperation"/>
  </aop:config>

@Transactional アノテーションの有効化
リファレンス“11.5.6 Using @Transactional”に従って @Transactional を利用できるようにします。

<tx:annotation-driven transaction-manager="txManager"/>

次回は DAO レイヤーの boilerplate な部分を排除します。

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 を試してみるのも悪くないと思いました。

2012年4月11日水曜日

SLF4J と Logback でロギング!


前回の『Spring AOP で流れを追う!』で、特定の Aspect(相、特徴) を持つメソッド周りの動きを捕捉したので、今回は取得した情報を基にメッセージを整形し、ログに残します。

SLF4J
SLF4J(Simple Logging Facade for Java) とは、公式サイトにある SLF4J user manualイラストが表しているように、log4j や Logback といった様々な Logging Framework にシンプルな“Facade(ファサード)”、「外観」を提供するツールです。

因みに、この SLF4J だけでも前回の最後で見せたようなログメッセージをコンソールに出力することができます。

準備 - SLF4J
前回書いたとおり SLF4J サイトから slf4j-[version] の圧縮ファイルを持ってきて解凍後、slf4j-api-[version].jarslf4j-simple-[version].jar を /WEB-INF/lib にコピーし、ビルドパスにも登録します。

準備 - Logback
SLF4J user manual に『Logback の Logger クラスは SLF4J の Logger インターフェースの直接的な実装クラスなので、両者の接続においてリソースのオーバヘッドが無い』という旨が記されています。確かにイラストにも log4j や Apache Commons Logging(JCL の実装)に見られるようなアダプターが必要ない様子が示されています。

(これを信じて)Logback Project サイトから logback-[version] の圧縮ファイルを持ってきます。解凍先に出てくる 3 つの jar の内、次の 2 を /WEB-INF/ lib にコピーします。

logback-classic-[version].jar
logback-core-[version].jar

今回 Servlet のアクセスログは取らないので logback-access-[version].jar は使いません。


Logback の構成 - logback.xml
The logback manual の“Chapter 3: Logback configuration”によると、Logback は初期化時にまず、以下の順番でクラスパス上の構成ファイルを探します。

1. logback.groovy
2. logback-test.xml
3. logback.xml

クラスパスに以上のファイルが見つからなかった場合は自動的に ch.qos.logback.classic パッケージの BasicConfigurator を呼び出して基本構成を行います。

今回は、以下の“logback.xml”を作成しました。コンソール出力用(STDOUT)とファイル出力用(FILE)の 2 つの appender を定義しています。後者は前者に対して、より詳細な内容となるように定義しています。

logback.xml
<?xml version="1.0" encoding="utf-8"?>
<configuration debug="false">
  <contextName>ishtar</contextName>
  
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%contextName] %-5level %logger{18} - %msg%n</pattern>
    </encoder>
  </appender>
  
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>d:/usr/var/logs/logback/ishtar.log</file>
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%contextName] %-5level [%thread] %logger{36} [%file:%line] %msg%n</pattern>
    </encoder>
  </appender>
  
  <root level="info">
    <appender-ref ref="STDOUT"/>
    <appender-ref ref="FILE"/>
  </root>
</configuration>

<appender>要素
2 つの appender にname 属性で「STDOUT」、「FILE」という名前を定義しています。STDOUT では ConsoleAppender クラス、FILE では FileAppender クラスを使用します。両クラスとも OutputStreamAppender クラスを extends したもので、子要素の <encoder> で OutputSteam に乗せるバイトアレイへの変換方法などを Encoder に知らせます。

<pattern>要素
<encoder> の子要素である <pattern> で、具体的なレイアウトを指定します。今回は次の項目を適当に組み合わせて各 appender に設定しています。

時刻:%d{HH:mm:ss.SSS}
コンテキスト名:%contextName
ログレベル:level
スレッド名:%thread
ロガー名:%logger{文字数}
ロガーが出したメッセージ:%msg

指定可能な項目についての詳細はマニュアルの“Chapter 6: Layouts”にあります。

<timestamp>要素
尚、<timestamp>要素を使うと、例えば日付ごとに新しいログファイルを生成させることができます。
<contextName>ishtar</contextName>
  <timestamp key="byDate" datePattern="yyyyMMdd"/>
    :
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>d:/usr/var/logs/logback/ishtar-${byDate}.log</file>
    :
  </appender>

マニュアルの“Chapter 4: Appenders”に Appender についての詳細が記されています。ロールオーバーやリモートホスト/SMTPによる転送、データベースへの記録など、試してみたい機能が色々とあります。


メッセージの成型
Logback の導入に伴い前回作成した WebAdvices クラスの各 Advice メソッドに手を加えました。

ポイントは、SLF4J がサポートする“parameterized logging”と呼ばれる機能を利用していることです。直訳すると「パラメーター化されたロギング」となりますが、要は、雛形に含まれるプレースホルダーと文字列を対応付けてメッセージを成型する仕掛けということです。

logger.info()メソッドは、雛形となる String 型の引数と、そこに含まれる各プレースホルダー‘{}’に当てはめる文字列を Object[] 型の引数で受け取ることができます。WebAdvice クラスでは、これを利用して outputLogMessage() という private メソッドへの可変長引数という形でプレースホルダーに適用する値を渡しています。

WebAdvices.java(抜粋)
package com.scopeandtarget.wrider.aop.advice;
  :
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Aspect
@Component
public class WebAdvices {
  
  private Logger logger = LoggerFactory.getLogger(WebAdvices.class);

  @Before(..)
  public void logFieldValidationOccured(..) {
    
    String format = "{}.{} will be invoked! [{}]";
    outputLogMessage(
        format, 
        jp.getTarget().getClass().getSimpleName(), 
        jp.getSignature().getName(), 
        param.toString());
    
  }
  
  @AfterReturning(..)
  public Object logFieldValidationFinished(..) {
    
    String format = "{}.{} completed! result is {}";
    outputLogMessage(
        format, 
        jp.getTarget().getClass().getSimpleName(), 
        jp.getSignature().getName(), 
        retVal.toString());
    
    return retVal;
  }
  
  @Around(..)
  public Object logFieldValidationOnGoing(..)  throws Throwable {
    
    String format = "{}.{} is on going!";
    outputLogMessage(
        format, 
        pjp.getTarget().getClass().getSimpleName(), 
        pjp.getSignature().getName());
    
    return pjp.proceed();
  }
  
  private void outputLogMessage(String format, String... args) {
    logger.info(format, args);
  }
}

前回は、地道に文字列を連結して System.out.println() していましたが、今回はプレースホルダーを使ったことでコードが見易くなったと思います。


実行!
以上で一通りの作業は完了です。アプリケーションを起動しブラウザーで /account/login.html にアクセスすると、logback.xml で設定したログファイル([contextName].log)が作成され、呼び出されたバリデーターやフォームに入力されたデータ、検証結果、スレッド名、各 Advice メソッドで成型されたメッセージなどが克明に記録されています。

ishtar.log(抜粋)
:
14:42:10.156 [ishtar] INFO  [http-bio-8080-exec-7] wrider.aop.advice.WebAdvices [WebAdvices.java:66] CheckEmailValidator.isValid will be invoked! [wrider@abc.com]
14:42:10.156 [ishtar] INFO  [http-bio-8080-exec-7] wrider.aop.advice.WebAdvices [WebAdvices.java:66] CheckEmailValidator.isValid is on going!
14:42:10.156 [ishtar] INFO  [http-bio-8080-exec-7] wrider.aop.advice.WebAdvices [WebAdvices.java:66] CheckEmailValidator.isValid completed! result is true
14:42:10.156 [ishtar] INFO  [http-bio-8080-exec-7] wrider.aop.advice.WebAdvices [WebAdvices.java:66] CheckPasswordValidator.isValid will be invoked! [123456]
14:42:10.156 [ishtar] INFO  [http-bio-8080-exec-7] wrider.aop.advice.WebAdvices [WebAdvices.java:66] CheckPasswordValidator.isValid is on going!
14:42:10.156 [ishtar] INFO  [http-bio-8080-exec-7] wrider.aop.advice.WebAdvices [WebAdvices.java:66] CheckPasswordValidator.isValid completed! result is true
  :

当然の話ですが、アプリケーションは見た目上動いているだけでは駄目で、その内部動作を詳しく把握しておくことが理想です。Spring AOP と Logback のようなロギング・フレームワークを活用すれば、処理フローの捕捉と記録が割りと容易にできます。

ロギング・フレームワークの「機能面」に絞って言えば、開発や評価だけでなく、例えば「ログローテーションをきちんと計画するようなシステム」とか「アクセスログを DB に蓄積して、解析を行うようなシステム」とか、色々な用途に対応できると思います。

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