Spring Framework における AOP とプロキシパターンの実装

横断的関心事の分離

アプリケーション開発において、ログ記録やトランザクション管理、セキュリティチェックなどの機能は、ビジネスロジックとは独立して複数の箇所に必要となります。これらを横断的関心事(Cross-cutting Concerns)と呼びます。これらのコードを各メソッドに直接記述すると、コードの重複が発生し、保守性が低下します。

例えば、算術演算を行うインターフェースとその実装クラスを用意します。

package com.example.core;

public interface MathOperation {
    int add(int x, int y);
    int subtract(int x, int y);
    int multiply(int x, int y);
    int divide(int x, int y);
}

基本的な実装クラスは以下のようになります。

package com.example.core;

public class MathOperationImpl implements MathOperation {
    @Override
    public int add(int x, int y) {
        int outcome = x + y;
        System.out.println("内部計算結果:" + outcome);
        return outcome;
    }

    @Override
    public int subtract(int x, int y) {
        int outcome = x - y;
        System.out.println("内部計算結果:" + outcome);
        return outcome;
    }

    @Override
    public int multiply(int x, int y) {
        int outcome = x * y;
        System.out.println("内部計算結果:" + outcome);
        return outcome;
    }

    @Override
    public int divide(int x, int y) {
        int outcome = x / y;
        System.out.println("内部計算結果:" + outcome);
        return outcome;
    }
}

ここにログ機能を追加する場合、各メソッドの前後に出力処理を挿入することになりますが、これはコードの重複を招きます。

プロキシパターンの活用

静的プロキシ

プロキシパターンを用いることで、対象オブジェクトへのアクセスを仲介させることができます。静的プロキシでは、代理クラスを事前に作成します。

package com.example.core;

public class MathOperationProxy implements MathOperation {
    private final MathOperation target;

    public MathOperationProxy(MathOperation target) {
        this.target = target;
    }

    @Override
    public int add(int x, int y) {
        System.out.println("[ログ] 加算開始:引数=" + x + "," + y);
        int outcome = target.add(x, y);
        System.out.println("[ログ] 加算終了:結果=" + outcome);
        return outcome;
    }

    // 他のメソッドも同様に実装が必要だが、重複コードが発生する
    @Override
    public int subtract(int x, int y) { return 0; }
    @Override
    public int multiply(int x, int y) { return 0; }
    @Override
    public int divide(int x, int y) { return 0; }
}

静的プロキシは型安全ですが、インターフェースのメソッドが増えるたびに代理クラスも修正する必要があり、柔軟性に欠けます。

動的プロキシ

JDK 動的プロキシを使用すると、実行時に代理オブジェクトを生成できます。これにより、汎用的な処理ロジックを一元管理できます。

package com.example.core;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

public class ProxyUtil {
    private final Object target;

    public ProxyUtil(Object target) {
        this.target = target;
    }

    public Object createProxy() {
        ClassLoader loader = target.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();

        InvocationHandler handler = (proxy, method, args) -> {
            System.out.println("[動的プロキシ] メソッド:" + method.getName() + ", 引数:" + Arrays.toString(args));
            Object result = method.invoke(target, args);
            System.out.println("[動的プロキシ] メソッド:" + method.getName() + ", 結果:" + result);
            return result;
        };

        return Proxy.newProxyInstance(loader, interfaces, handler);
    }
}

利用側は以下の様にプロキシオブジェクトを取得して使用します。

package com.example.core;

public class Client {
    public static void main(String[] args) {
        ProxyUtil util = new ProxyUtil(new MathOperationImpl());
        MathOperation proxy = (MathOperation) util.createProxy();
        proxy.multiply(3, 4);
    }
}

AOP の基本概念

Spring AOP は、動的プロキシをベースにした AOP 実装です。主な用語は以下の通りです。

  • 横断的関心事: ログ、セキュリティなど、複数のモジュールに跨る機能。
  • 通知(Advice): 特定の結合点で実行される処理。前置、後置、返回、異常、环绕通知などがあります。
  • アスペクト(Aspect): 通知とポイントカットをまとめたクラス。
  • ターゲット(Target): 通知を適用される対象オブジェクト。
  • ポイントカット(Pointcut): 通知を適用する結合点を特定する式。
  • 結合点(Joinpoint): 処理の実行中に挿入可能な点(メソッド呼び出しなど)。

ポイントカット式の構文

AspectJ の式を用いて対象を指定します。

  • *: 戻り値や修饰符を任意に匹配。
  • ..: パッケージの階層や引数リストを任意に匹配。
  • 例:execution(public int com.example.core.*Service.*(..))

アノテーションによる AOP 設定

Spring ではアノテーションを用いて簡潔に AOP を設定できます。まず、対象となるビーンを定義します。

package com.example.annotated;

import org.springframework.stereotype.Component;

@Component
public class MathOperationImpl implements MathOperation {
    @Override
    public int add(int x, int y) {
        int outcome = x + y;
        System.out.println("計算結果:" + outcome);
        return outcome;
    }
    // 他のメソッドも同様
    @Override public int subtract(int x, int y) { return x - y; }
    @Override public int multiply(int x, int y) { return x * y; }
    @Override public int divide(int x, int y) { return x / y; }
}

次に、 аспекト クラスを作成し、通知を定義します。

package com.example.annotated;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.annotated.MathOperationImpl.*(..))")
    public void logBefore(JoinPoint jp) {
        System.out.println("【前置】メソッド:" + jp.getSignature().getName() + ", 引数:" + Arrays.toString(jp.getArgs()));
    }

    @AfterReturning(value = "execution(* com.example.annotated.MathOperationImpl.*(..))", returning = "res")
    public void logAfterReturn(JoinPoint jp, Object res) {
        System.out.println("【返回】メソッド:" + jp.getSignature().getName() + ", 結果:" + res);
    }

    @AfterThrowing(value = "execution(* com.example.annotated.MathOperationImpl.*(..))", throwing = "ex")
    public void logException(JoinPoint jp, Throwable ex) {
        System.out.println("【異常】メソッド:" + jp.getSignature().getName() + ", エラー:" + ex.getMessage());
    }

    @After("execution(* com.example.annotated.MathOperationImpl.*(..))")
    public void logAfter(JoinPoint jp) {
        System.out.println("【後置】メソッド:" + jp.getSignature().getName());
    }

    @Around("execution(* com.example.annotated.MathOperationImpl.*(..))")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("【环绕】開始");
        Object result = pjp.proceed();
        System.out.println("【环绕】終了");
        return result;
    }
}

ポイントカット式を共通化するには、@Pointcut アノテーションを使用します。

@Pointcut("execution(* com.example.annotated.MathOperationImpl.*(..))")
public void commonPointcut() {}

// 通知定義で参照
@Before("commonPointcut()")
public void logBefore(JoinPoint jp) { ... }

XML による AOP 設定

アノテーションではなく XML 設定ファイルで AOP を構成することも可能です。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:component-scan base-package="com.example.xmlconfig"/>

    <aop:config>
        <aop:aspect ref="loggingAspect">
            <aop:pointcut id="pc" expression="execution(* com.example.xmlconfig.MathOperationImpl.*(..))"/>
            <aop:before method="logBefore" pointcut-ref="pc"/>
            <aop:after-returning method="logAfterReturn" pointcut-ref="pc" returning="res"/>
            <aop:after-throwing method="logException" pointcut-ref="pc" throwing="ex"/>
            <aop:after method="logAfter" pointcut-ref="pc"/>
            <aop:around method="logAround" pointcut-ref="pc"/>
        </aop:aspect>
    </aop:config>
</beans>

XML 設定の場合、aspect クラス自体には タグでメソッドを紐付けるため、@Aspect アノテーションは必須ではありませんが、コンポーネントスキャンの対象とするには@Component が必要です。

package com.example.xmlconfig;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
import java.util.Arrays;

@Component
public class LoggingAspect {
    public void logBefore(JoinPoint jp) {
        System.out.println("【XML 前置】" + jp.getSignature().getName());
    }
    public void logAfterReturn(JoinPoint jp, Object res) {
        System.out.println("【XML 返回】" + res);
    }
    public void logException(JoinPoint jp, Throwable ex) {
        System.out.println("【XML 異常】" + ex);
    }
    public void logAfter(JoinPoint jp) {
        System.out.println("【XML 後置】");
    }
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("【XML 环绕】開始");
        Object result = pjp.proceed();
        System.out.println("【XML 环绕】終了");
        return result;
    }
}

タグ: Spring AOP AspectJ ProxyPattern Java

7月6日 19:46 投稿