Spring AOP 与动态代理:从 @Transactional 到代理失效

举报
yd_216253085 发表于 2026/10/31 22:59:34 2026/10/31
【摘要】 讲清 AOP 的织入时机、两种代理的取舍,以及代理失效的根因。

AOP 常被当成"给方法加日志"的语法糖,实际上它是 Spring 事务、缓存、异步、安全校验的共同底座。不理解代理,就无法解释为什么 @Transactional 在自调用时失效、为什么 private 方法上的注解不生效、为什么同类方法调用绕过拦截器。本文从 AbstractAutoProxyCreator 的源码路径出发,把 Spring AOP 的织入机制讲清楚。适合能写业务代码、想掌握框架底层行为的中级后端阅读。

一、AOP 的基本概念

  • 切面(Aspect):横切关注点的模块化,比如"所有写操作都要记录审计日志"。
  • 连接点(JoinPoint):程序执行中可以插入逻辑的点。Spring AOP 只支持方法执行级别的连接点。
  • 切点(Pointcut):匹配连接点的表达式,决定"在哪织入"。
  • 通知(Advice):织入的具体逻辑,分 @Before、@After、@AfterReturning、@AfterThrowing、@Around。
  • 织入(Weaving):把通知应用到目标对象的过程。Spring 在运行时通过动态代理完成织入,AspectJ 则在编译期或类加载期完成。

Spring AOP 基于代理,因此有两个天然限制:只能拦截方法调用(字段访问、构造器不行),只能拦截外部调用(通过代理对象进来的调用)。

二、代理的创建时机

代理不是在 Bean 创建之初就生成的,而是在初始化后置处理阶段。调用链如下:

AbstractApplicationContext#refresh
  └─ finishBeanFactoryInitialization
      └─ DefaultListableBeanFactory#preInstantiateSingletons
          └─ AbstractAutowireCapableBeanFactory#doCreateBean
              └─ initializeBean
                  └─ applyBeanPostProcessorsAfterInitialization
                      └─ AbstractAutoProxyCreator#postProcessAfterInitialization
                          └─ wrapIfNecessary
                              └─ createProxy

wrapIfNecessary 的核心判断:

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
        return bean;
    }
    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
        return bean;
    }
    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    if (specificInterceptors != DO_NOT_PROXY) {
        this.advisedBeans.put(cacheKey, Boolean.TRUE);
        Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }
    this.advisedBeans.put(cacheKey, Boolean.FALSE);
    return bean;
}

getAdvicesAndAdvisorsForBean 会遍历容器中所有 Advisor,用 Pointcut#matches 判断是否匹配当前类的方法。匹配到至少一个,就创建代理。

三、JDK 代理与 CGLIB

DefaultAopProxyFactory 的决策逻辑很直接:

public AopProxy createAopProxy(AdvisedSupport config) {
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class");
        }
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
        }
        return new ObjenesisCglibAopProxy(config);
    }
    return new JdkDynamicAopProxy(config);
}

用表格对比两种方式:

| 维度 | JDK 动态代理 | CGLIB |

| --- | --- | --- |

| 实现方式 | 生成实现同一接口的类,InvocationHandler 拦截 | 生成目标类的子类,覆写非 final 方法 |

| 前提 | 目标类必须实现接口 | 目标类不能是 final,方法不能是 final/private |

| 依赖 | JDK 内置 | Spring 5.2 起内置 spring-core,无需单独引入 |

| 性能 | 调用略快,创建略慢 | 创建略快,调用因多态略慢 |

| 类型 | 只能转换为接口类型 | 可转换为目标类类型 |

Spring Boot 2.x 起,spring.aop.proxy-target-class 默认是 true,即默认强制 CGLIB。原因是 JDK 代理下注入接口类型才能成功,直接注入实现类会报 BeanNotOfRequiredTypeException,对开发者不友好。

Spring Framework 6 使用的 CGLIB 已改为基于 ClassWriter 的字节码生成,并支持在 JDK 17 上运行,不再需要 --add-opens。

四、拦截器链的执行

代理对象被调用后,进入 JdkDynamicAopProxy#invoke 或 CglibAopProxy#intercept,两者都会构造 ReflectiveMethodInvocation 并调用 proceed():

public Object proceed() throws Throwable {
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return invokeJoinpoint();
    }
    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher dm) {
        Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
        if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
            return dm.interceptor.invoke(this);
        }
        return proceed();
    }
    return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}

这是一个递归的责任链:每个 MethodInterceptor 调用 invocation.proceed() 推进到下一个,@Around 的"环绕"正是通过这种递归实现的。多个通知的默认顺序是 @Around → @Before → 目标方法 → @AfterReturning / @AfterThrowing → @After → @Around 后半段。同类型通知按 @Order 排序。

五、代理失效的三种典型场景

5.1 自调用绕过代理

@Service
public class UserService {

    public void register(User user) {
        save(user);   // this.save(user),走的是原始对象
    }

    @Transactional
    public void save(User user) {
        userMapper.insert(user);
    }
}

register 通过代理调用,代理把 register 转发给目标对象;目标对象内部的 this.save(user) 是普通 Java 调用,不经过代理,事务拦截器不会触发。

正确写法之一是把 save 移到另一个 Bean:

@Service
public class UserService {
    private final UserRepositoryService repositoryService;

    public UserService(UserRepositoryService repositoryService) {
        this.repositoryService = repositoryService;
    }

    public void register(User user) {
        repositoryService.save(user);
    }
}

之二是注入自身代理:

@Service
public class UserService {
    @Lazy
    @Autowired
    private UserService self;

    public void register(User user) {
        self.save(user);
    }
}

之三是在启动类加 @EnableAspectJAutoProxy(exposeProxy = true),通过 AopContext.currentProxy() 获取当前代理。这种方式侵入性强,且要求调用发生在代理上下文内。

5.2 方法可见性不匹配

@Service
public class OrderService {
    @Transactional
    private void createOrder() {   // 永远不会生效
        // ...
    }
}

CGLIB 通过子类覆写实现拦截,private 方法无法被覆写;protected、包级可见方法在跨包场景下也无法拦截。Spring 6 中,@Transactional 标注在非 public 方法上时,AnnotationTransactionAttributeSource 默认直接忽略,不报错也不生效。受检的 AOP 方法一律声明为 public。

5.3 final 类或 final 方法

@Service
public final class PayService {   // final 类无法被 CGLIB 继承
    @Transactional
    public void pay() { /* ... */ }
}

启动时报:

Cannot subclass final class com.example.PayService

把 final 去掉,或改为实现接口并让 Spring 使用 JDK 代理。

六、切点表达式的写法

切点表达式决定"在哪里织入",写错它比写错通知更难排查,因为不报错、只是不生效。

// 匹配注解:方法上有 @Audit
@Pointcut("@annotation(com.example.audit.Audit)")

// 匹配注解:类上有 @Audit,作用于该类所有方法
@Pointcut("@within(com.example.audit.Audit)")

// 按包路径匹配,.. 表示任意层子包
@Pointcut("execution(* com.example.service..*.*(..))")

// 匹配所有实现某接口的 Bean
@Pointcut("this(com.example.service.Payable)")

// 组合条件
@Pointcut("execution(public * com.example.service..*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)")

execution 的完整语法是 execution(修饰符 返回类型 声明类型.方法名(参数) 异常),其中 * 匹配任意单层,.. 匹配任意多层(包或参数)。@annotation 匹配方法级注解,@within 匹配类型级注解,@target 在运行期按目标对象类型判断、性能更差,优先用前两者。

七、一个完整的切面示例

@Aspect
@Component
@Order(1)
public class AuditAspect {

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

    @Pointcut("@annotation(com.example.audit.Audit)")
    public void auditPointcut() {
    }

    @Around("auditPointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.nanoTime();
        String method = pjp.getSignature().toShortString();
        try {
            Object result = pjp.proceed();
            log.info("method={} cost={}ms status=ok", method, (System.nanoTime() - start) / 1_000_000);
            return result;
        } catch (Throwable ex) {
            log.warn("method={} cost={}ms status=fail", method, (System.nanoTime() - start) / 1_000_000, ex);
            throw ex;
        }
    }
}

自定义注解需要 @Retention(RetentionPolicy.RUNTIME),否则运行期读不到。

如果切面依赖 Web 请求上下文,可以直接注入 HttpServletRequest 并使用 RequestContextHolder,但要注意异步线程中取不到:

@Around("execution(* com.example.web..*.*(..))")
public Object logRequest(ProceedingJoinPoint pjp) throws Throwable {
    ServletRequestAttributes attrs =
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    if (attrs != null) {
        HttpServletRequest request = attrs.getRequest();
        log.info("uri={} method={}", request.getRequestURI(), request.getMethod());
    }
    return pjp.proceed();
}

八、多个切面的顺序

多个切面同时匹配一个方法时,顺序由 @Order 或 Ordered 决定,值越小越靠外层。假设 TxAspect 的 order = 1、LogAspect 的 order = 2,执行顺序是:

TxAspect 前半段
  LogAspect 前半段
    目标方法
  LogAspect 后半段
TxAspect 后半段

这个顺序直接决定语义。如果日志切面在外层、事务切面在内层,那么日志记录的是"事务已提交后"的耗时;反过来,日志会包含事务提交时间。做接口耗时统计时通常希望包含提交,就把日志切面放在外层。

Spring 内置的事务拦截器优先级是 Ordered.LOWEST_PRECEDENCE,自定义切面若不显式设 @Order,会与之同级,顺序不确定。需要"包住事务"的切面必须显式给一个较小的值。

九、常见坑 / 最佳实践

  1. 在同一个类中调用带注解的方法。这是事务、缓存、异步失效的头号原因。抽类或注入自身代理,不要指望注解自动生效。
  2. 把 @Transactional 写在接口上。CGLIB 代理基于实现类,接口上的注解在 proxyTargetClass = true 时可能读不到。统一写在实现类的 public 方法上。
  3. 切面里捕获异常后不抛出。@Around 吞掉异常会让事务拦截器误以为执行成功,导致该回滚的不回滚。要么原样抛出,要么明确转换后抛出。
  4. @Order 控制切面顺序但忽略了事务优先级。事务拦截器默认 Ordered.LOWEST_PRECEDENCE,自定义切面若也用最低优先级,顺序不确定。需要"事务外层"语义时显式设一个较小的值。
  5. 对 final 类或 final 方法使用 AOP。CGLIB 无法代理,启动直接失败。检查被代理类是否被 Lombok 的 @Value(会生成 final 类)等工具改造成 final。

十、小结

  • Spring AOP 是运行时代理织入,发生在 initializeBean 的 AfterInitialization 阶段。
  • Spring Boot 默认 proxyTargetClass = true,使用 CGLIB;目标类不能是 final,方法不能是 private/final。
  • 拦截链是递归责任链,@Around 的环绕语义来自 proceed() 的递归调用。
  • 自调用、非 public、final 三类场景是代理失效的主要来源。
  • 需要事务、缓存、异步生效时,调用必须从代理对象进入。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。