Skip to content

AOP 原理与实践

Web 与框架 ⭐⭐ 中级 🔥🔥🔥 高频

💡 核心要点

AOP 把 横切关注点(日志/事务/缓存/权限)从业务代码抽离。Spring AOP = 运行时代理(JDK / CGLIB)AspectJ = 编译期 / 类加载期织入。面试核心:5 大失效场景(自调用、非 public、final、非 Bean、内部类)都是同一个原因 —— 不走代理就不生效。2026 必补:虚拟线程时代 @Async / ThreadLocal 陷阱


核心概念

AOP(Aspect-Oriented Programming,面向切面编程)把散布在多个类中的横切逻辑(日志、事务、权限)抽取到一个地方统一处理。

概念说明类比
切面(Aspect)横切关注点的模块化一个处理日志的类
连接点(JoinPoint)程序执行中可以插入切面的点每一个方法的执行
切入点(Pointcut)匹配连接点的表达式"所有 Service 层的方法"
通知(Advice)在连接点执行的动作方法执行前打印日志
织入(Weaving)把切面应用到目标对象的过程Spring 在运行时通过代理织入

五种通知类型

类型注解执行时机
前置通知@Before目标方法执行前
后置通知@After目标方法执行后(无论是否异常)
返回通知@AfterReturning目标方法正常返回后
异常通知@AfterThrowing目标方法抛出异常后
环绕通知@Around包裹目标方法,可控制是否执行
java
@Aspect
@Component
public class LogAspect {

    // 切入点:所有 service 包下的 public 方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}

    @Before("serviceLayer()")
    public void logBefore(JoinPoint jp) {
        System.out.println("调用: " + jp.getSignature().getName());
    }

    @Around("serviceLayer()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed(); // 执行目标方法
        long cost = System.currentTimeMillis() - start;
        System.out.println(pjp.getSignature().getName() + " 耗时: " + cost + "ms");
        return result;
    }
}

多个切面的执行顺序:@Order(n) 控制,值越小优先级越高。

代理机制

Spring AOP 通过动态代理实现,不修改目标类的源代码:

对比项JDK 动态代理CGLIB 代理
代理目标接口
实现方式实现目标接口生成目标类的子类
限制目标必须有接口目标类和方法不能是 final
性能调用通过反射,较慢调用通过字节码,较快
Spring Boot 默认✅(spring.aop.proxy-target-class=true)

Spring Boot 默认用 CGLIB,即使目标类实现了接口也用 CGLIB 代理。原因是 CGLIB 统一代理方式,避免了 JDK 代理下注入时必须用接口类型的问题。

AOP 的典型应用

场景实现
事务管理@Transactional — 最经典的 AOP 应用
日志记录自定义 @Around 记录方法入参、出参、耗时
权限校验@PreAuthorize(Spring Security 基于 AOP)
缓存@Cacheable(Spring Cache 基于 AOP)
重试@Retryable(Spring Retry 基于 AOP)

AOP 失效场景

这是高频面试题 — 以下情况 AOP 不会生效:

1. 自调用(同类方法调用)

java
@Service
public class OrderService {
    public void createOrder() {
        this.validate(); // ❌ this 是原始对象,不是代理对象,AOP 不生效
    }
    
    @Transactional
    public void validate() { ... }
}

原因: this.validate() 直接调用的是原始对象的方法,没有经过代理。

解决方式:

  • 将方法拆到另一个 Bean 中
  • 注入自身代理:@Autowired private OrderService self; 然后 self.validate()
  • 使用 AopContext.currentProxy()(需开启 exposeProxy

2. 方法不是 public

Spring AOP 默认只拦截 public 方法。protected、private、default 方法上的 @Transactional 等注解不会生效。

3. final 类或 final 方法

CGLIB 通过继承实现代理,final 类无法被继承,final 方法无法被重写,因此 AOP 不生效。

4. 非 Spring 管理的对象

直接 new MyService() 创建的对象不受 Spring 管理,AOP 不会生效。必须由 Spring 容器创建的 Bean 才有代理。

5. 内部类

内部类通常不由 Spring 直接管理,AOP 不会生效。

@Transactional 深度:AOP 应用的"王者"

@Transactional 是 AOP 在 Spring 中最常见、最容易踩坑的应用。2025-2026 年大厂 Java 面试几乎必问事务传播行为和失效场景。

7 种事务传播行为(必背)

传播行为含义典型场景
REQUIRED(默认)当前有事务则加入,没有则新建99% 的业务方法
REQUIRES_NEW总是新建独立事务,挂起当前事务写日志、发通知(即使主事务回滚也要保留)
NESTED在当前事务中开 savepoint 子事务部分失败可回滚到 savepoint 而不影响外层
SUPPORTS有事务就加入,没有就以非事务运行工具方法
NOT_SUPPORTED总是非事务执行,挂起当前事务长查询不需要事务开销
NEVER必须无事务,否则抛异常强制无事务的场景
MANDATORY必须有事务,否则抛异常强制要求被事务调用

REQUIRES_NEW 关键场景

java
@Service
public class OrderService {
    @Autowired private LogService logService;

    @Transactional   // 默认 REQUIRED
    public void createOrder(Order o) {
        orderRepo.save(o);
        try {
            logService.writeAudit(o);   // 即使日志失败也不要影响订单
        } catch (Exception e) {
            log.error("audit failed", e);
        }
        // ...
    }
}

@Service
public class LogService {
    @Transactional(propagation = REQUIRES_NEW)   // 独立事务!
    public void writeAudit(Order o) {
        auditRepo.save(...);
    }
}

⚠️ NESTED ≠ REQUIRES_NEW

  • NESTED:savepoint 子事务,和外层共享数据库连接,外层回滚则子事务一定回滚
  • REQUIRES_NEW完全独立的新连接 + 新事务,互不影响

很多人混淆这两个,面试官最爱在这里设陷阱。

@Transactional 失效场景(高频追问)

6 种最常见的失效场景,每种都要能讲出原因 + 修复:

失效场景原因修复
1. 同类内部方法调用this.xxx() 不走代理自注入 / 拆 Bean / AopContext.currentProxy()
2. 方法不是 publicSpring AOP 只代理 public改为 public
3. 异常被 catch 吃掉没有异常抛出 → 不会回滚catch主动 rethrowsetRollbackOnly()
4. 抛出 checked 异常(如 IOException)默认只回滚 RuntimeException@Transactional(rollbackFor = Exception.class)
5. 数据库引擎不支持事务MyISAM 不支持改 InnoDB
6. 多线程调用事务绑定线程,新线程不在事务中不要在事务方法里开异步

同类调用失效(最经典)

java
@Service
public class OrderService {
    public void createOrder(Order o) {
        this.save(o);          // ❌ 走的是原始对象,不走代理
    }
    @Transactional
    public void save(Order o) { /* ... */ }
}
java
// ✅ 修复方案 1: 拆 Bean(推荐)
@Service
public class OrderFacade {
    @Autowired private OrderService orderService;
    public void create(Order o) {
        orderService.save(o);   // 走代理
    }
}

// ✅ 修复方案 2: 自注入
@Service
public class OrderService {
    @Autowired private OrderService self;   // 注入的是代理
    public void createOrder(Order o) {
        self.save(o);
    }
    @Transactional
    public void save(Order o) { /* ... */ }
}

异常吞噬失效(生产 Top 1 事故)

java
// ❌ 错误:catch 后没有抛出 → 事务不回滚
@Transactional
public void transfer(Long from, Long to, BigDecimal amount) {
    try {
        accountRepo.debit(from, amount);
        accountRepo.credit(to, amount);
    } catch (Exception e) {
        log.error("transfer failed", e);   // 吞了异常,事务正常提交!
    }
}

// ✅ 修复 1:rethrow
} catch (Exception e) {
    log.error("transfer failed", e);
    throw new RuntimeException(e);
}

// ✅ 修复 2:主动标记回滚
} catch (Exception e) {
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}

@Transactional 的"小问号"答疑

💡 三个高频细节

Q:@Transactional 默认隔离级别?

跟随数据库默认(MySQL 是 RR)。可以用 isolation = Isolation.READ_COMMITTED 显式指定。

Q:@Transactional 默认超时?

-1(无超时)。生产应该显式设置 timeout=30(秒),防止长事务持有连接和锁。

Q:readOnly = true 真的能优化吗?

能。MySQL 会跳过 MVCC 的某些开销;Hibernate 会跳过 dirty check;对长查询/报表方法务必加 @Transactional(readOnly = true)

虚拟线程时代的 @Async / AOP(Boot 3.2+ 必读)

⚠️ 2026 实战陷阱

Spring Boot 3.2 起 spring.threads.virtual.enabled=true 一行开启虚拟线程,@Async 默认 Executor 也切到虚拟线程。带来两个新问题:

问题原因解决
@Async 不再用线程池虚拟线程"用完即弃",没有池化不再需要 corePoolSize 调优;但要小心没有队列、限流由信号量代替
ThreadLocal 跨方法漏数据虚拟线程频繁创建销毁,旧的"绑线程上下文"假设失效切到 ScopedValue(Java 21+ 预览,25 GA)或 Context Propagation(io.micrometer:context-propagation)
synchronized 触发 Pinning虚拟线程被钉到 OS 线程,丢失轻量性JDK 24+ 已修复绝大多数 pinning;老库改用 ReentrantLock
AOP 代理跨虚拟线程边界代理对象正常工作,但事务/SecurityContext 不会自动传递到 @Async显式传递(DelegatingSecurityContextRunnable / @WithSpan

面试黄金答法:「虚拟线程时代,AOP 代理本身没变;变的是被代理方法内的"线程上下文"假设——ThreadLocal 退场、ScopedValue / Context Propagation 上位」。

Spring AOP vs AspectJ:性能与选型补充

指标Spring AOP(JDK/CGLIB)AspectJ(编译期)
调用开销每次方法调用走 invoke 反射 + interceptor 链直接字节码内联,几乎 0 开销
典型基准(百万次/秒)~5-10M~50-100M(一个数量级差距)
冷启动容器创建代理(轻)编译期织入(影响构建)
支持 private/final/构造方法
配置复杂度一行 @EnableAspectJAutoProxy需配 aspectjrt + aspectj-maven-plugin
AOT / Native ImageBoot 3 已支持 CGLIB AOT 优化编译期织入与 Native 天然契合

何时切 AspectJ:① 切面被高频调用(如每次 RPC、每次 Cache 查找),代理开销不可接受;② 需要切 private / final / 构造器;③ 监控 agent / 性能埋点(LTW + javaagent);④ Spring Native 极致体积。其余场景 Spring AOP 完全够用

面试常问 & 怎么答

Q1: JDK 动态代理和 CGLIB 的区别?

JDK 代理基于接口(实现接口的代理类),CGLIB 基于继承(生成目标类的子类)。JDK 代理要求目标有接口,CGLIB 要求目标类不能是 final。Spring Boot 默认用 CGLIB,统一代理方式,避免必须用接口类型注入的问题。

Q2: AOP 在什么场景下会失效?

五种情况:自调用(this 调用不经过代理)、方法不是 public、类或方法是 final(CGLIB 无法继承)、对象不是 Spring 管理的(手动 new)、内部类。最常考的是自调用 — @Transactional 标在同类的方法上,通过 this 调用时事务不会生效。

Q3: @Transactional 为什么会失效?

@Transactional 是基于 AOP 实现的,所以 AOP 失效的场景它都会失效。最典型的是自调用:在同一个类中用 this 调用 @Transactional 方法,因为没经过代理所以事务不生效。另外,默认只回滚 RuntimeException 和 Error,checked exception 不回滚(除非指定 rollbackFor)。

看到什么就先想到这类

  • 出现 @Aspect、@Pointcut、@Around、@Before。
  • 出现 JDK 动态代理、CGLIB。
  • 出现 @Transactional 失效、AOP 失效。
  • 出现"为什么自调用不生效"。