ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring AOP底层原理:动态代理与CGLIB字节码增强解析

Spring AOP底层原理:动态代理与CGLIB字节码增强解析 最近在帮一个团队升级项目到 Spring Boot 4.0 的预览版本时遇到了一件挺有意思的事服务能正常启动日志里也没有任何异常但原本跑得好好的 AOP 切面突然全部失效事务回滚也不生效了连Aspect注解都报了找不到类的错误。顺着这个故障往下查最后落到的问题原点反而是“Spring AOP 实现原理动态代理与字节码增强”这两个最基础的机制。这篇文章就从这次排查经历切入把 Spring AOP 的底层实现原理完整拆开讲一遍为什么需要动态代理JDK 代理和 CGLIB 字节码增强各自是怎么工作的Spring 容器又是如何把代理对象“偷梁换柱”塞进业务代码里的。1. 从热搜词说起Spring Boot 4.0 找不到 AOP 是怎么回事1.1 版本升级之后切面“哑火”的现场还原先说这个让不少人困惑的现象。在 Spring Boot 3.x 时代只要引入spring-boot-starter-webAOP 相关的基础依赖基本都会跟着进来Aspect、Before、Around这些注解开箱即用。到了 Spring Boot 4.0 的预览版本里依赖结构做了更细粒度的拆分AOP 不再默认随 Web Starter 传递这才出现了“找不到 AOP”的情况。实际报错可能是这样java.lang.NoClassDefFoundError: org/aspectj/lang/annotation/Aspect也可能是切面静默失效完全没有报错。为什么会有两种表现取决于项目里是否通过其他方式间接引入了 AspectJ 注解包。只要aspectjweaver缺失切面类的注解解析就会失败Spring 自然也就扫描不到任何切面定义。1.2 一条命令定位缺失的依赖遇到这种问题第一件事永远是看依赖树而不是改业务代码。用 Maven 的话两条命令就能定位mvn dependency:tree -Dincludesorg.springframework:spring-aop mvn dependency:tree -Dincludesorg.aspectj:aspectjweaverGradle 项目则用./gradlew dependencyInsight --dependency org.springframework:spring-aop ./gradlew dependencyInsight --dependency org.aspectj:aspectjweaver正常情况下应该能看到spring-aop和aspectjweaver这两个关键依赖。如果发现它们不在依赖树里或者版本被某个冲突规则排除了那 AOP 失效的原因就直接浮出水面。修复方式也很简单显式加上spring-boot-starter-aop依赖即可。1.3 从故障回到原理AOP 不是一个魔法这次升级故障最值得思考的地方在于很多同学平时用 AOP 用得很熟练但一旦脱离 Spring Boot 的自动配置连 AOP 依赖了哪几个 jar 包都说不清楚。这说明我们对 AOP 的理解还停留在“加注解、写切面”的 API 层没有真正落到实现原理层。AOP 不是什么运行时魔法它本质上就是一句话在 Bean 创建过程中通过动态代理生成一个代理对象让代理对象在调用目标方法前后执行额外的横切逻辑。而动态代理与字节码增强正是两条最核心的技术路径。2. AOP到底在解决什么问题从横切逻辑到代理模式2.1 一段没有切面的痛苦代码在没有 AOP 的世界里给业务方法加上日志和事务控制代码会长成这样public Order createOrder(OrderDTO dto) { long start System.currentTimeMillis(); try { // 开启事务 TransactionStatus status transactionManager.begin(); Order order doCreate(dto); // 提交事务 transactionManager.commit(status); log.info(createOrder cost {} ms, System.currentTimeMillis() - start); return order; } catch (Exception e) { // 回滚事务 transactionManager.rollback(status); log.error(createOrder failed, e); throw e; } finally { log.info(createOrder end); } }如果只有createOrder一个方法还好说问题是updateOrder、cancelOrder、queryOrder每一个方法都要重复这么一套逻辑。这就是典型的横切逻辑日志、事务、权限校验这些功能不关心业务方法的具体逻辑但又散落在每个业务方法里导致代码严重重复维护成本直线上升。2.2 从继承模板到代理模式的演进有人会想到用继承来解决写一个抽象基类把日志和事务的逻辑放到模板方法里子类只需要实现业务步骤。但继承的问题是硬编码的父子关系而且容易破坏业务类的继承结构。装饰器模式也可以做但每写一个业务类都要手工写一个装饰器类依然繁琐。真正让横切逻辑和业务逻辑解耦的是代理模式。代理模式的核心思想是客户端不直接持有目标对象而是持有一个代理对象。代理对象内部包装了目标对象在调用目标方法的前后插入额外逻辑。放到上面的例子里OrderServiceImpl不需要再写任何日志和事务代码代理对象负责统一处理。这样业务类保持干净公共逻辑集中管理。2.3 静态代理与动态代理的分水岭静态代理的问题是代理类需要手动编写一个业务类对应一个代理类项目会急剧膨胀。动态代理则不一样它在运行时动态生成代理类不用手工写代理类文件。AOP 正是站在动态代理的肩膀上。Spring AOP 中两种最常见的实现路径就是 JDK 动态代理和 CGLIB 字节码增强两者对应着不同的适用场景这也是面试中追问最深的点。3. JDK动态代理与CGLIB字节码增强两条技术路线的底层拆解3.1 JDK动态代理接口优先的官方方案JDK 动态代理是 Java 原生支持的代理方式使用起来非常简洁。先定义一个业务接口和实现类public interface OrderService { void createOrder(OrderDTO dto); } public class OrderServiceImpl implements OrderService { Override public void createOrder(OrderDTO dto) { // 核心业务逻辑 } }然后实现InvocationHandler接口把增强逻辑写在invoke方法里public class TimeLogInvocationHandler implements InvocationHandler { private final Object target; public TimeLogInvocationHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start System.currentTimeMillis(); Object result method.invoke(target, args); System.out.println(method.getName() cost (System.currentTimeMillis() - start) ms); return result; } }最后通过Proxy.newProxyInstance创建代理对象OrderService proxy (OrderService) Proxy.newProxyInstance( OrderServiceImpl.class.getClassLoader(), new Class?[] {OrderService.class}, new TimeLogInvocationHandler(new OrderServiceImpl()) ); proxy.createOrder(dto);这里有一个关键问题为什么 JDK 动态代理必须要求目标类实现接口原因在代理类的生成机制上。JDK 在运行时生成的代理类统一继承了java.lang.reflect.Proxy这个父类而 Java 是单继承的代理类已经占用了唯一的继承位不能再继承目标类。它只能通过实现接口的方式对外暴露与目标类一致的业务方法。所以当调用方通过接口引用调用方法时实际走的是代理类的invoke方法。我遇到过不少同学在这个地方栽跟头目标类没有实现接口强行用 JDK 动态代理结果抛ClassCastException或者创建代理失败。这不是代码写错了而是 JDK 动态代理在机制上就不支持无接口的类。3.2 CGLIB字节码增强没有接口也能代理的硬核方案CGLIB 解决的是 JDK 动态代理解决不了的问题目标类没有接口时怎么办。Spring 已经把 CGLIB 重定位到自己的包路径下也就是org.springframework.cglib所以工程里不需要显式引入 CGLIB 的独立依赖。CGLIB 的使用方式和 JDK 动态代理非常像核心入口是EnhancerEnhancer enhancer new Enhancer(); enhancer.setSuperclass(OrderServiceImpl.class); enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) - { long start System.currentTimeMillis(); Object result proxy.invokeSuper(obj, args); System.out.println(method.getName() cost (System.currentTimeMillis() - start) ms); return result; }); OrderServiceImpl proxy (OrderServiceImpl) enhancer.create();这段代码里最核心的一行是enhancer.setSuperclass(OrderServiceImpl.class)。CGLIB 会在运行时通过 ASM 字节码框架动态生成一个OrderServiceImpl的子类这个子类覆写了目标类中所有可继承的方法。客户端调用proxy.createOrder(dto)时实际调用的是子类覆写后的方法子类方法内部会回调MethodInterceptor并在拦截器里调用proxy.invokeSuper(obj, args)去执行原始方法。这就是“字节码增强”最直观的体现不是修改原始类而是生成一个新的子类字节码在子类方法中插入增强逻辑。3.3 字节码增强的边界final类、final方法为什么不行理解了“CGLIB 靠生成子类来增强”这一点很多限制就顺理成章了。final修饰的类无法被继承所以 CGLIB 无法代理 final 类。final修饰的方法无法被子类覆写所以无法对 final 方法做增强。private方法对子类不可见覆写无从谈起。static方法属于类而不是实例与代理对象的实例调用无关。这些限制不是 Spring 刻意为之而是 Java 继承机制和字节码生成框架本身就有的边界。面试时问到 CGLIB 的局限性从“继承”两个字回答基本就站住了。3.4 两条技术路线的完整对比对比维度JDK动态代理CGLIB字节码增强代理对象形态生成实现接口的 Proxy 子类生成目标类的子类目标类要求必须实现至少一个接口不能被 final 修饰底层技术Proxy InvocationHandlerASM 字节码生成方法调用方式反射调用FastClass 索引直调性能更好典型应用场景按接口编程的 Spring AOP无接口的类或Spring Boot默认场景有一个细节值得单独说明Spring Boot 2.x 之后spring.aop.proxy-target-class默认值是true意味着即使目标类实现了接口Spring 也会优先使用 CGLIB 生成代理。所以现代 Spring Boot 项目里我们大多数时候接触到的代理对象都是 CGLIB 代理而面试题里却还在反复问 JDK 和 CGLIB 的区别这说明两者的核心差异依然值得掌握。4. Spring AOP的完整组装链路从Aspect到代理对象4.1 切面的定义与Pointcut表达式先看一段最常见的切面代码Aspect Component public class LogAspect { Around(execution(* com.example.service.*.*(..))) public Object logAround(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); try { return pjp.proceed(); } finally { System.out.println(pjp.getSignature().getName() cost (System.currentTimeMillis() - start) ms); } } }这里的Aspect声明这是一个切面类Around声明环绕通知execution(* com.example.service.*.*(..))是切点表达式用来匹配哪些方法需要被增强。Spring AOP 本身并不做 AspectJ 的编译期织入它只是借用了 AspectJ 的注解和切点表达式语法实际运行时增强仍然是通过动态代理完成的。理解这一点对后面看源码会有很大帮助。4.2 EnableAspectJAutoProxy 开动了什么当我们在配置类上加了EnableAspectJAutoProxySpring 会注册一个核心的BeanPostProcessorAnnotationAwareAspectJAutoProxyCreator。这个类是整个 AOP 的枢纽。Spring 容器在创建每一个 Bean 时都会执行所有BeanPostProcessor的postProcessAfterInitialization方法而AnnotationAwareAspectJAutoProxyCreator正是在这个阶段介入完成代理对象的创建。如果项目里使用了SpringBootApplication其实这个注解也被自动配置包含了因为在自动配置类AopAutoConfiguration里已经默认开启了相关处理。4.3 从BeanDefinition到代理对象wrapIfNecessary 的决策过程把 AOP 代理的创建过程简化成四步非常清晰拿到当前 Bean 的类型。找到容器中所有的 Advisor通知器并对每个 Advisor 的切点表达式做匹配。如果至少有一个 Advisor 匹配成功说明当前 Bean 需要增强。根据配置和 Bean 的特征选择 JDK 动态代理或 CGLIB生成代理对象返回给容器。核心方法在AbstractAutoProxyCreator的wrapIfNecessary里。这个方法会先检查当前 Bean 是否已经配了增强然后遍历getAdvicesAndAdvisorsForBean拿到的候选通知器列表。匹配成功后调用createProxy创建代理。创建代理时具体走 JDK 还是 CGLIB取决于proxyTargetClass的配置true走 CGLIBfalse时再看目标类是否实现接口。4.4 Spring三级缓存与AOP代理为什么要提前生成这里要重点说一下 Spring 三级缓存和 AOP 的关系。很多同学对三级缓存的印象停留在“解决循环依赖”上但实际上它和 AOP 也有很深的纠缠。正常情况下Bean 的完整创建流程是实例化 - 属性填充 - 初始化 - 生成代理。但如果 Bean A 和 Bean B 发生了循环依赖比如 A 需要注入 BB 又需要注入 A那么在 A 实例化之后、还没完成初始化的时候Spring 就会把 A 提前暴露到一个 ObjectFactory 工厂里。如果 A 需要被 AOP 增强这个getEarlyBeanReference阶段就会提前创建 A 的代理对象并让 B 拿到这个代理对象。为什么要这么绕因为如果等到初始化完成后再生成代理B 拿到的就是 A 的原始对象A 上的事务、日志等切面逻辑全部失效。Spring 在AbstractAutoProxyCreator中专门有getEarlyBeanReference方法处理这种情况而且会通过earlyProxyReferences集合记录哪些 Bean 已经被提前代理防止后续初始化流程中再次包裹同样的对象。所以当有人问“Spring 三级缓存和 AOP 有什么关系”时回答的核心就是在循环依赖场景下AOP 代理必须在 Bean 提前暴露时就生成否则代理会晚于依赖注入导致切面失效。4.5 proxyTargetClass 的配置影响Spring Boot 配置文件里只要能写进来这一行spring.aop.proxy-target-classtrue代理策略就是 CGLIB。如果显式设置为false并且目标类实现了接口Spring 会改用 JDK 动态代理。不过现代 Spring Boot 默认已经是 CGLIB 了这个配置在实际项目中需要调整的场景不多但在老 Spring 项目中还是比较常见的。5. 真实项目里AOP的实战场景与代码示例5.1 接口耗时与日志切面最常见的入门场景几乎所有项目都需要的接口日志和性能监控用 AOP 实现是最自然的。下面这个切面可以直接抄进工程里Aspect Component public class AccessLogAspect { private static final Logger log LoggerFactory.getLogger(AccessLogAspect.class); Around(annotation(org.springframework.web.bind.annotation.RequestMapping) || annotation(org.springframework.web.bind.annotation.GetMapping) || annotation(org.springframework.web.bind.annotation.PostMapping)) public Object logController(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); Object result pjp.proceed(); String method pjp.getSignature().toShortString(); long cost System.currentTimeMillis() - start; log.info({} 执行耗时 {} ms, method, cost); if (cost 1000) { log.warn({} 执行超时耗时 {} ms, method, cost); } return result; } }这段切面的实际价值在于把耗时和超时告警逻辑从每个 Controller 里彻底去掉。配上Pointcut抽取公共切点后维护起来也更方便。5.2 基于自定义注解实现幂等与防重复提交AOP 和其他机制结合最典型的例子是自定义注解 切面实现防重复提交。先定义一个注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface NoRepeatSubmit { long timeout() default 3000; }然后在需要防止重复提交的接口上直接加注解NoRepeatSubmit(timeout 5000) PostMapping(/order) public Result createOrder(RequestBody OrderDTO dto) { // 业务逻辑 return Result.success(); }切面里通过 Redis 的 SET NX 实现分布式锁语义Aspect Component public class NoRepeatSubmitAspect { Autowired private StringRedisTemplate redisTemplate; Around(annotation(noRepeatSubmit)) public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable { String method pjp.getSignature().toLongString(); String key repeat:submit: method : JSON.toJSONString(pjp.getArgs()); Boolean success redisTemplate.opsForValue().setIfAbsent(key, 1, noRepeatSubmit.timeout(), TimeUnit.MILLISECONDS); if (Boolean.FALSE.equals(success)) { throw new RuntimeException(请勿重复提交); } return pjp.proceed(); } }这里就体现了 AOP 无侵入的优势业务方法完全不知道自己在被防重复提交保护所有逻辑都在切面里集中处理。5.3 声明式事务AOP最成功的商业实践Transactional是所有 Spring 开发者最先接触的 AOP 应用而且它背后就是一套完整的事务增强链路。Spring 内部通过ProxyTransactionManagementConfiguration注册了事务相关的 Advisor这个 Advisor 的切点会匹配带有Transactional注解的方法。匹配成功后容器创建代理对象在方法调用前后执行事务开启、提交、回滚等逻辑。理解了这一点面试中关于事务的很多问题都能迎刃而解为什么同类之间用this调用Transactional方法事务不生效因为this指向的是原始对象而不是代理对象包裹事务逻辑的代理完全没被经过。为什么Transactional加在private方法上不生效因为private方法无法被 CGLIB 覆写。这些问题的根源都在动态代理的底层机制上。5.4 动态数据源切换与读写分离多数据源场景里AOP 也是核心角色。我们通常会定义一个DataSource注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface DataSource { String value() default master; }然后在业务方法上标注走哪个数据源DataSource(slave) public ListUser listUsers() { // 查询逻辑 }切面里把注解中的值写入当前线程的 ThreadLocal动态数据源通过AbstractRoutingDataSource的determineCurrentLookupKey方法读取 ThreadLocal 中的 key实现读写分离。整个过程中业务代码同样没有出现任何数据源切换逻辑全部由 AOP 接管。5.5 什么时候不该用AOPAOP 不是万能的过度使用反而会增加排查难度。我自己总结了几条使用边界方法内部调用尤其是this调用达不到增强效果要让别人调外部 Bean。高频的短小方法如果有大量方法级切面会有额外的动态代理调用开销。切面表达式写的过宽容易误拦截到不该拦截的方法导致日志里出现大量无关的“噪音”。团队如果对 AOP 不熟建议把切面逻辑收敛在少数几个切面类里不要散落得到处都是。6. 高频踩坑与面试追问从自调用失效到代理选择6.1 同一个类里的自调用为什么让切面失效这是 AOP 面试出场率最高的坑。看这段代码Service public class UserService { Transactional public void createUser(User user) { // 插入用户 } public void createUserWithLog(User user) { this.createUser(user); // 自调用 } }外部调用createUserWithLog时Spring 注入给调用方的是一个代理对象。执行到this.createUser(user)这一行时this指向的是目标类原始对象而不是代理对象所以事务增强完全不会执行。这就是自调用导致切面失效的根本原因。6.2 三种可靠的自调用修复方案方案一注入自身。把代理对象注入进来通过代理对象调用方法Service public class UserService { Autowired private UserService self; public void createUserWithLog(User user) { self.createUser(user); } }方案二使用 AopContext。前提是开启 exposeProxyEnableAspectJAutoProxy(exposeProxy true)调用时通过AopContext.currentProxy()获取当前代理对象((UserService) AopContext.currentProxy()).createUser(user);方案三把被调用的方法拆到另一个 Bean 里。这是最彻底也最符合 Spring 设计风格的做法把createUser从UserService中拆到UserCreationService由UserService调用userCreationService.createUser(user)代理链自然生效。6.3 final、private、static 方法为什么不能被增强这里再把这些边界条件汇总一下其实原理在前面的 CGLIB 部分已经论证过了final方法不能覆写所以增强代码没有插入点。private方法对子类不可见CGLIB 子类无法覆写。static方法是类级别的和实例代理完全无关。所以 Spring 文档里也明确建议Transactional不要写在private方法上不是 Spring 做不到而是 Java 继承机制决定了看不到这个方法的代理入口。6.4 面试官视角的高频追问清单结合最近社群里的讨论整理一份面试高频问题清单每个问题都可以用本文前面讲过的机制来组织答案。Spring 如何选择 JDK 动态代理和 CGLIB核心看proxyTargetClassSpring Boot 默认 true强制 CGLIB设为 false 时优先 JDKJDK 不可用时再降级 CGLIB。为什么 JDK 动态代理必须要求接口因为生成的代理类已经继承了java.lang.reflect.Proxy单继承限制下只能通过接口暴露方法。Spring Boot 4.0 找不到 AOP 类第一步做什么查依赖树重点看spring-aop和aspectjweaver。Transactional同类自调用为什么失效this是原始对象不是代理对象事务增强走不到。Spring 三级缓存和 AOP 有什么关系循环依赖时需要在早曝光阶段提前生成代理对象否则其他 Bean 拿到的是原始对象。动态代理对象的类型是什么JDK 代理是com.sun.proxy.$ProxyNCGLIB 代理是目标类的子类类名包含$$SpringCGLIB$$。6.5 一点实测经验在调试 AOP 相关问题时我个人的习惯是先在 IDEA 里对调用点打断点然后查看对象的实际类型。如果看到UserService$$SpringCGLIB$$0这种类名说明代理链路是正常的如果看到的是原生的UserService那就说明 Spring 容器里根本没有生成代理对象问题大概率出在依赖或配置上。另外一个体会是很多代理失效问题其实是团队编码习惯问题比如内部自调用、把Transactional写到private方法上。与其在出问题时手忙脚乱地查不如在代码规范里加一条所有需要 AOP 增强的方法都必须从外部 Bean 调用方法不能用final和private修饰。这几条规则配合动态代理原理落地之后团队里关于 AOP 的故障一下少了很多。
返回列表