Spring AOP 与动态代理:从 @Transactional 到代理失效
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,会与之同级,顺序不确定。需要"包住事务"的切面必须显式给一个较小的值。
九、常见坑 / 最佳实践
- 在同一个类中调用带注解的方法。这是事务、缓存、异步失效的头号原因。抽类或注入自身代理,不要指望注解自动生效。
- 把
@Transactional写在接口上。CGLIB 代理基于实现类,接口上的注解在proxyTargetClass = true时可能读不到。统一写在实现类的public方法上。 - 切面里捕获异常后不抛出。
@Around吞掉异常会让事务拦截器误以为执行成功,导致该回滚的不回滚。要么原样抛出,要么明确转换后抛出。 @Order控制切面顺序但忽略了事务优先级。事务拦截器默认Ordered.LOWEST_PRECEDENCE,自定义切面若也用最低优先级,顺序不确定。需要"事务外层"语义时显式设一个较小的值。- 对
final类或final方法使用 AOP。CGLIB 无法代理,启动直接失败。检查被代理类是否被 Lombok 的@Value(会生成final类)等工具改造成final。
十、小结
- Spring AOP 是运行时代理织入,发生在
initializeBean的AfterInitialization阶段。 - Spring Boot 默认
proxyTargetClass = true,使用 CGLIB;目标类不能是final,方法不能是private/final。 - 拦截链是递归责任链,
@Around的环绕语义来自proceed()的递归调用。 - 自调用、非
public、final三类场景是代理失效的主要来源。 - 需要事务、缓存、异步生效时,调用必须从代理对象进入。
- 点赞
- 收藏
- 关注作者
评论(0)