Spring AOPの実装アプローチ:アノテーション駆動からプログラムによるプロキシ生成まで

Spring AOPの基本概念と用語

SpringフレームワークにおけるAOP(Aspect-Oriented Programming)は、ビジネスロジックから横断的関心事を分離し、コードの保守性と再利用性を高めるための強力な機能です。実装に入る前に、AOPで頻出する専門用語の定義を整理します。

  • 関心事(Concerns):アプリケーションが解決しようとする特定の課題や興味の対象。
  • 横断的関心事(Cross-cutting Concerns):複数のモジュールやレイヤーにまたがって共通する関心事。ロギング、セキュリティ、トランザクション管理などが該当します。
  • アスペクト(Aspect):横断的関心事をモジュール化したもの。Springでは通常、アノテーションが付与されたクラスとして定義され、ポイントカットとアドバイスを含みます。
  • ジョインポイント(JoinPoint):プログラム実行中の特定の時点。メソッドの呼び出しや例外のスローなどが該当し、Spring AOPではメソッド実行が主なジョインポイントとなります。
  • ポイントカット(Pointcut):アドバイスを適用するジョインポイントを絞り込むための条件式。
  • アドバイス(Advice):特定のジョインポイントで実行される具体的な処理(コード)。事前(Before)、事後(After)、返戻後(AfterReturning)、例外発生時(AfterThrowing)、周囲(Around)などの種類があります。
  • ターゲットオブジェクト(Target):アドバイスが適用される対象のオブジェクト。
  • ウィービング(Weaving):アスペクトをターゲットオブジェクトに適用し、プロキシオブジェクトを生成するプロセス。

アノテーション駆動のアスペクト実装

Spring Boot環境では、@Aspect アノテーションを使用した宣言的なアプローチが標準的です。以下は、メソッドの実行時間を計測するためのカスタムアノテーションとアスペクトの実装例です。

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 実行時間計測の対象となるメソッドに付与するカスタムアノテーション
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackExecutionTime {
}

/**
 * 実行時間を計測するアスペクト定義
 */
@Aspect
@Component
public class ExecutionTimeMonitor {

    /**
     * @TrackExecutionTime が付与されたメソッドをインターセプトする
     *
     * @param pjp ジョインポイントのコンテキスト情報
     * @return ターゲットメソッドの戻り値
     * @throws Throwable ターゲットメソッドで発生した例外
     */
    @Around("@annotation(TrackExecutionTime)")
    public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
        long startTime = System.currentTimeMillis();
        try {
            // ターゲットメソッドの実行
            return pjp.proceed();
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            System.out.printf("[Monitor] Method '%s' executed in %d ms%n", 
                              pjp.getSignature().getName(), duration);
        }
    }
}

上記の@Around以外にも、Spring AOPでは以下のアノテーションが頻繁に使用されます。

  • @Before:メソッド実行前にアドバイスを実行。
  • @After:メソッドの成否に関わらず、実行後にアドバイスを実行。
  • @AfterReturning:メソッドが正常に返却された後にアドバイスを実行。
  • @AfterThrowing:メソッドが例外をスローした後にアドバイスを実行。

プログラムによるプロキシ生成と低レベルAPI

SpringのDIコンテナ外でAOPを適用したい場合や、プロキシ生成プロセスをより細かに制御したい場合は、ProxyFactory とSpring AOPの低レベルインターフェース(org.springframework.aop および org.aopalliance パッケージ)を直接使用します。このアプローチは、フレームワークの内部動作を理解するためにも有益です。

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.ThrowsAdvice;
import org.springframework.aop.framework.ProxyFactory;

import java.lang.reflect.Method;

/**
 * 事前アドバイス:メソッド実行前に監査ログを出力
 */
class AuditBeforeAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) {
        System.out.println("[Audit] Preparing to execute: " + method.getName());
    }
}

/**
 * 返戻後アドバイス:メソッド正常終了時にメトリクスを記録
 */
class MetricsAfterAdvice implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) {
        System.out.println("[Metrics] Successfully completed: " + method.getName());
    }
}

/**
 * 例外発生時アドバイス:例外発生時のフォールバック処理
 */
class FallbackThrowsAdvice implements ThrowsAdvice {
    public void afterThrowing(Method method, Object[] args, Object target, Exception ex) {
        System.out.println("[Fallback] Exception caught in " + method.getName() + ": " + ex.getMessage());
    }
}

/**
 * 周囲アドバイス:認可チェックと実行制御
 */
class AuthorizationInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("[Auth] Checking permissions...");
        // proceed()を呼び出さない場合、ターゲットメソッドは実行されない
        Object result = invocation.proceed();
        System.out.println("[Auth] Access granted and finalized.");
        return result;
    }
}

/**
 * ターゲットインターフェース
 */
interface DataRepository {
    String fetchData();
}

/**
 * ターゲット実装クラス
 */
class UserDataRepository implements DataRepository {
    @Override
    public String fetchData() {
        System.out.println("Fetching data from database...");
        // デモンストレーションのために意図的に例外をスロー
        throw new RuntimeException("Connection timeout");
    }
}

/**
 * プログラムによるAOP適用の実行クラス
 */
public class ProgrammaticAopDemo {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory();
        
        // 各種アドバイスの登録
        proxyFactory.addAdvice(new AuthorizationInterceptor());
        proxyFactory.addAdvice(new AuditBeforeAdvice());
        proxyFactory.addAdvice(new MetricsAfterAdvice());
        proxyFactory.addAdvice(new FallbackThrowsAdvice());
        
        // ターゲットオブジェクトの設定とCGLIBプロキシの強制使用
        proxyFactory.setTarget(new UserDataRepository());
        proxyFactory.setProxyTargetClass(true);

        // プロキシオブジェクトの生成
        DataRepository proxy = (DataRepository) proxyFactory.getProxy();
        
        try {
            proxy.fetchData();
        } catch (Exception e) {
            System.out.println("Handled exception at client level.");
        }
    }
}

タグ: Spring AOP AspectJ ProxyFactory Java

9月17日 03:21 投稿