はじめに
ソフトウェア開発の目的はさまざまな要件を満たすことです。これらの要件はビジネス要件とシステム要件に分けられます。例えば、ログイン機能において、ユーザーがパスワードを入力してログインする動作はビジネス要件ですが、ログイン前後にログを出力する処理はシステム要件です。同様に、ユーザーがデータを取得する動作はビジネス要件ですが、アクセス時に権限チェックを行う処理はシステム要件です。ビジネス要件はユーザーが意識するものであり、システム要件はユーザーが意識しないものといえます。
ビジネス要件は実装コードと1対1の関係にあることが多いですが、システム要件は1対多の関係になります。例えば、操作ログの出力機能は登録時にもログ出力し、ログイン時にもログ出力します。このようなシステム要件を各ビジネスコードに直接記述すると、保守が困難になるため、共通の処理を分離して自動的に挿入する必要があります。SpringのAOPはこのような設計思想に基づいています。
1. AOPの概要
AOP(Aspect Oriented Programming)は、ログ出力や権限チェックなどのシステム要件を「切り口」として、複数のビジネス機能に横断的に適用するプログラミング手法です。この切り口を「アスペクト」と呼びます。
1.1 AOPの基本概念
- JoinPoint(接続点):横断処理を挿入可能なポイント(メソッド呼び出し、メソッド実行など)
- PointCut(切点):特定のJoinPointをパターンで定義(例:
com.example.service.*.add*()) - Advice(アドバイス):横断処理の実装(ログ出力、権限チェックなど)
- Aspect(アスペクト):PointCutとAdviceの組み合わせ
- Target(ターゲット):アスペクトを適用する対象オブジェクト
- Weaving(織り込み):アドバイスをターゲットに組み込むプロセス
要約すると、「ターゲットのJoinPointに、AdviceとPointCutで構成されたAspectをWeavingする」となります。
1.2 アドバイスの種類
| 種類 | 実行タイミング | 説明 |
|---|---|---|
| Before Advice | メソッド実行前 | パラメータ検証などに使用 |
| After Returning | 正常終了後 | 戻り値の処理に使用 |
| After Throwing | 例外発生時 | エラーロギングなどに使用 |
| After Advice | 常に実行 | finallyブロックに相当 |
| Around Advice | 実行前後両方 | 時間計測などに使用 |
| Introduction | 定義変更なし | 新しいメソッド/属性の追加 |
1.3 織り込みの種類
- コンパイル時織り込み:コンパイル時にアドバイスを組み込む
- クラスロード時織り込み:クラスロード時にアドバイスを組み込む
- ランタイム織り込み:実行時に動的にアドバイスを組み込む
2. AOPの実装
2.1 実装例
Spring 2.0以降は@AspectアノテーションでAOPを実装できます。以下に実装例を示します。
@Aspect
@Component
public class LogAspect {
private final ThreadLocal<Long> startTime = new ThreadLocal<>();
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
startTime.set(System.currentTimeMillis());
String methodName = joinPoint.getSignature().getName();
System.out.println("Before: " + methodName);
}
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("After: " + methodName + ", 時間: " + (System.currentTimeMillis() - startTime.get()));
}
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
System.out.println("Around Start: " + methodName);
Object result = joinPoint.proceed();
System.out.println("Around End: " + methodName + ", 時間: " + (System.currentTimeMillis() - start));
return result;
}
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void logException(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Exception: " + methodName + ", エラー: " + ex.getMessage());
}
@AfterReturning(pointcut = "execution(!void com.example.service.*.*())", returning = "result")
public String logReturn(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Return: " + methodName + ", 結果: " + result);
return result.toString();
}
}
2.2 代理オブジェクトの取得
内部メソッド呼び出しではアドバイスが適用されない場合があります。これを回避するには、AopContext.currentProxy()で代理オブジェクトを取得します。
public class ProductService implements ProductOperations {
@Override
public void addProduct() {
System.out.println("商品追加処理");
ProductOperations proxy = (ProductOperations) AopContext.currentProxy();
proxy.getProductDetails();
}
@Override
public String getProductDetails() {
System.out.println("商品詳細取得");
return "product details";
}
}
XML設定ではexpose-proxy="true"を設定する必要があります。