☰
Spring代理模式全解析:从静态代理到CGLIB与三级缓存
2026/10/5 11:59:59 网站建设 项目流程

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 的核心

先别看那些高大上的@Aspect,Spring 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 初始化之后。

整个流程是这样的:

  1. Spring 容器创建 Bean,正常走实例化、属性填充、初始化回调。
  2. 初始化完成后,容器会调用所有BeanPostProcessor的postProcessAfterInitialization方法。
  3. AbstractAutoProxyCreator在这个方法内部调用wrapIfNecessary。
  4. wrapIfNecessary会拿着当前 Bean 去匹配所有已经注册的 Advisor(切面通知器)。
  5. 如果匹配上了,就调用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 依赖 B,B 又依赖 A,在创建 A 的时候需要先有 B,而创建 B 的时候又需要有 A,两边死锁了就转不动了。

Spring 解决这个问题的思路很直接:先创建 A 的早期引用(半成品对象,还没完成属性填充和初始化),把早期引用存起来,然后继续创建 B;B 拿到 A 的早期引用完成自己的创建,最后再回来填充 A,让 A 走完完整的初始化流程。

这三个缓存分别是:

  • 一级缓存singletonObjects:存放已经完整初始化好的单例 Bean。
  • 二级缓存earlySingletonObjects:存放刚实例化但还没完成属性填充的早期 Bean。
  • 三级缓存singletonFactories:存放ObjectFactory工厂对象,用来在需要时生成早期引用。

这里每个缓存承担的角色不一样,最精髓的是三级缓存,它里面存的是一个 lambda 表达式:

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

5.2 早期引用如何解决循环依赖

画个流程就清楚了,还是 A 依赖 B、B 依赖 A:

  1. 创建 A,实例化得到原始对象a。
  2. 把a封装成一个ObjectFactory放入三级缓存。
  3. 开始给 A 填充属性 B,但是 B 还没创建,于是去创建 B。
  4. 创建 B,实例化得到原始对象b。
  5. 把b的ObjectFactory放入三级缓存。
  6. 填充 B 的属性 A,此时检查三级缓存,发现已经有 A 的ObjectFactory,于是调用它的getEarlyBeanReference(),拿到 A 的早期引用。
  7. B 拿到 A 的早期引用,完成自己的实例化和初始化,最后放入一级缓存。
  8. 回到 A 的创建流程,把刚才填充到 B 里的那个 A 的早期引用作为 A 的属性值(实际上 B 里持有的就是 A 的早期引用,之后不会再变)。
  9. 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类和InvocationHandler;CGLIB 通过继承目标类生成子类来代理,不能代理 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.$Proxy17;CGLIB 代理类名会有$$EnhancerBySpringCGLIB$$,比如com.example.UserService$$EnhancerBySpringCGLIB$$abc123。这是排查业务代码里“为什么强转会炸”的利器。

6.2 实战排坑记录

我在实际项目里遇到过一个特别有意思的问题:一个@Service类里,方法 A 调用了方法 B,B 上有@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 拦截器,写一遍比看十遍源码都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询