一次调用为什么绕过了切面:代理对象的生成与匹配
线上有个订单服务,@Transactional 加在 private 方法上,日志里看不到事务开启的痕迹。排查半天发现两件事:方法不是 public,调用还发生在同一个类内部。这两个坑都指向同一个东西——代理对象到底怎么来的、切面怎么匹配上。这篇把动态代理和切面匹配拆开讲,事务失效的根因下一篇再收口。
手把手实操
先造一个最小可跑的 AOP 场景。定义一个接口和实现,再写一个切面统计方法耗时。
public interface OrderService {
void create(String orderNo);
}
@Service
public class OrderServiceImpl implements OrderService {
public void create(String orderNo) {
System.out.println("create " + orderNo);
}
}
切面用 @Aspect 加 @Around,切点写 execution( com.demo.service..*(..))。启动类加 @EnableAspectJAutoProxy,Spring Boot 里 AopAutoConfiguration 会自动开,但显式写上便于观察。
@Aspect
@Component
public class TimeAspect {
@Around("execution(* com.demo.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long s = System.nanoTime();
try { return pjp.proceed(); }
finally { System.out.println(pjp.getSignature() + " cost " + (System.nanoTime()-s)); }
}
}
跑起来后注入 OrderService,打印它的类名。你会看到类似 com.demo.service.OrderServiceImpl$$EnhancerBySpringCGLIB$$... 的名字,说明拿到的是 CGLIB 代理,不是原始对象。把 @EnableAspectJAutoProxy(proxyTargetClass = false) 显式设上,再让实现类只实现接口,类名会变成 com.sun.proxy.$Proxy...,这是 JDK 动态代理。
关键点在两个开关:proxyTargetClass 决定用哪种代理,目标类有没有接口决定 JDK 代理能不能用。踩坑点:proxyTargetClass=true 时即使有接口也走 CGLIB,很多人以为有接口就一定用 JDK 代理,这是错的。
看源码及解析
代理选择的核心在 DefaultAopProxyFactory.createAopProxy。它的判断逻辑大致是:config.isOptimize()、config.isProxyTargetClass()、目标类是否是接口、是否是 Proxy 子类,任一条件触发就用 CGLIB,否则用 JDK 代理。proxyTargetClass 默认 false,所以有接口时默认 JDK 代理。
JDK 代理走 JdkDynamicAopProxy.invoke,它先拿当前方法的拦截器链 List。链为空就直接反射调目标方法,链不为空才包成 ReflectiveMethodInvocation 走 proceed()。这就是为什么没匹配到切点的方法几乎没开销——不是每次调用都过一遍切面逻辑。
CGLIB 走 CglibAopProxy.getProxy,用 Enhancer 设 setSuperclass、setCallback,回调里同样是拦截器链。CGLIB 生成的是目标类的子类,所以 final 类、final 方法没法被代理——子类不能覆盖。JDK 代理生成的是接口实现,所以只有接口里声明的方法能被拦截,实现类自己加的 public 方法不在代理视野里。
切点匹配靠 AspectJExpressionPointcut,它把 execution 表达式编译成 AspectJ 的匹配器,matches 时拿方法签名和声明类型去比对。这里有个容易忽略的点:匹配发生在方法调用进入代理时,不是启动时全量算好。拦截器链有缓存,但第一次匹配仍有成本。
为什么这么设计?JDK 代理不依赖字节码生成库,JDK 自带,但只能代理接口;CGLIB 能代理类,但要生成子类,启动时多一次字节码生成。Spring 把选择权交给配置,默认偏向 JDK 代理是为了减少对第三方库的依赖,代价是要求面向接口编程。真实面试里常追问:为什么 Spring Boot 2.x 起默认改成 proxyTargetClass=true?因为大量项目直接注入实现类,JDK 代理会抛 BeanNotOfRequiredTypeException,改成 CGLIB 更省事。这个默认值变化要记准,别答反。
验证方法
三个自测动作。第一,打印注入对象的 getClass(),确认代理类型和预期一致。第二,在切面里打 pjp.getTarget().getClass(),确认目标对象是原始类,代理和目标不是同一个。第三,把切点表达式改成只匹配某个方法,观察其他方法调用时切面日志是否消失——消失说明匹配生效,没消失说明表达式写错了。再补一个:给目标方法加 final,用 CGLIB 代理时启动或调用会报错,这是验证代理方式的直接手段。
面试速答
- 代理选择看
DefaultAopProxyFactory:有接口且proxyTargetClass=false走 JDK 代理,否则 CGLIB;Spring Boot 2.x 起默认proxyTargetClass=true。 - JDK 代理基于接口,
JdkDynamicAopProxy.invoke里拦截器链为空就直接反射调用;CGLIB 生成子类,final类和final方法代理不了。 - 切点匹配在调用进入代理时进行,
AspectJExpressionPointcut负责表达式比对,链有缓存。 - 同类内部调用不走代理,所以切面不生效——这是事务失效的头号原因。
代理是入口,切面是规则,两者对不上,注解写得再对也白搭。下一步:把 @Transactional 的传播行为和失效场景逐个过一遍。
本文关键词:JDK动态代理、CGLIB、AspectJExpressionPointcut、proxyTargetClass