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

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