Spring BootでREST APIを開発していると、ロギング、トランザクション管理、パフォーマンス計測など、複数のメソッドに共通する処理を効率的に実装したくなる場面があります。このような**横断的関心事(Cross-cutting Concerns)**を分離・再利用可能にする技術がAOP(Aspect-Oriented Programming)です。

この記事では、Spring AOPの基本概念から、@Aspect@Pointcut@Aroundを使った実装パターン、execution式やアノテーションベースのPointcut指定まで、REST API開発で実践的に使えるAOPの知識を解説します。

実行環境と前提条件

本記事のサンプルコードは以下の環境で動作確認しています。

項目 バージョン
Java 21
Spring Boot 3.4.1
Gradle 8.x

前提条件として、Spring Boot Webプロジェクトが作成済みであることを想定しています。spring-boot-starter-aop依存関係が必要です。

1
2
3
4
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

期待される学習成果

本記事を読み終えると、以下のことができるようになります。

  • Spring AOPの基本概念(Aspect、Pointcut、Advice、JoinPoint)を理解できる
  • @Before@After@Aroundアドバイスの使い分けを判断できる
  • execution式によるメソッド指定とアノテーションベースのPointcutを記述できる
  • JoinPointProceedingJoinPointからメソッド情報を取得できる
  • ロギングやパフォーマンス計測などの実践的なAspectを実装できる

Spring AOPとは

**AOP(Aspect-Oriented Programming)**は、オブジェクト指向プログラミング(OOP)を補完するプログラミングパラダイムです。OOPでは縦方向のモジュール分割(クラス階層)が得意ですが、横断的関心事(複数のクラスに共通する処理)を扱うのは苦手です。AOPはこの横断的関心事を「Aspect」という単位で分離し、コードの重複を排除して保守性を高めます。

横断的関心事の例

REST API開発でよく見られる横断的関心事には以下のようなものがあります。

  • ロギング: メソッドの呼び出しログ、引数・戻り値の記録
  • パフォーマンス計測: メソッドの実行時間測定
  • トランザクション管理: データベース操作のトランザクション制御
  • セキュリティ: 認証・認可のチェック
  • キャッシング: メソッド結果のキャッシュ
  • 例外ハンドリング: 共通の例外処理ロジック

Spring AOPの仕組み

Spring AOPはプロキシベースのAOP実装を採用しています。対象となるBeanに対してプロキシオブジェクトを作成し、メソッド呼び出し時にAdvice(共通処理)を実行します。

sequenceDiagram
    participant Client as クライアント
    participant Proxy as AOPプロキシ
    participant Aspect as Aspect
    participant Target as ターゲットBean

    Client->>Proxy: メソッド呼び出し
    Proxy->>Aspect: Before Advice実行
    Aspect-->>Proxy: 前処理完了
    Proxy->>Target: 実際のメソッド実行
    Target-->>Proxy: 戻り値
    Proxy->>Aspect: After Advice実行
    Aspect-->>Proxy: 後処理完了
    Proxy-->>Client: 戻り値

AOPの基本用語

Spring AOPを理解するために、まず基本用語を押さえておきましょう。

用語 説明
Aspect 横断的関心事をモジュール化したクラス。@Aspectで定義
JoinPoint Adviceが適用される可能性のあるポイント。Spring AOPではメソッド実行時のみ
Pointcut Adviceを適用するJoinPointを選択する式。どのメソッドに適用するかを定義
Advice 特定のJoinPointで実行される処理。Before、After、Aroundなど種類がある
Target Adviceが適用される対象のオブジェクト
Weaving AspectをTargetに適用してプロキシを作成するプロセス

Adviceの種類と使い分け

Spring AOPでは5種類のAdviceが提供されています。それぞれの特徴と使い分けを解説します。

@Before - メソッド実行前の処理

対象メソッドの実行前に処理を行います。メソッドの実行を止めることはできません(例外をスローする場合を除く)。

1
2
3
4
5
@Before("execution(* com.example.api.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
    String methodName = joinPoint.getSignature().getName();
    log.info("メソッド開始: {}", methodName);
}

使用例: ログ出力、引数のバリデーション、セキュリティチェック

@AfterReturning - 正常終了後の処理

対象メソッドが例外をスローせずに正常終了した後に実行されます。戻り値にアクセスできます。

1
2
3
4
5
6
7
8
@AfterReturning(
    pointcut = "execution(* com.example.api.service.*.*(..))",
    returning = "result"
)
public void logAfterReturning(JoinPoint joinPoint, Object result) {
    log.info("メソッド正常終了: {} - 戻り値: {}", 
             joinPoint.getSignature().getName(), result);
}

使用例: 戻り値のログ出力、結果の加工、統計情報の収集

@AfterThrowing - 例外発生後の処理

対象メソッドが例外をスローした場合に実行されます。発生した例外にアクセスできます。

1
2
3
4
5
6
7
8
@AfterThrowing(
    pointcut = "execution(* com.example.api.service.*.*(..))",
    throwing = "ex"
)
public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
    log.error("メソッドで例外発生: {} - 例外: {}", 
              joinPoint.getSignature().getName(), ex.getMessage());
}

使用例: 例外ログの出力、エラー通知、ロールバック処理

@After - メソッド実行後の処理(finally相当)

対象メソッドの実行後、正常終了・例外発生に関わらず必ず実行されます。try-finallyのfinallyブロックに相当します。

1
2
3
4
@After("execution(* com.example.api.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
    log.info("メソッド終了: {}", joinPoint.getSignature().getName());
}

使用例: リソースのクリーンアップ、必ず実行したい後処理

@Around - メソッド実行前後の処理

最も強力なAdviceで、メソッドの実行前後両方で処理を行えます。メソッドの実行自体を制御でき、戻り値の変更も可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Around("execution(* com.example.api.service.*.*(..))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();
    
    try {
        Object result = joinPoint.proceed();
        return result;
    } finally {
        long executionTime = System.currentTimeMillis() - start;
        log.info("メソッド {} の実行時間: {}ms", 
                 joinPoint.getSignature().getName(), executionTime);
    }
}

使用例: パフォーマンス計測、トランザクション管理、キャッシング、リトライ処理

Adviceの選択指針

flowchart TD
    A[Adviceの選択] --> B{メソッド実行の<br/>制御が必要?}
    B -->|Yes| C[@Around]
    B -->|No| D{処理のタイミングは?}
    D -->|実行前| E[@Before]
    D -->|実行後| F{正常/例外の<br/>区別が必要?}
    F -->|正常時のみ| G[@AfterReturning]
    F -->|例外時のみ| H[@AfterThrowing]
    F -->|両方| I[@After]

Pointcut式の記述方法

Pointcutはどのメソッドにadviceを適用するかを指定する式です。Spring AOPでは主にexecution式とアノテーションベースの指定を使用します。

execution式の構文

execution式は最もよく使用されるPointcut指定方法です。

execution(修飾子パターン? 戻り値パターン クラスパターン.メソッドパターン(引数パターン) throws例外パターン?)

各部分の意味は以下の通りです。

部分 必須 説明
修飾子パターン 任意 public、privateなど public
戻り値パターン 必須 メソッドの戻り値の型 *voidString
クラスパターン 任意 完全修飾クラス名 com.example..*
メソッドパターン 必須 メソッド名 get**
引数パターン 必須 メソッドの引数 (..)(String, *)
throws例外パターン 任意 スローする例外 throws IOException

execution式のワイルドカード

ワイルドカード 意味 使用箇所
* 任意の1要素にマッチ 型名、メソッド名、パッケージ名
.. 任意の0個以上の要素にマッチ パッケージ(サブパッケージ含む)、引数
+ 指定クラスとそのサブクラス クラス名の後

execution式の実例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// すべてのpublicメソッド
@Pointcut("execution(public * *(..))")
public void allPublicMethods() {}

// com.example.api.serviceパッケージ内のすべてのメソッド
@Pointcut("execution(* com.example.api.service.*.*(..))")
public void serviceLayer() {}

// com.example.api配下のすべてのサブパッケージを含むメソッド
@Pointcut("execution(* com.example.api..*.*(..))")
public void allApiMethods() {}

// Serviceで終わるクラスのすべてのメソッド
@Pointcut("execution(* com.example.api..*Service.*(..))")
public void allServiceMethods() {}

// findで始まるメソッド
@Pointcut("execution(* com.example.api..*.find*(..))")
public void findMethods() {}

// 引数がStringのメソッド
@Pointcut("execution(* com.example.api..*.*(.., String, ..))")
public void methodsWithStringArg() {}

// Listを返すメソッド
@Pointcut("execution(java.util.List com.example.api..*.*(..))")
public void methodsReturningList() {}

アノテーションベースのPointcut

特定のアノテーションが付与されたメソッドやクラスを対象にできます。これは特に便利で、ビジネスロジックに影響を与えずにAspectの適用範囲を制御できます。

@annotationによるメソッド指定

1
2
3
// @Loggableアノテーションが付いたメソッドを対象
@Pointcut("@annotation(com.example.api.annotation.Loggable)")
public void loggableMethods() {}

@withinによるクラス指定

1
2
3
// @RestControllerが付いたクラス内のすべてのメソッドを対象
@Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
public void restControllerMethods() {}

@targetによる実行時の型指定

1
2
3
// @Serviceが付いたクラスのメソッドを対象(プロキシではなく実際の型で判定)
@Pointcut("@target(org.springframework.stereotype.Service)")
public void serviceBeanMethods() {}

Pointcutの組み合わせ

複数のPointcutを論理演算子で組み合わせることができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Aspect
@Component
public class CombinedAspect {
    
    @Pointcut("execution(* com.example.api.service.*.*(..))")
    public void serviceLayer() {}
    
    @Pointcut("execution(* com.example.api.repository.*.*(..))")
    public void repositoryLayer() {}
    
    // AND条件: 両方の条件を満たす
    @Pointcut("serviceLayer() && args(id,..)")
    public void serviceMethodsWithIdArg() {}
    
    // OR条件: いずれかの条件を満たす
    @Pointcut("serviceLayer() || repositoryLayer()")
    public void dataAccessLayer() {}
    
    // NOT条件: 条件を満たさない
    @Pointcut("serviceLayer() && !execution(* *.get*(..))")
    public void serviceMethodsExceptGetters() {}
}

JoinPointからメソッド情報を取得する

Advice内ではJoinPointオブジェクトを通じて、実行されるメソッドの詳細情報にアクセスできます。

JoinPointで取得できる情報

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Before("execution(* com.example.api.service.*.*(..))")
public void logMethodDetails(JoinPoint joinPoint) {
    // メソッドシグネチャの取得
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    
    // メソッド名
    String methodName = signature.getName();
    
    // 戻り値の型
    Class<?> returnType = signature.getReturnType();
    
    // 宣言クラス
    Class<?> declaringClass = signature.getDeclaringType();
    
    // パラメータ名(-parametersオプションでコンパイル時)
    String[] parameterNames = signature.getParameterNames();
    
    // パラメータの型
    Class<?>[] parameterTypes = signature.getParameterTypes();
    
    // 実際の引数値
    Object[] args = joinPoint.getArgs();
    
    // 対象オブジェクト(プロキシではない実際のオブジェクト)
    Object target = joinPoint.getTarget();
    
    // プロキシオブジェクト
    Object proxy = joinPoint.getThis();
    
    log.info("クラス: {}, メソッド: {}, 引数: {}", 
             declaringClass.getSimpleName(), methodName, Arrays.toString(args));
}

Methodオブジェクトへのアクセス

リフレクションAPIのMethodオブジェクトを取得することで、アノテーション情報などにもアクセスできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Before("execution(* com.example.api.service.*.*(..))")
public void inspectMethodAnnotations(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();
    
    // メソッドに付与されたアノテーションを取得
    if (method.isAnnotationPresent(Transactional.class)) {
        Transactional transactional = method.getAnnotation(Transactional.class);
        log.info("トランザクション設定: readOnly={}", transactional.readOnly());
    }
}

ProceedingJoinPointの使用(@Around専用)

@AroundアドバイスではProceedingJoinPointを使用します。これはJoinPointを継承しており、proceed()メソッドでターゲットメソッドを実行できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Around("execution(* com.example.api.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    // 元の引数を取得
    Object[] originalArgs = joinPoint.getArgs();
    
    // 引数を変更することも可能
    Object[] modifiedArgs = modifyArgs(originalArgs);
    
    // 変更した引数でメソッドを実行
    Object result = joinPoint.proceed(modifiedArgs);
    
    // 戻り値を変更することも可能
    return modifyResult(result);
}

実践的なAspect実装例

ロギングAspect

REST APIのリクエスト・レスポンスをロギングするAspectの実装例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.example.api.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Aspect
@Component
public class LoggingAspect {
    
    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
    
    @Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
    public void restControllerMethods() {}
    
    @Pointcut("execution(* com.example.api.service.*.*(..))")
    public void serviceLayerMethods() {}
    
    @Around("restControllerMethods() || serviceLayerMethods()")
    public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String className = signature.getDeclaringType().getSimpleName();
        String methodName = signature.getName();
        Object[] args = joinPoint.getArgs();
        
        log.info("[START] {}.{}() - 引数: {}", className, methodName, formatArgs(args));
        
        long startTime = System.currentTimeMillis();
        
        try {
            Object result = joinPoint.proceed();
            long executionTime = System.currentTimeMillis() - startTime;
            
            log.info("[END] {}.{}() - 実行時間: {}ms - 戻り値: {}", 
                     className, methodName, executionTime, formatResult(result));
            
            return result;
        } catch (Exception e) {
            long executionTime = System.currentTimeMillis() - startTime;
            
            log.error("[ERROR] {}.{}() - 実行時間: {}ms - 例外: {}", 
                      className, methodName, executionTime, e.getMessage());
            
            throw e;
        }
    }
    
    private String formatArgs(Object[] args) {
        if (args == null || args.length == 0) {
            return "なし";
        }
        return Arrays.stream(args)
                .map(arg -> arg == null ? "null" : arg.toString())
                .reduce((a, b) -> a + ", " + b)
                .orElse("なし");
    }
    
    private String formatResult(Object result) {
        if (result == null) {
            return "null";
        }
        String str = result.toString();
        // 長すぎる場合は省略
        return str.length() > 100 ? str.substring(0, 100) + "..." : str;
    }
}

カスタムアノテーションによるパフォーマンス計測

特定のメソッドのみパフォーマンスを計測するカスタムアノテーションを作成します。

まずアノテーションを定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.example.api.annotation;

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 MeasurePerformance {
    /**
     * 警告を出す閾値(ミリ秒)
     */
    long warnThresholdMs() default 1000;
}

次にAspectを実装します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.example.api.aspect;

import com.example.api.annotation.MeasurePerformance;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
public class PerformanceAspect {
    
    private static final Logger log = LoggerFactory.getLogger(PerformanceAspect.class);
    
    @Around("@annotation(com.example.api.annotation.MeasurePerformance)")
    public Object measurePerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        MeasurePerformance annotation = method.getAnnotation(MeasurePerformance.class);
        
        String className = signature.getDeclaringType().getSimpleName();
        String methodName = signature.getName();
        
        long startTime = System.nanoTime();
        
        try {
            return joinPoint.proceed();
        } finally {
            long executionTimeMs = (System.nanoTime() - startTime) / 1_000_000;
            
            if (executionTimeMs >= annotation.warnThresholdMs()) {
                log.warn("[SLOW] {}.{}() - 実行時間: {}ms (閾値: {}ms)", 
                         className, methodName, executionTimeMs, annotation.warnThresholdMs());
            } else {
                log.debug("[PERF] {}.{}() - 実行時間: {}ms", 
                          className, methodName, executionTimeMs);
            }
        }
    }
}

使用例は以下の通りです。

1
2
3
4
5
6
7
8
@Service
public class ProductService {
    
    @MeasurePerformance(warnThresholdMs = 500)
    public List<Product> searchProducts(String keyword) {
        // 検索処理
    }
}

リトライAspect

一時的なエラーに対してリトライを行うAspectの実装例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.api.annotation;

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 Retryable {
    /**
     * 最大リトライ回数
     */
    int maxAttempts() default 3;
    
    /**
     * リトライ間隔(ミリ秒)
     */
    long delayMs() default 1000;
    
    /**
     * リトライ対象の例外クラス
     */
    Class<? extends Throwable>[] retryOn() default {Exception.class};
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.example.api.aspect;

import com.example.api.annotation.Retryable;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

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

@Aspect
@Component
public class RetryAspect {
    
    private static final Logger log = LoggerFactory.getLogger(RetryAspect.class);
    
    @Around("@annotation(com.example.api.annotation.Retryable)")
    public Object retry(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Retryable retryable = method.getAnnotation(Retryable.class);
        
        String methodName = signature.getDeclaringType().getSimpleName() 
                          + "." + signature.getName();
        
        int attempts = 0;
        Throwable lastException = null;
        
        while (attempts < retryable.maxAttempts()) {
            attempts++;
            try {
                return joinPoint.proceed();
            } catch (Throwable e) {
                lastException = e;
                
                if (!shouldRetry(e, retryable.retryOn())) {
                    throw e;
                }
                
                if (attempts < retryable.maxAttempts()) {
                    log.warn("[RETRY] {} - 試行 {}/{} 失敗: {} - {}ms後にリトライ", 
                             methodName, attempts, retryable.maxAttempts(), 
                             e.getMessage(), retryable.delayMs());
                    
                    Thread.sleep(retryable.delayMs());
                }
            }
        }
        
        log.error("[RETRY EXHAUSTED] {} - 全{}回の試行が失敗", methodName, attempts);
        throw lastException;
    }
    
    private boolean shouldRetry(Throwable e, Class<? extends Throwable>[] retryOn) {
        return Arrays.stream(retryOn)
                .anyMatch(exClass -> exClass.isInstance(e));
    }
}

Aspectの実行順序

複数のAspectが同じJoinPointに適用される場合、@OrderアノテーションまたはOrderedインターフェースで実行順序を制御できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Aspect
@Component
@Order(1)  // 数値が小さいほど先に実行(外側)
public class SecurityAspect {
    // セキュリティチェックを最初に実行
}

@Aspect
@Component
@Order(2)
public class LoggingAspect {
    // ログ出力
}

@Aspect
@Component
@Order(3)
public class PerformanceAspect {
    // パフォーマンス計測を最後に実行(最も内側)
}

実行順序のイメージは以下の通りです。

flowchart LR
    subgraph Order1[SecurityAspect - Order:1]
        subgraph Order2[LoggingAspect - Order:2]
            subgraph Order3[PerformanceAspect - Order:3]
                Target[ターゲットメソッド]
            end
        end
    end

@Aroundアドバイスの場合、Orderが小さいAspectのproceed()前処理が先に実行され、後処理は逆順で実行されます。

Spring AOPの注意点と制約

自己呼び出しの問題

Spring AOPはプロキシベースで動作するため、同一クラス内でのメソッド呼び出し(自己呼び出し)ではAspectが適用されません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Service
public class UserService {
    
    public void processUser(Long id) {
        // この内部呼び出しではAOPが適用されない
        User user = findById(id);
        // ...
    }
    
    @Loggable  // このアノテーションは内部呼び出しでは無効
    public User findById(Long id) {
        return userRepository.findById(id).orElseThrow();
    }
}

対処法としては以下の方法があります。

  1. メソッドを別クラスに分離する
  2. 自己注入を使用する(非推奨)
  3. AopContext.currentProxy()を使用する
1
2
3
4
5
6
7
8
9
@Service
public class UserService {
    
    public void processUser(Long id) {
        // プロキシ経由で呼び出し
        UserService proxy = (UserService) AopContext.currentProxy();
        User user = proxy.findById(id);
    }
}

AopContextを使用する場合は、@EnableAspectJAutoProxy(exposeProxy = true)の設定が必要です。

finalメソッドとクラスの制限

CGLIBプロキシでは、finalメソッドやfinalクラスにはAspectを適用できません。

プライベートメソッドの制限

Spring AOPはpublicメソッドおよびprotectedメソッドにのみ適用できます。privateメソッドへの適用はできません。

パフォーマンスへの影響

AOPはプロキシを介してメソッドを呼び出すため、わずかなオーバーヘッドが発生します。パフォーマンスが重要な場合は、Pointcutを適切に絞り込み、不要なメソッドにAspectが適用されないようにしましょう。

まとめ

Spring AOPを活用することで、ロギング、パフォーマンス計測、リトライ処理などの横断的関心事をビジネスロジックから分離し、コードの可読性と保守性を向上させることができます。

本記事で解説した内容のポイントは以下の通りです。

  • Aspectは横断的関心事をモジュール化したクラス
  • PointcutでAdviceを適用するメソッドを指定
  • Adviceは5種類あり、用途に応じて使い分ける
    • @Before: 前処理
    • @AfterReturning: 正常終了後の処理
    • @AfterThrowing: 例外発生後の処理
    • @After: 終了後の処理(finally相当)
    • @Around: 前後の処理とメソッド実行の制御
  • execution式でメソッドのシグネチャを指定
  • アノテーションベースのPointcutで柔軟な適用範囲制御
  • JoinPointからメソッドの詳細情報を取得
  • 自己呼び出しやfinalメソッドには適用できない制約がある

AOPを適切に活用して、クリーンで保守しやすいREST APIを構築しましょう。

参考リンク