Spring AOP(アスペクト指向プログラミング)

📢 この記事は gemini-2.5-flash によって翻訳されました

Aspect Oriented Programming(アスペクト指向プログラミング)は、特定メソッド向けのプログラミングだよ。

動的プロキシは、アスペクト指向プログラミングの最も主流な実装だね。そしてSpring AOPは、Springフレームワークの高度な技術なんだ。主に、Beanオブジェクトを管理する過程で、基盤となる動的プロキシ機構を通じて、特定のメソッドに対してプログラミングを行うことを目的としているよ。

メソッド実行時間の統計

依存関係をインポートするよ。

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

AOPプログラムを書いて、特定のメソッドに対してビジネス要件に応じてプログラミングしよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Slf4j
@Component
@Aspect // AOPクラス
public class TimeAspect {
    // ポイントカット式
    @Around("execution(* net.yexca.service.*.*(..))")
    public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long begin = System.currentTimeMillis();
        // 元のメソッドを呼び出す
        Object object = proceedingJoinPoint.proceed();
        long end = System.currentTimeMillis();
        log.info(proceedingJoinPoint.getSignature() + "メソッド実行時間:{}ms", end-begin);
        return object;
    }
}

AOPの応用シナリオとしては、操作ログの記録、権限制御、トランザクション管理なんかがあるよ。

コアコンセプト

ジョインポイント:JoinPoint、AOPで制御できるメソッドのこと(メソッド実行時の関連情報が暗に含まれる)。

アドバイス:Advice、繰り返されるロジック、つまり共通機能を指すよ(最終的には一つのメソッドとして現れる)。

ポイントカット:PointCut、ジョインポイントをマッチングさせる条件のこと。アドバイスは、このポイントカットのメソッドが実行されるときにだけ適用されるよ。

アスペクト:Aspect、アドバイスとポイントカットの対応関係を記述したもの(アドバイス+ポイントカット)。

ターゲットオブジェクト:Target、アドバイスが適用されるオブジェクトのこと。

上の例で言うと、書かれてないServiceの全メソッドがジョインポイントで、ポイントカット式で選択されたメソッドがポイントカットだね。AOPクラスのrecordTimeメソッドがアドバイスで、@Aroundアノテーションとアドバイスが一緒になってアスペクトになるんだ。そしてTimeAspectクラスはアスペクトクラスと呼ばれるよ。

アドバイス

アドバイスの種類

@Around:アラウンドアドバイス、このアノテーションが付いたアドバイスメソッドは、ターゲットメソッドの前と後の両方で実行されるよ。

@Before:ビフォーアドバイス、このアノテーションが付いたアドバイスメソッドは、ターゲットメソッドの前に実行されるんだ。

@After:アフターアドバイス、このアノテーションが付いたアドバイスメソッドは、ターゲットメソッドの後に実行されるよ。例外があってもなくても実行されるんだ。

@AfterReturning:アフターリターニングアドバイス、このアノテーションが付いたアドバイスメソッドは、ターゲットメソッドの後に実行されるよ。例外がある場合は実行されないんだ。

@AfterThrowing:アフタースローイングアドバイス、このアノテーションが付いたアドバイスメソッドは、ターゲットメソッドで例外が発生した後に実行されるよ。

 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
@Component
@Aspect
public class MyAspect {
    
    @Before("execution(* net.yexca.service.impl.*(..))")
    public void before(){
        System.out.println("Before");
    }
    
    @Around("execution(* net.yexca.service.impl.*(..))")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("Around before");
        Object result = proceedingJoinPoint.proceed();
        System.out.println("Around after");
        return result;
    }
    
    @After("execution(* net.yexca.service.impl.*(..))")
    public void after(){
        System.out.println("After");
    }
    
    @AfterReturning("execution(* net.yexca.service.impl.*(..))")
    public void afterRetruning(){
        System.out.println("AfterReturning");
    }
    
    @AfterThrowing("execution(* net.yexca.service.impl.*(..))")
    public void afterThrowing(){
        System.out.println("AfterThrowing");
    }
}

@Aroundアラウンドアドバイスは、元のメソッドを実行させるために自分でProceedingJoinPoint.proceed()を呼ばなきゃいけないんだ。他のアドバイスはターゲットメソッドの実行を考慮する必要はないよ。

@Aroundアラウンドアドバイスメソッドの戻り値は、元のメソッドの戻り値を受け取るためにObjectとして指定する必要があるんだ。


上記の5つのアノテーションのポイントカット式は全部同じだから、以下のように抽出できるよ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class MyAspect {
    
    @Pointcut("execution(* net.yexca.service.impl.*(..))")
    public void pt(){}

    @Before("pt()")
    public void before(){
        System.out.println("Before");
    }
}

pt()メソッドがpublicアクセス修飾子を持っていれば、他のクラスから参照できるよ。

アドバイスの実行順序

複数のアスペクトのポイントカットがターゲットメソッドにマッチした場合、ターゲットメソッドが実行されるとき、複数のアドバイスメソッドが全部実行されるよ。

1
2
3
4
5
6
net: 
  yexca: 
    aop: 
      - MyAspect1
      - MyAspect2
      - MyAspect3

3つのAOPクラスが全部同じメソッドを選んだとしよう。異なるアスペクトクラスの中では、デフォルトでアスペクトクラスの名前のアルファベット順に並ぶんだ。

  • ターゲットメソッドの前のBeforeアドバイス:アルファベット順で早い方が先に実行される。
  • ターゲットメソッドの後のAfterアドバイス:アルファベット順で遅い方が先に実行される。

3つのAOPクラスが全部@Before@Afterを持っていると仮定すると、実行順序はこうなるよ。

1
2
3
4
5
6
MyAspect1 before
MyAspect2 before
MyAspect3 before
MyAspect3 after
MyAspect2 after
MyAspect1 after

@Order(num)アノテーションをアスペクトクラスに付けて順序を制御できるよ。numが小さいほど先に実行されて、@Before@Afterの実行は上と同じだね。

ポイントカット式

ポイントカットメソッドを記述する式で、主にプロジェクトのどのメソッドにアドバイスを追加するかを決めるために使うんだ。

よくある形式は、メソッドのシグネチャに基づいてマッチングするexecution(...)と、アノテーションに基づいてマッチングするannotationだよ。

execution

主にメソッドの戻り値、パッケージ名、クラス名、メソッド名、メソッド引数などの情報に基づいてマッチングさせるんだ。構文はこれだよ。

1
execution(アクセス修飾子 戻り値 パッケージ名.クラス名.メソッド名(メソッド引数) throws 例外)

その中で、アクセス修飾子、パッケージ名.クラス名throws例外は省略できるけど、パッケージ名.クラス名は省略しない方がいいよ。

ワイルドカードを使ってポイントカットを記述することもできるんだ。

  • *:単一の独立した任意の記号で、任意の戻り値、パッケージ名、クラス名、メソッド名、任意の型の1つの引数をマッチングさせることができるし、パッケージ、クラス、メソッド名の一部をマッチングさせることもできるよ。
1
execution(* com.*.service.*.update*(*))
  • ..:複数の連続した任意の記号で、任意の階層のパッケージ、または任意の型、任意の数の引数をマッチングさせることができるよ。
1
execution(* com.yexca..service.*(..))

&&||!を使って、もっと複雑なポイントカット式を組み合わせることもできるよ。

書き方のヒント

  • 全てのビジネスメソッド名は、名前を付けるときにできるだけ規範的にすると、ポイントカット式が素早くマッチングできて便利だよ。
  • ポイントカットメソッドは通常、実装クラスではなくインターフェースに基づいて記述すると、拡張性が高まるんだ。
  • ビジネス要件を満たす前提で、ポイントカットのマッチング範囲はできるだけ狭くする方がいいよ。

@annotation

@annotationポイントカット式は、特定の注解でマークされたメソッドをマッチングさせるために使うんだ。使うには、まずカスタムアノテーションを定義する必要があるよ。

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
}

それから、メソッドにそのアノテーションを追加して、AOPクラスのメソッド上で使うんだ。

1
@Before("@annotation(net.yexca.aop.MyLog)")

ジョインポイント

Springでは、JoinPointを使ってジョインポイントが抽象化されてるんだ。これを使うと、メソッド実行時の関連情報を取得できるよ。

  • @Aroundアドバイスの場合、ジョインポイント情報の取得はProceedingJoinPointしか使えないんだ。
  • 他の4つのアドバイスの場合、ジョインポイント情報の取得はJoinPointしか使えないよ。これはProceedingJoinPointの親クラスだね。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    // ターゲットオブジェクトのクラス名を取得する
    String className = proceedingJoinPoint.getTarget().getClass().getName();
        
    // ターゲットメソッドのメソッド名を取得する
   String methodNAme = proceedingJoinPoint.getSignature().getName();
        
    // ターゲットメソッド実行時に渡された引数を取得する
    Object[] args = proceedingJoinPoint.getArgs();
    
    // 元のメソッドを呼び出す
    Object object = proceedingJoinPoint.proceed();

    return object;
}

Visits Since 2025-02-28

Hugo で構築されています。 | テーマ StackJimmy によって設計されています。