ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring代理模式全解析:从静态代理到CGLIB与三级缓存

Spring代理模式全解析:从静态代理到CGLIB与三级缓存 Spring 的很多黑魔法本质上都是代理模式在支撑。很多人刚开始接触 Spring 的时候被 IOC、AOP、动态代理、CGLIB、三级缓存这些词砸得头晕面试时一旦被问到“Spring AOP 底层怎么实现的”就卡壳。其实把这些概念剥开核心就是一句话代理模式给目标对象加了一层壳Spring 在合适的时机把这层壳生成出来并塞进容器。这篇文章就是带着你从最原始的静态代理开始一层层走到动态代理最后把 Spring AOP 和三级缓存的底层逻辑串起来。无论你是刚学 Spring 的初学者还是准备面试的开发者甚至已经在项目里用它做二次开发的工程师看完这篇至少能应对 90% 和 Spring 代理原理有关的提问。1. Spring 与代理模式一切增强的起点1.1 代理模式到底在解决什么问题代理模式本身不是什么高端玩意它就是一个中间层。你想买 iPhone不想跑华强北于是找代购代购去买了手机再转交给你你在过程中还顺便获得了一个“正品保证”的附加服务。放在代码里代理对象和被代理对象实现同一个接口客户端真正调用的其实是代理对象而代理对象内部再去调用真正的目标对象并且可以在调用前后插入额外的逻辑。这就是代理模式最朴素的定义。这个模式解决的核心问题有三个控制访问比如权限校验只有符合条件的用户才能执行目标方法。增强功能在目标方法执行前后加日志、做缓存、做事务但又不污染目标类的代码。降低耦合目标类不需要关心上游有没有额外需求代理层把横切逻辑集中管理。你去看 Spring 框架本身Transactional注解为什么方法执行完自动回滚Async为什么一个注解方法就变成异步执行Cacheable为什么能自动命中和写入缓存这些统统不是业务代码自己实现的而是 Spring 通过代理对象在方法调用链路上插入了一个拦截器。所以不理解代理模式你对 Spring 的所有认知都只能浮在表面。1.2 Spring 框架里到底哪些地方用了代理很多人以为代理只是 AOP 模块的事情这么想格局小了。Spring 里用到代理的地方比想象中多得多我随手列几个声明式事务Transactional的底层就是TransactionInterceptor配合动态代理。异步调用Async通过AsyncAnnotationBeanPostProcessor为 Bean 生成代理。缓存管理Cacheable、CacheEvict基于CacheInterceptor实现。安全框架Spring Security 的PreAuthorize也依赖 AOP 机制。远程调用Spring Cloud 中的OpenFeign客户端本质就是动态代理帮你把接口方法转成 HTTP 请求。可以说Spring 的“魔法”一半在 IOC 容器另一半全在代理机制里。当你理解了代理模式这条主线再回看这些高阶特性就会发现它们不过是同一个套路的不同场景应用。2. 静态代理从代码层面理解“增强”2.1 一个租房中介的经典例子直接讲动态代理很多人会懵所以我们先从静态代理入手。静态代理的意思是代理类是我们开发人员在编译期手动写好的一个目标类对应一个代理类代码是写死的。举个业务场景你有一个IUserService里面有一个saveUser方法现在你想在保存用户之前打印一条日志在保存之后也打印一条日志。最简单粗暴的方式是改UserServiceImpl但那样会污染业务代码。用静态代理的方式我们单独写一个代理类public interface IUserService { void saveUser(User user); } public class UserServiceImpl implements IUserService { Override public void saveUser(User user) { System.out.println(真正保存用户: user.getName()); } } public class UserServiceProxy implements IUserService { private final IUserService target; public UserServiceProxy(IUserService target) { this.target target; } Override public void saveUser(User user) { System.out.println( 保存用户开始 ); target.saveUser(user); System.out.println( 保存用户结束 ); } } // 客户端使用 public class StaticProxyTest { public static void main(String[] args) { IUserService userService new UserServiceProxy(new UserServiceImpl()); userService.saveUser(new User(张三)); } }你看客户端拿到的其实是UserServiceProxy但它对调用方完全透明接口还是IUserService。UserServiceImpl自己完全没有改动却被附加了日志能力这就是静态代理的工作方式。2.2 静态代理的致命短板静态代理好理解但在真实企业级项目里你用不起来。只有一个saveUser还好如果你的接口有几十个方法代理类就得把每个方法都转调一遍还得在各个方法里穿插增强逻辑代码量直接爆炸。更麻烦的是如果你要增加一个方法不仅目标类要加代理类也要加维护成本翻倍。而且接口一旦变化所有代理类都得跟着改。后来你意识到日志这种逻辑可能会在IUserService、IOrderService、IProductService等所有服务上生效那就意味着每个接口都要配一个代理类而这些代理类内部的增强逻辑几乎一模一样大量重复。静态代理还有一个隐蔽的问题代理类是在编译期就写死的如果目标对象不是我们自己写的类比如我们要增强一个第三方 Jar 包里的类你怎么写静态代理你连源码都没有。这个问题直接催生了动态代理。2.3 静态代理在 Spring 中的“残留痕迹”其实 Spring 本身也有一些静态代理的类似设计比如早期的ProxyFactoryBean里配置proxyInterfaces但那种方式慢慢被动态代理取代了。现在如果你去写一些老项目还会看到xxxProxy这种类那就是静态代理的思路。静态代理这个知识点现在最大的价值就是帮你建立“代理层”这个心智模型让你明白动态代理到底做了什么优化——把“代理类”从手动编写变成了运行时自动生成。3. 动态代理JDK Proxy 与 CGLIB 的相爱相杀3.1 JDK 动态代理接口驱动动态代理是 Java 反射包的拿手好戏核心是java.lang.reflect.Proxy类。它可以在运行时动态地创建一个实现了一组接口的代理类你只需要提供一个InvocationHandler所有接口方法的调用都会集中转发到handler.invoke()方法里。还是用刚才的例子public class LogHandler implements InvocationHandler { private final Object target; public LogHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println( 方法开始: method.getName() ); Object result method.invoke(target, args); System.out.println( 方法结束: method.getName() ); return result; } } public class JdkProxyTest { public static void main(String[] args) { IUserService target new UserServiceImpl(); IUserService proxy (IUserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogHandler(target) ); proxy.saveUser(new User(李四)); } }JDK 动态代理生成的字节码里面$Proxy0这个类实现了你传入的所有接口并且把每个接口方法都对应到InvocationHandler.invoke()上。这样一来不管你接口里有多少个方法逻辑只需要在invoke里写一份就够了。相比静态代理代码量少了不是一点半点。但这里有个硬性前提目标对象必须实现了至少一个接口。如果你的业务类压根没有接口比如直接public class UserService {}JDK 动态代理就无能为力了因为Proxy只能代理接口。这时候就需要 CGLIB 登场。3.2 CGLIB 动态代理继承改写CGLIB 的底层是字节码操作框架 ASM它通过继承目标类并覆盖非 final 方法来生成一个代理子类。代理子类在调用父类方法前后插入增强逻辑。代码写起来如下public class UserService { public void saveUser(User user) { System.out.println(真正保存用户: user.getName()); } } public class CglibProxyFactory implements MethodInterceptor { public static Object createProxy(Class? targetClass) { Enhancer enhancer new Enhancer(); enhancer.setSuperclass(targetClass); enhancer.setCallback(new CglibProxyFactory()); return enhancer.create(); } Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println( CGLIB 开始 ); Object result proxy.invokeSuper(obj, args); System.out.println( CGLIB 结束 ); return result; } }注意这里用的是proxy.invokeSuper(obj, args)意思是直接调用父类即目标类的这个方法而不是method.invoke(obj, args)。因为obj是代理子类的实例如果再用反射去调用方法会有再次走代理逻辑的风险导致死循环。用MethodProxy直接调用父类实现可以绕开代理。CGLIB 能代理没有接口的普通类但它也有自己的局限final 方法不能覆盖final 类不能代理。另外生成子类相比 JDK 代理要重一点性能开销稍大不过现在 Spring 在处理上有优化而且两种代理方式在调用性能上差异已经很小了。3.3 为什么 Spring 优先使用 JDK 动态代理这是面试高频题。Spring 的老版本比如 Spring Boot 1.x 之前的默认行为在没有接口时会用 CGLIB有接口时会用 JDK 动态代理。后来 Spring Boot 2.x 把proxyTargetClass默认值设成了true默认就全走 CGLIB。为什么做了这个调整我结合自己的踩坑经验说几个原因不需要强制接口很多项目里的 Service 类压根就不写接口JDK 代理直接没法用。CGLIB 作为默认策略能统一处理这种情况。避免强制类型转换问题JDK 动态代理生成的代理对象没有继承目标类如果你在代码里强行把代理对象转成具体类比如(UserServiceImpl) userServiceProxy会抛ClassCastException。而 CGLIB 生成的代理对象是目标类的子类强转不会炸。Spring 官方也推荐Boot 2.x 开始官方文档默认建议采用proxyTargetClass true因为大多数场景下我们并不需要接口而且 Java 配置类Configuration类在 CGLIB 代理下能够保证Bean方法内的单例语义下面会提。但注意JDK 动态代理并不是被淘汰了它依靠接口更简洁且没有 CGLIB 那样的final限制在很多中间件比如 MyBatis MApper 代理、Feign 客户端里依然是绝对主力。Spring 只是在默认策略上倾斜了 CGLIB实际两者同时在用。4. Spring AOP 底层实现从 ProxyFactory 到自动代理4.1 AOP 的核心概念先理一遍说到 Spring AOP 的底层不能不先过一遍 AOP 术语切面Aspect、通知Advice、连接点JoinPoint、切入点Pointcut。我用大白话解释接入点目标对象里可以被增强的地方比如某个业务方法的执行前后。切入点你要在哪些方法上增强通过表达式来匹配比如execution(* com.example.service.*.*(..))。通知增强逻辑本身比如“打印日志”这个动作分为前置Before、后置After、环绕Around、异常AfterThrowing、返回AfterReturning。切面把这些通知和切入点组合在一起的模块通常是一个带有Aspect注解的类。Spring AOP 并不是像 AspectJ 那样通过修改字节码织入来实现的它本质上是运行时动态代理。Spring 容器拿到 Bean 后发现这个 Bean 上有需要增强的通知就会用代理对象替换掉原始 Bean 放进容器所以你从容器里拿到的对象往往已经不是原来的那个实例了。4.2 ProxyFactory编程式 AOP 的核心先别看那些高大上的AspectSpring AOP 最底层的编程式入口是ProxyFactory。它内部会根据条件选择 JDK 动态代理还是 CGLIB。我们来看一段最小化实现public class ProxyFactoryDemo { public static void main(String[] args) { UserService target new UserService(); ProxyFactory factory new ProxyFactory(); factory.setTarget(target); factory.addAdvice(new MethodInterceptor() { Override public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println( 前置通知 ); Object result invocation.proceed(); System.out.println( 后置通知 ); return result; } }); // 获取代理对象 UserService proxy (UserService) factory.getProxy(); proxy.saveUser(new User(王五)); } }ProxyFactory里最重要的方法是getProxy()它内部调用createAopProxy()去创建一个AopProxy而这层决策就封装在DefaultAopProxyFactory中。源码逻辑大致如下public AopProxy createAopProxy(AdvisedSupport config) { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class? targetClass config.getTargetClass(); if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); } return new JdkDynamicAopProxy(config); }简单解读一下如果满足条件默认配置且目标类不是接口就会优先用 CGLIB否则用 JDK 动态代理。其中hasNoUserSuppliedProxyInterfaces的意思是用户没手写指定代理接口Spring Boot 默认就把proxyTargetClass设为 true 了所以通常都进入 CGLIB 分支。4.3 自动代理创建器BeanPostProcessor 的威力ProxyFactory是编程式用法而我们在实际开发中几乎没有直接写过它。因为 Spring 提供了一个自动代理机制核心是AbstractAutoProxyCreator它实现了BeanPostProcessor接口。这里要强调一个关键点Spring AOP 的自动代理并不是在 Bean 实例化过程中完成的而是在 Bean 初始化之后。整个流程是这样的Spring 容器创建 Bean正常走实例化、属性填充、初始化回调。初始化完成后容器会调用所有BeanPostProcessor的postProcessAfterInitialization方法。AbstractAutoProxyCreator在这个方法内部调用wrapIfNecessary。wrapIfNecessary会拿着当前 Bean 去匹配所有已经注册的 Advisor切面通知器。如果匹配上了就调用createProxy创建代理对象并返回替换如果没有匹配到就返回原 Bean。源码里这一段的逻辑核心方法签名是public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean ! null) { return wrapIfNecessary(bean, beanName, cacheKey); } return bean; }wrapIfNecessary里又调用了getAdvicesAndAdvisorsForBean去查找有哪一个 Advisor 跟当前 Bean 匹配然后再走createProxy。这个过程中AnnotationAwareAspectJAutoProxyCreator是 Spring Boot 默认注册的自动代理创建器它能识别Aspect注解的类并把里面的Before、Around等通知方法解析成对应的 Advisor。4.4 EnableAspectJAutoProxy 和 Transactional 背后的代理当你引入了spring-boot-starter-aop并写了Aspect类Spring 就会通过EnableAspectJAutoProxy自动把AnnotationAwareAspectJAutoProxyCreator注册进容器。哪怕你不写这个注解Spring Boot 的自动配置也会在检测到Aspect类的时候帮你加一个代理创建器。Transactional用的也是同一套机制只不过它识别的是Transactional注解。具体实现里TransactionInterceptor在invoke方法中先拿到事务管理器然后决定commit还是rollback。当你在一个类里写Transactional方法时容器中装进去的实际上已经是那个类的代理对象。这就引出一个经典坑同一个类内部方法调用比如this.foo()调用同类的bar()bar()上的Transactional不会生效。原因是外部调用才经过代理对象内部this指向的是原始目标对象自然就没法触发代理逻辑。解决办法之一是用AopContext.currentProxy()或者把内部调用抽到另一个 Bean 里。这个我们放到后面排查章节细说。5. 三级缓存与代理的相爱相杀循环依赖中的“早引用”5.1 为什么会有三级缓存Spring 容器里有一个很著名的东西叫做“三级缓存”。先说结论三级缓存是为了解决单例 Bean 的循环依赖问题。什么叫循环依赖比如 A 依赖 BB 又依赖 A在创建 A 的时候需要先有 B而创建 B 的时候又需要有 A两边死锁了就转不动了。Spring 解决这个问题的思路很直接先创建 A 的早期引用半成品对象还没完成属性填充和初始化把早期引用存起来然后继续创建 BB 拿到 A 的早期引用完成自己的创建最后再回来填充 A让 A 走完完整的初始化流程。这三个缓存分别是一级缓存singletonObjects存放已经完整初始化好的单例 Bean。二级缓存earlySingletonObjects存放刚实例化但还没完成属性填充的早期 Bean。三级缓存singletonFactories存放ObjectFactory工厂对象用来在需要时生成早期引用。这里每个缓存承担的角色不一样最精髓的是三级缓存它里面存的是一个 lambda 表达式addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean));5.2 早期引用如何解决循环依赖画个流程就清楚了还是 A 依赖 B、B 依赖 A创建 A实例化得到原始对象a。把a封装成一个ObjectFactory放入三级缓存。开始给 A 填充属性 B但是 B 还没创建于是去创建 B。创建 B实例化得到原始对象b。把b的ObjectFactory放入三级缓存。填充 B 的属性 A此时检查三级缓存发现已经有 A 的ObjectFactory于是调用它的getEarlyBeanReference()拿到 A 的早期引用。B 拿到 A 的早期引用完成自己的实例化和初始化最后放入一级缓存。回到 A 的创建流程把刚才填充到 B 里的那个 A 的早期引用作为 A 的属性值实际上 B 里持有的就是 A 的早期引用之后不会再变。A 继续完成初始化最终放入一级缓存。这里面关键点在于B 中引用的 A 和最开始的 A 是同一个引用但因为代理的存在事情变得稍微复杂。5.3 代理对象在三级缓存中的特殊处理如果 A 需要被 AOP 代理那么在第六步调用getEarlyBeanReference()时Spring 会检查 A 是否已经暴露了SmartInstantiationAwareBeanPostProcessor其中有一个方法getEarlyBeanReference。AbstractAutoProxyCreator重写了这个方法如果它判断当前 Bean 需要被代理就会直接返回一个代理对象作为早期引用。也就是说三级缓存 代理机制保证了即使还有属性没有填完半成品的 A 也已经以代理对象的形态暴露给了 B。后面在第九步A 走完初始化后postProcessAfterInitialization再次调用wrapIfNecessary这时候会发现earlyProxyReferences里已经存在 A 的记录说明早期引用已经代理过了就不会再重复代理。这里可能有人会问为什么要三级缓存而不是一级因为第三级缓存存的不是对象本身而是一个可以“延迟创建代理”的工厂。如果对象没有循环依赖那么永远不需要提前生成代理也就不用提前调用createProxy这样可以避免无谓的代理开销。这就是把普通对象缓存和代理对象生成逻辑分离开来的好处。举一个实际对比假如 A 和 C 没有循环依赖A 在正常初始化完成后才被BeanPostProcessor代理那么 A 被放入一级缓存时已经是代理对象了。但如果 A 存在循环依赖B 可能会在 A 的早期引用阶段就去拿它这时如果三级缓存里存的只是原始对象B 持有的就是一个未被代理的裸 A后来 A 的代理对象和早期引用不一致就会出大问题。所以三级缓存的ObjectFactory就是用来解决“早期引用需要的就是最终被代理后的对象”这一难题的。6. 面试与实战高频问题与排查实录6.1 高频面试题深度剖析问题一JDK 动态代理和 CGLIB 动态代理有什么区别答案要点JDK 动态代理只能代理接口基于Proxy类和InvocationHandlerCGLIB 通过继承目标类生成子类来代理不能代理 final 方法。JDK 原生反射CGLIB 字节码操作早期 CGLIB 生成代理类较慢但调用性能稍好现在两者调用性能差距不大。Spring Boot 2.x 默认使用 CGLIB。问题二Spring AOP 的代理对象是在什么时候创建的答案要点不是类加载时而是在 Bean 初始化完成后由AbstractAutoProxyCreator.postProcessAfterInitialization触发调用wrapIfNecessary匹配 Advisor再调用createProxy创建代理对象。如果 Bean 存在循环依赖则可能提前到三级缓存的getEarlyBeanReference阶段创建代理。问题三Spring 三级缓存为什么是三级不是两级这是一个非常有深度的提问。答案关键在于第三级缓存存的是ObjectFactory用来延迟创建代理。如果提前到二级缓存就把代理创建好那所有 Bean 都得无条件创建代理性能受损。三级缓存的写法让只有真正发生循环依赖时才调用getEarlyBeanReference没有循环依赖的 Bean 走正常流程。问题四Transactional 为什么有时候不生效最常见的两个原因方法不是 public因为 Spring AOP 基于代理CGLIB 代理无法覆盖 private 方法JDK 动态代理接口方法也默认是 public。同类内部调用this.method()不会经过代理对象。解决方式注入自身的代理 Bean 或者使用AopContext.currentProxy()。还有一个原因是事务管理器和数据源没匹配上或者传播行为配置错误但这些超出了代理范围这里不展开。问题五代理对象的标识怎么判断到底走了 JDK 还是 CGLIB可以断点看代理对象的类名。JDK 代理类名里会有$Proxy比如com.sun.proxy.$Proxy17CGLIB 代理类名会有$$EnhancerBySpringCGLIB$$比如com.example.UserService$$EnhancerBySpringCGLIB$$abc123。这是排查业务代码里“为什么强转会炸”的利器。6.2 实战排坑记录我在实际项目里遇到过一个特别有意思的问题一个Service类里方法 A 调用了方法 BB 上有Async结果 B 每次都还是会同步执行。查了半天发现方法 A 和 B 都在同一个类里this调用导致Async拦截器没走。后来我在类里注入了Lazy SelfService selfService用selfService.b()去调用问题就解决了。这就是典型的“代理对象不自见”陷阱。还有一个容易踩的坑Configuration类配合Scope和代理模式。当你在Configuration类中使用 CGLIB 增强Spring 默认对配置类是 CGLIB 代理的Bean方法内部的直接调用会被拦截从而保证单例语义。但如果你把Configuration改成Component配置类就不再被 CGLIB 代理Bean方法如果互相调用可能产生多个实例造成作用域混乱。所以不要随便把Configuration换成Component。另外当你手写工具类想统一打印 Controller 的入参与出参时建议优先用 AOP 的Around而不是在每个接口里手动打日志。但注意AOP 切入的是 Spring 管理的 Bean 方法如果是 Controller 里的 private 方法或者通过new出来的对象方法AOP 是不会生效的。最后分享一个排查思路如果一个业务方法没有按预期执行切面逻辑第一反应不是去看切面表达式写没写对而是确认你调用的对象是不是代理对象。最快的方法就是打断点看类名看到$$EnhancerBySpringCGLIB$$就说明是代理对象看到纯原类名就说明调用方拿到的不是代理。如果调用方是从容器里Autowired进来的通常没问题如果是通过new创建的那百分百是原生对象。我在实际工作中发现很多人学了 Spring 好几年但对代理模式的理解还是停在“会用Aspect”的层面一旦遇到内部调用失效、循环依赖、代理类型选择这种细节就抓瞎。其实只要你把静态代理、JDK 动态代理、CGLIB 三种形态理清楚再顺着BeanPostProcessor这条线看 Spring 源码真的一点都不难。遇到问题也别慌先判断“调用对象是不是代理”再判断“代理是什么时候生成的”这两个问题想明白了剩下的基本都是表达式的锅。最后建议你自己动手不用框架手写一个ProxyFactory或者简单的 AOP 拦截器写一遍比看十遍源码都管用。
返回列表