横断的関心事の分離
アプリケーション開発において、ログ記録やトランザクション管理、セキュリティチェックなどの機能は、ビジネスロジックとは独立して複数の箇所に必要となります。これらを横断的関心事(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 クラス自体には
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;
}
}