Spring AOP とは、ログ・計測・権限確認のように多くのメソッドに共通する処理を、各メソッドの中に書かずに1か所(アスペクト)へまとめ、指定したメソッドの前後に差し込む仕組みです。 @Aspect を付けたクラスに「どのメソッドに(ポイントカット)」「いつ何をするか(アドバイス)」を書くと、Spring が対象の Bean をプロキシ(代理オブジェクト)で包み、呼び出しの前後に処理を入れます。

この記事では、用途別にそのまま貼れる @Aspect のコード全文、ポイントカット指示子10種の一覧、execution 式の読み方、アスペクトが動かないときの原因表をまとめました。式がどのメソッドに当たるかは AspectJ のパーサーで、プロキシの挙動は Java 標準のプロキシで動かし、出力を載せています。

確認環境:Spring Boot 4.1.1/Spring Framework 7.0.9 の公式リファレンスで確認(確認日 2026年10月7日)。Java 単体で再現できる部分は Ubuntu 24.04・OpenJDK 21.0.10・AspectJ Weaver 1.9.5 で実行。

Spring AOP の早見表

やりたいこと アノテーション ポイント
メソッドの前に処理を入れる @Before 引数を見られる
正常終了の後に処理を入れる @AfterReturning returning で戻り値を受け取る
例外が出たときに処理を入れる @AfterThrowing throwing で例外を受け取る
結果にかかわらず後処理する @After finally に相当
前後をまとめて制御する @Around proceed() で本体を呼ぶ
対象を名前を付けて定義する @Pointcut 式を使い回せる
アスペクトの順番を決める @Order 値が小さいほど外側

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring AOPの使い方|@Aspectのコード全文と指示子一覧」

依存関係の書き方(Spring Boot 3.x と 4.x)

Spring Boot 4.0 の移行ガイドに、spring-boot-starter-aop は spring-boot-starter-aspectj に名前が変わったと書かれています。

Spring Boot 4.x

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aspectj'
}

Spring Boot 3.x

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

移行ガイドは、このスターターが必要かどうかの目安として「org.aspectj.lang.annotation パッケージのアノテーションを使っているか」を挙げています。@Aspect・@Around・@Before などはこのパッケージにあるので、自分でアスペクトを書く場合は必要です。Spring Framework リファレンスによると、@AspectJ 形式のサポートには org.aspectj:aspectjweaver(1.9 以降)がクラスパスに必要で、スターターはこれを取り込む役割を持ちます。

用途別の @Aspect コード全文4本

対象のパッケージは com.example.demo.service としています。自分のパッケージ名に置き換えて使ってください。どのクラスにも @Aspect と @Component の両方を付けています。Spring Framework リファレンスに「@Aspect だけではクラスパススキャンで自動検出されないため、別途 @Component を付ける」とあるためです。

1. メソッドの開始・終了ログ

package com.example.demo.aop;

import java.util.Arrays;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ServiceLoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(ServiceLoggingAspect.class);

    /** サービス層(service パッケージとその配下)のメソッド */
    @Pointcut("within(com.example.demo.service..*)")
    public void inServiceLayer() {
    }

    @Before("inServiceLayer()")
    public void logStart(JoinPoint jp) {
        log.info("開始 {} args={}",
                jp.getSignature().toShortString(), Arrays.toString(jp.getArgs()));
    }

    @AfterReturning(pointcut = "inServiceLayer()", returning = "result")
    public void logEnd(JoinPoint jp, Object result) {
        log.info("終了 {} result={}", jp.getSignature().toShortString(), result);
    }
}

JoinPoint はどのアドバイスでも第1引数に置けます。getArgs() で引数、getSignature() でメソッドの情報、getTarget() で本体のオブジェクト、getThis() でプロキシを取得できます。引数や戻り値に個人情報などが含まれる場合は、項目を選んで出力するようにします。

2. 処理時間の計測

package com.example.demo.aop;

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

@Aspect
@Component
public class TimingAspect {

    private static final Logger log = LoggerFactory.getLogger(TimingAspect.class);

    @Around("execution(* com.example.demo.service..*.*(..))")
    public Object measure(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        try {
            return pjp.proceed(); // 本体のメソッドを実行し、戻り値をそのまま返す
        } finally {
            long ms = (System.nanoTime() - start) / 1_000_000;
            log.info("{} {}ms", pjp.getSignature().toShortString(), ms);
        }
    }
}

@Around の決まりごとは3つです。戻り値の型は Object、第1引数は ProceedingJoinPoint、本体を動かすには proceed() を呼ぶ。 戻り値の型を void にすると、呼び出し元には常に null が返ります(Spring Framework リファレンス)。

3. 自作アノテーション @Loggable で対象を指定

package com.example.demo.aop;

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable {
    String value() default "";
}
package com.example.demo.aop;

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

@Aspect
@Component
public class LoggableAspect {

    private static final Logger log = LoggerFactory.getLogger(LoggableAspect.class);

    /** @Loggable が付いたメソッドだけを対象にし、アノテーションの値も受け取る */
    @Around("@annotation(loggable)")
    public Object logOperation(ProceedingJoinPoint pjp, Loggable loggable) throws Throwable {
        String label = loggable.value().isEmpty()
                ? pjp.getSignature().toShortString()
                : loggable.value();
        log.info("[{}] 開始", label);
        try {
            Object result = pjp.proceed();
            log.info("[{}] 完了", label);
            return result;
        } catch (Throwable ex) {
            log.warn("[{}] 中断: {}", label, ex.toString());
            throw ex; // 例外はそのまま呼び出し元へ
        }
    }
}

使う側は、対象のメソッドにアノテーションを付けるだけです。

package com.example.demo.service;

import org.springframework.stereotype.Service;

import com.example.demo.aop.Loggable;

@Service
public class OrderService {

    @Loggable("注文の登録")
    public void save(String item, int quantity) {
        // 登録処理
    }
}

@annotation(loggable) のように小文字の引数名を書くと、アドバイスの引数 Loggable loggable の型から対象のアノテーションが決まり、その値を受け取れます。パッケージ名で一括指定するより対象が目に見えるので、「どこに掛かっているか」を追いやすくなります。

4. 例外の記録

package com.example.demo.aop;

import java.util.Arrays;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ExceptionLoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(ExceptionLoggingAspect.class);

    @AfterThrowing(pointcut = "within(com.example.demo.service..*)", throwing = "ex")
    public void recordException(JoinPoint jp, Throwable ex) {
        log.error("例外 {} args={}",
                jp.getSignature().toShortString(), Arrays.toString(jp.getArgs()), ex);
    }
}

@AfterThrowing は記録するだけで、例外はそのまま呼び出し元へ伝わります。throwing に書いた引数の型(ここでは Throwable)で、対象にする例外の種類を絞れます。例外を別の例外に変えたり、代わりの値を返したりしたい場合は @Around を使います。

アドバイス5種の一覧

種類 動くタイミング 戻り値 例外 本体の実行を止める
@Before 本体の前 触れられない 触れられない 例外を投げれば止まる
@AfterReturning 正常に戻った後 受け取れる 触れられない —
@AfterThrowing 例外で抜けた後 触れられない 受け取れる —
@After 結果にかかわらず後 触れられない 触れられない —
@Around 前後を包む 受け取り、差し替えも可 try-catch で扱える proceed() を呼ばなければ止まる

@Around と @Before/@AfterReturning の選び方

Spring Framework リファレンスは「要件を満たすもののうち、いちばん力の弱いアドバイスを使う」ことを勧めています。

条件 選ぶもの
前に1回動けばよい(引数の記録、事前チェック) @Before
戻り値を見て記録するだけ @AfterReturning
例外を記録するだけ @AfterThrowing
成功でも例外でも後始末したい @After
前と後で同じ変数を使う(時間の計測など) @Around
戻り値や引数を差し替える、再試行する、実行を止める @Around

@Around は proceed() の呼び忘れや戻り値の返し忘れがあると本体が動かなくなるため、必要な場面に絞ると読みやすさも保てます。

同じアスペクト内での順番と @Order

同じ @Aspect クラスの中に複数の種類のアドバイスがあるとき、優先度は高い順に @Around、@Before、@After、@AfterReturning、@AfterThrowing です(Spring Framework リファレンス)。ただし @After は、同じアスペクトの @AfterReturning/@AfterThrowing の後に呼ばれます。

別々のアスペクトの順番は @Order で決めます。値が小さいほど優先度が高く、入るときは先に、出るときは後に動きます。

package com.example.demo.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(1) // TimingAspect を @Order(2) にすると、こちらが外側になる
public class OuterAspect {

    private static final Logger log = LoggerFactory.getLogger(OuterAspect.class);

    @Around("within(com.example.demo.service..*)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        log.info("外側:入る");
        try {
            return pjp.proceed();
        } finally {
            log.info("外側:出る");
        }
    }
}

@Order(1) と @Order(2) の2つのアスペクトがあると、「1 が入る → 2 が入る → 本体 → 2 が出る → 1 が出る」の順になります。同じアスペクトの中に同じ種類のアドバイスを2つ書いた場合の順番は決まっていないため、1つにまとめるか、アスペクトを分けて @Order を付けます。

ポイントカット指示子10種の一覧

Spring AOP が対応している指示子は次の10種です(Spring Framework 7.0.9 リファレンス)。bean だけは Spring 独自の拡張です。

指示子 何で絞るか 記入例
execution メソッドのシグネチャ(主役の指示子) execution(* com.example.demo.service.*.*(..))
within 型(クラス・パッケージ)の中 within(com.example.demo.service..*)
this プロキシが指定の型のインスタンス this(com.example.demo.service.OrderService)
target 本体のオブジェクトが指定の型のインスタンス target(com.example.demo.service.OrderService)
args 実行時の引数が指定の型のインスタンス args(java.io.Serializable)
@target 本体のクラスに指定のアノテーションがある @target(com.example.demo.aop.Audited)
@args 引数の実行時の型に指定のアノテーションがある @args(com.example.demo.aop.Classified)
@within 指定のアノテーションが付いた型の中 @within(com.example.demo.aop.Audited)
@annotation メソッドに指定のアノテーションがある @annotation(com.example.demo.aop.Loggable)
bean Bean 名(ワイルドカード可) bean(orderService)、bean(*Service)

補足です。

  • execution と args の違い:execution(* *(java.io.Serializable)) は宣言された引数の型で判定し、args(java.io.Serializable) は実行時に渡された引数の型で判定します。
  • this・target・args・@annotation は、型名の代わりにアドバイスの引数名を書くと、その値をアドバイスで受け取れます(前掲の @annotation(loggable))。
  • 組み合わせ:&&(かつ)、||(または)、!(否定)でつなげます。
  • 絞り込みの指示子を入れる:公式は、execution のような種類を決める指示子に加えて within のような範囲を決める指示子を入れると照合が速くなる、と案内しています。

名前を付けたポイントカットを1つのクラスにまとめておくと、複数のアスペクトから使い回せます。

package com.example.demo.aop;

import org.aspectj.lang.annotation.Pointcut;

public class CommonPointcuts {

    @Pointcut("within(com.example.demo.web..*)")
    public void inWebLayer() {
    }

    @Pointcut("within(com.example.demo.service..*)")
    public void inServiceLayer() {
    }

    @Pointcut("execution(public * *(..))")
    public void publicMethod() {
    }

    @Pointcut("publicMethod() && inServiceLayer()")
    public void serviceOperation() {
    }
}

使う側は完全修飾名で参照します。

package com.example.demo.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ServiceStartLogAspect {

    private static final Logger log = LoggerFactory.getLogger(ServiceStartLogAspect.class);

    @Before("com.example.demo.aop.CommonPointcuts.serviceOperation()")
    public void logStart(JoinPoint jp) {
        log.info("開始 {}", jp.getSignature().toShortString());
    }
}

execution 式の読み方

公式の書式は次のとおりです。? が付いた部分は省略できます。

execution(modifiers-pattern?
            ret-type-pattern
            declaring-type-pattern?
            name-pattern(param-pattern)
            throws-pattern?)

execution(public * com.example.demo.service.OrderService.find*(Long, ..)) を部位ごとに分けると、次のようになります。

部位 この例での値 省略 意味
修飾子 public できる public などの修飾子
戻り値の型 * できない * はどの型でも
宣言している型 com.example.demo.service.OrderService. できる 末尾の . でメソッド名につなぐ
メソッド名 find* できない * は名前の全部または一部
引数 (Long, ..) できない 下の表を参照
throws (なし) できる 投げる例外の型

記号の意味は次のとおりです。

書き方 意味
* 任意の1つ(型、名前の全部または一部)
..(パッケージ内) そのパッケージと配下のパッケージすべて
() 引数なし
(..) 引数が0個以上(何でもよい)
(*) 任意の型の引数が1個
(*, String) 引数が2個で、2個目が String

よく使う式と、実際に当たったメソッド

式がどのメソッドに当たるかを、AspectJ のパーサー(PointcutParser)で確かめました。対象は次の3クラスです。

// com/example/demo/aop/Audited.java
package com.example.demo.aop;

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Audited {
}
// com/example/demo/service/OrderService.java
package com.example.demo.service;

import java.util.List;

import com.example.demo.aop.Audited;
import com.example.demo.aop.Loggable;

@Audited
public class OrderService {

    public String findById(Long id) {
        return "order-" + id;
    }

    public List<String> findAll() {
        return List.of("order-1", "order-2");
    }

    @Loggable("注文の登録")
    public void save(String item, int quantity) {
    }

    public void setStatus(String status) {
    }

    protected void recalc() {
    }
}
// com/example/demo/service/sub/StockService.java
package com.example.demo.service.sub;

public class StockService {

    public int count(String item) {
        return 3;
    }
}
// com/example/demo/web/OrderController.java
package com.example.demo.web;

public class OrderController {

    public String show(Long id) {
        return "order";
    }
}

確認用のプログラムです(Loggable は前掲のものを使います)。

// check/PointcutCheck.java
package check;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import org.aspectj.weaver.tools.PointcutExpression;
import org.aspectj.weaver.tools.PointcutParser;
import org.aspectj.weaver.tools.ShadowMatch;

import com.example.demo.service.OrderService;
import com.example.demo.service.sub.StockService;
import com.example.demo.web.OrderController;

public class PointcutCheck {

    static final String[] EXPRESSIONS = {
        "execution(public * *(..))",
        "execution(* set*(..))",
        "execution(* com.example.demo.service.OrderService.*(..))",
        "execution(* com.example.demo.service.*.*(..))",
        "execution(* com.example.demo.service..*.*(..))",
        "execution(* com.example.demo..*Service.find*(..))",
        "execution(String com.example.demo..*.*(..))",
        "execution(java.util.List com.example.demo..*.*(..))",
        "execution(void com.example.demo..*.*(..))",
        "execution(* com.example.demo..*.*())",
        "execution(* com.example.demo..*.*(*))",
        "execution(* com.example.demo..*.*(Long))",
        "execution(* com.example.demo..*.*(String, ..))",
        "execution(* com.example.demo..*.*(*, int))",
        "within(com.example.demo.service.*)",
        "within(com.example.demo.service..*)",
        "within(com.example.demo.service..*) && !execution(* set*(..))",
        "within(com.example.demo.service..*) || within(com.example.demo.web..*)",
        "@annotation(com.example.demo.aop.Loggable)",
        "@within(com.example.demo.aop.Audited)",
    };

    public static void main(String[] args) {
        List<Method> methods = new ArrayList<>();
        for (Class<?> c : List.of(OrderService.class, StockService.class, OrderController.class)) {
            Method[] declared = c.getDeclaredMethods();
            Arrays.sort(declared, Comparator.comparing(Method::getName));
            methods.addAll(Arrays.asList(declared));
        }

        PointcutParser parser = PointcutParser
                .getPointcutParserSupportingAllPrimitivesAndUsingSpecifiedClassloaderForResolution(
                        PointcutCheck.class.getClassLoader());

        for (String expr : EXPRESSIONS) {
            PointcutExpression pointcut = parser.parsePointcutExpression(expr);
            List<String> matched = new ArrayList<>();
            for (Method m : methods) {
                ShadowMatch match = pointcut.matchesMethodExecution(m);
                if (match.alwaysMatches()) {
                    matched.add(label(m));
                } else if (match.maybeMatches()) {
                    matched.add(label(m) + "(実行時に判定)");
                }
            }
            System.out.println(expr);
            System.out.println("  -> " + (matched.isEmpty() ? "なし" : String.join(", ", matched)));
        }
    }

    static String label(Method m) {
        String mod = Modifier.isPublic(m.getModifiers()) ? "" : "[protected]";
        return m.getDeclaringClass().getSimpleName() + "." + m.getName() + mod;
    }
}

コンパイルと実行(AspectJ Weaver 1.9.5 の jar をクラスパスに指定):

javac -encoding UTF-8 -cp aspectjweaver.jar -d out $(find src -name '*.java')
java -cp out:aspectjweaver.jar check.PointcutCheck

実行結果(OpenJDK 21.0.10):

execution(public * *(..))
  -> OrderService.findAll, OrderService.findById, OrderService.save, OrderService.setStatus, StockService.count, OrderController.show
execution(* set*(..))
  -> OrderService.setStatus
execution(* com.example.demo.service.OrderService.*(..))
  -> OrderService.findAll, OrderService.findById, OrderService.recalc[protected], OrderService.save, OrderService.setStatus
execution(* com.example.demo.service.*.*(..))
  -> OrderService.findAll, OrderService.findById, OrderService.recalc[protected], OrderService.save, OrderService.setStatus
execution(* com.example.demo.service..*.*(..))
  -> OrderService.findAll, OrderService.findById, OrderService.recalc[protected], OrderService.save, OrderService.setStatus, StockService.count
execution(* com.example.demo..*Service.find*(..))
  -> OrderService.findAll, OrderService.findById
execution(String com.example.demo..*.*(..))
  -> OrderService.findById, OrderController.show
execution(java.util.List com.example.demo..*.*(..))
  -> OrderService.findAll
execution(void com.example.demo..*.*(..))
  -> OrderService.recalc[protected], OrderService.save, OrderService.setStatus
execution(* com.example.demo..*.*())
  -> OrderService.findAll, OrderService.recalc[protected]
execution(* com.example.demo..*.*(*))
  -> OrderService.findById, OrderService.setStatus, StockService.count, OrderController.show
execution(* com.example.demo..*.*(Long))
  -> OrderService.findById, OrderController.show
execution(* com.example.demo..*.*(String, ..))
  -> OrderService.save, OrderService.setStatus, StockService.count
execution(* com.example.demo..*.*(*, int))
  -> OrderService.save
within(com.example.demo.service.*)
  -> OrderService.findAll, OrderService.findById, OrderService.recalc[protected], OrderService.save, OrderService.setStatus
within(com.example.demo.service..*)
  -> OrderService.findAll, OrderService.findById, OrderService.recalc[protected], OrderService.save, OrderService.setStatus, StockService.count
within(com.example.demo.service..*) && !execution(* set*(..))
  -> OrderService.findAll, OrderService.findById, OrderService.recalc[protected], OrderService.save, StockService.count
within(com.example.demo.service..*) || within(com.example.demo.web..*)
  -> OrderService.findAll, OrderService.findById, OrderService.recalc[protected], OrderService.save, OrderService.setStatus, StockService.count, OrderController.show
@annotation(com.example.demo.aop.Loggable)
  -> OrderService.save
@within(com.example.demo.aop.Audited)
  -> OrderService.findAll, OrderService.findById, OrderService.recalc[protected], OrderService.save, OrderService.setStatus

読み取れることを表にまとめます。

式 意味
execution(public * *(..)) すべての public メソッド
execution(* set*(..)) 名前が set で始まるメソッド
execution(* com.example.demo.service.OrderService.*(..)) OrderService の全メソッド
execution(* com.example.demo.service.*.*(..)) service パッケージ直下のクラスの全メソッド
execution(* com.example.demo.service..*.*(..)) service とその配下のパッケージの全メソッド
execution(* com.example.demo..*Service.find*(..)) 名前が Service で終わるクラスの find で始まるメソッド
execution(String com.example.demo..*.*(..)) 戻り値が String のメソッド
execution(java.util.List com.example.demo..*.*(..)) 戻り値が List のメソッド
execution(void com.example.demo..*.*(..)) 戻り値が void のメソッド
execution(* com.example.demo..*.*()) 引数なしのメソッド
execution(* com.example.demo..*.*(*)) 引数が1個のメソッド
execution(* com.example.demo..*.*(Long)) 引数が Long 1個のメソッド
execution(* com.example.demo..*.*(String, ..)) 1個目の引数が String のメソッド
execution(* com.example.demo..*.*(*, int)) 引数が2個で2個目が int のメソッド
within(com.example.demo.service.*) service パッケージ直下のクラス内
within(com.example.demo.service..*) service とその配下のクラス内
within(...) && !execution(* set*(..)) サービス層のうち set で始まるもの以外
within(...) || within(...) サービス層または Web 層
@annotation(com.example.demo.aop.Loggable) @Loggable が付いたメソッド
@within(com.example.demo.aop.Audited) @Audited が付いたクラスのメソッド

出力から次のことがわかります。

  • service.* は直下だけ、service..* は配下も含む:StockService(service.sub パッケージ)は .. を使った式にだけ当たっています。
  • 修飾子を書かない式は protected のメソッドにも当たる:recalc は execution(public * *(..)) には当たらず、修飾子を省いた式には当たっています。式に当たっても、実際にアドバイスが動くかどうかは次の節のプロキシの制限で決まります。
  • java.lang 以外の型は完全修飾名で書く:String や Long はそのまま書けますが、List は java.util.List と書いています。同じパーサーに execution(List com.example.demo..*.*(..)) を渡すと、次のメッセージで受け付けられませんでした。
IllegalArgumentException: warning no match for this type name: List [Xlint:invalidAbsoluteTypeName]

アスペクトが動かない原因の早見表

原因 なぜ動かないか 直し方
同じクラス内からの呼び出し this 経由の呼び出しはプロキシを通らない 別の Bean に分ける
private メソッド 上書きできず、プロキシが割り込めない public にする
final のクラス・メソッド CGLIB はサブクラスを作って上書きする方式 final を外す
Bean でないオブジェクト new したものにはプロキシが作られない コンテナから注入する
ポイントカットの書き間違い パッケージ名や . と .. の違いで当たらない 式を見直す(前節の表)
@Component の付け忘れ @Aspect だけでは検出されない @Component を付ける
スターター名の違い 4.x では spring-boot-starter-aspectj 依存関係を書き換える
アスペクトに掛けようとした アスペクト自身は他のアスペクトの対象外 対象を通常の Bean にする

プロキシの方式について、Spring Framework リファレンス(7.0.9)の説明をまとめると次のとおりです。

項目 JDK 動的プロキシ CGLIB プロキシ
使われる条件 対象がインターフェースを1つ以上実装 対象がインターフェースを実装していない
作り方 インターフェースを実装した代理 対象クラスのサブクラスを実行時に生成
横取りできるメソッド インターフェースの public メソッド public と protected(必要に応じてパッケージ可視も)
final — クラスもメソッドも対象外
private 対象外 対象外

公式は「ポイントカットは基本的に public のシグネチャを前提に設計する」ことを勧めています。CGLIB を必ず使いたいときは @EnableAspectJAutoProxy(proxyTargetClass = true) を指定します。

Java 標準のプロキシで自己呼び出しを確かめる

「プロキシ経由の呼び出しだけが横取りされる」ことは、Spring を使わない Java だけで再現できます。java.lang.reflect.Proxy で、メソッドの前後に出力を入れる代理を作ります。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class ProxyDemo {

    interface OrderService {
        void placeOrder(String item);
        void validate(String item);
    }

    static class OrderServiceImpl implements OrderService {
        @Override
        public void placeOrder(String item) {
            System.out.println("    placeOrder 本体: " + item);
            this.validate(item);              // 自己呼び出し(this 経由)
        }

        @Override
        public void validate(String item) {
            System.out.println("    validate 本体: " + item);
        }
    }

    public static void main(String[] args) {
        OrderService target = new OrderServiceImpl();

        InvocationHandler advice = (proxy, method, methodArgs) -> {
            System.out.println("[前] " + method.getName());
            try {
                return method.invoke(target, methodArgs);
            } finally {
                System.out.println("[後] " + method.getName());
            }
        };

        OrderService proxy = (OrderService) Proxy.newProxyInstance(
                OrderService.class.getClassLoader(),
                new Class<?>[] { OrderService.class },
                advice);

        System.out.println("--- 1) プロキシ経由で placeOrder を呼ぶ ---");
        proxy.placeOrder("ノート");

        System.out.println("--- 2) プロキシ経由で validate を直接呼ぶ ---");
        proxy.validate("ノート");

        System.out.println("--- 3) new した本体を直接呼ぶ ---");
        target.placeOrder("ノート");

        System.out.println("proxy のクラス名: " + proxy.getClass().getSimpleName().replaceAll("\\d+", "N"));
        System.out.println("proxy は OrderServiceImpl か: " + (proxy instanceof OrderServiceImpl));
    }
}

実行結果(java ProxyDemo.java、OpenJDK 21.0.10):

--- 1) プロキシ経由で placeOrder を呼ぶ ---
[前] placeOrder
    placeOrder 本体: ノート
    validate 本体: ノート
[後] placeOrder
--- 2) プロキシ経由で validate を直接呼ぶ ---
[前] validate
    validate 本体: ノート
[後] validate
--- 3) new した本体を直接呼ぶ ---
    placeOrder 本体: ノート
    validate 本体: ノート
proxy のクラス名: $ProxyN
proxy は OrderServiceImpl か: false
  • 1):placeOrder には [前]/[後] が付きましたが、その中から this.validate(...) で呼ばれた validate には付いていません。自己呼び出しはプロキシを通らず、本体のメソッドに直接届きます。
  • 2):同じ validate でも、プロキシ経由で呼べば [前]/[後] が付きます。
  • 3):new した本体を直接呼ぶと、どちらにも付きません。Bean でないオブジェクトにアスペクトが掛からないのと同じ形です。
  • 最後の2行から、プロキシは本体とは別のクラスのオブジェクトだとわかります。

自己呼び出しを直す例

直す前:

package com.example.demo.service;

import org.springframework.stereotype.Service;

import com.example.demo.aop.Loggable;

@Service
public class OrderService {

    public void placeOrder(String item) {
        validate(item); // this.validate(...) と同じ。アスペクトは動かない
        // 登録処理
    }

    @Loggable("注文の検証")
    public void validate(String item) {
        // 検証処理
    }
}

直した後(公式が第一に勧める「自己呼び出しをなくす」形):

package com.example.demo.service;

import org.springframework.stereotype.Service;

import com.example.demo.aop.Loggable;

@Service
public class OrderValidator {

    @Loggable("注文の検証")
    public void validate(String item) {
        // 検証処理
    }
}
package com.example.demo.service;

import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final OrderValidator orderValidator;

    public OrderService(OrderValidator orderValidator) {
        this.orderValidator = orderValidator;
    }

    public void placeOrder(String item) {
        orderValidator.validate(item); // 注入されたプロキシ経由。アスペクトが動く
        // 登録処理
    }
}

Spring Framework リファレンスは、対処を次の順で挙げています。

方法 内容 公式の位置づけ
自己呼び出しをなくす 別の Bean に分ける 推奨
自分自身を注入する 自分の型のフィールドを注入し、それ経由で呼ぶ 選択肢の1つ
AopContext.currentProxy() 現在のプロキシを取り出して呼ぶ(exposeProxy が必要) コードが Spring AOP に結び付くため控えめに

AspectJ のコンパイル時・ロード時ウィービングはプロキシではなくバイトコードに処理を織り込むため、自己呼び出しでもアドバイスが動きます。

Spring Boot 3.x と 4.x の違いの対応表

公式の移行ガイドとリファレンスで確認できた範囲です。

項目 Spring Boot 3.x Spring Boot 4.x
AOP のスターター spring-boot-starter-aop spring-boot-starter-aspectj
Spring Framework 3.0 は 6.0 7.x
Java 17 以上 17 以上
Jakarta EE 3.0 は 10 11
Bean 単位のプロキシ方式指定 — @Proxyable(Framework 7.0 以降)

@Aspect・@Around・@Pointcut などのアノテーションは org.aspectj.lang.annotation パッケージのままで、アスペクトのコード自体は 3.x と 4.x で同じ書き方です。移行時に書き換えるのは依存関係の名前です。Spring Boot 4.x の現行版は 4.1.1 です(spring.io のプロジェクトページ)。

よくある質問

Q. @Aspect を付けたのにログが出ません。最初に何を確認すればよいですか? A. 上から順に、依存関係(4.x は spring-boot-starter-aspectj)、@Component の有無、対象が Spring の Bean か、同じクラス内からの呼び出しでないか、ポイントカットのパッケージ名、の5点を確認します。

Q. Spring AOP と AspectJ は何が違いますか? A. Spring AOP はプロキシ方式で、Spring の Bean のメソッド実行だけを対象にします。AspectJ はコンパイル時やロード時にバイトコードへ織り込む方式で、自己呼び出しやコンストラクターも対象にできます。Spring AOP は AspectJ のアノテーションと式の文法を借りて使っています。

Q. コントローラーにもアスペクトを掛けられますか? A. 掛けられます。コントローラーも Bean なので、within(com.example.demo.web..*) のように指定できます。

Q. @Around で例外を外に出さずに処理してもよいですか? A. 呼び出し元が失敗に気づけなくなるため、記録したうえで throw し直すのが基本です。代わりの値を返す場合は、戻り値の型と合っているかを確認します。

まとめ

  • @Aspect + @Component を付けたクラスに、ポイントカット(どこに)とアドバイス(いつ何を)を書く
  • 依存関係は 3.x が spring-boot-starter-aop、4.x が spring-boot-starter-aspectj
  • アドバイスは5種。記録だけなら @Before/@AfterReturning/@AfterThrowing、前後で同じ変数を使う・差し替えるなら @Around
  • 指示子は10種。主役は execution、範囲は within、目印で指定するなら @annotation
  • service.* は直下だけ、service..* は配下も含む。式は AspectJ のパーサーで当たり先を確認できる
  • 動かないときは、自己呼び出し・private/final・Bean でないオブジェクト・式の書き間違い・スターター名を確認する
  • @Order は値が小さいほど外側。入るときは先、出るときは後に動く

参考・出典

確認日はいずれも 2026年10月7日です。

  • Spring Framework リファレンス(7.0.9)Declaring a Pointcut:https://docs.spring.io/spring-framework/reference/core/aop/ataspectj/pointcuts.html
  • Spring Framework リファレンス(7.0.9)Declaring Advice:https://docs.spring.io/spring-framework/reference/core/aop/ataspectj/advice.html
  • Spring Framework リファレンス(7.0.9)Declaring an Aspect:https://docs.spring.io/spring-framework/reference/core/aop/ataspectj/at-aspectj.html
  • Spring Framework リファレンス(7.0.9)Enabling @AspectJ Support:https://docs.spring.io/spring-framework/reference/core/aop/ataspectj/aspectj-support.html
  • Spring Framework リファレンス(7.0.9)Proxying Mechanisms:https://docs.spring.io/spring-framework/reference/core/aop/proxying.html
  • Spring Boot 4.0 Migration Guide:https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide
  • Spring Boot 3.0 Migration Guide:https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide
  • Spring Boot プロジェクトページ(現行 4.1.1):https://spring.io/projects/spring-boot

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Spring AOPの使い方|@Aspectのコード全文と指示子一覧」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?