详解 Spring AOP 代理模式:JDK 动态代理与 CGLIB 原理
- 发布时间:2026-05-24 02:14:59
- 本文热度:浏览 4 赞 0 评论 0
- 文章标签: Spring AOP Spring JDK动态代理
- 全文共1字,阅读约需1分钟
Spring AOP 里的“代理模式”到底代理了什么
Spring AOP 不是直接改写目标类本身,而是在目标对象外面包了一层代理对象。业务代码以为自己调用的是 UserService,实际拿到的往往是一个代理实例。方法调用先进入代理,再由代理决定要不要执行通知逻辑,比如事务、日志、权限校验、性能统计,最后再决定是否调用目标方法。
这件事如果没想清楚,后面很多现象都会显得很奇怪:
- 为什么有的类能被 AOP 增强,有的不能
- 为什么
this.xxx()调用会让切面失效 - 为什么接口代理和类代理的行为不完全一样
- 为什么
final方法经常“织”不进去
Spring AOP 的核心不是“切面语法”,而是“基于代理的拦截模型”。
Spring AOP 为什么选择代理模式
Spring AOP 的定位一直很明确:它主要解决 Spring 容器中 Bean 的方法级增强,不是一个覆盖所有场景的字节码织入方案。
它选择代理模式,有几个直接好处:
1. 不改业务代码结构
目标类不用手写代理逻辑,也不用显式继承某个基类。只要被 Spring 管理,满足代理条件,就能在运行期被增强。
2. 和 IoC 容器天然契合
Spring 在创建 Bean 时,就可以顺手判断这个 Bean 是否需要增强;如果需要,直接返回代理对象,而不是原始对象。这样 AOP 和 DI 的整合成本很低。
3. 成本可控
Spring AOP 只拦截 Spring Bean 的方法调用,范围有限,但实现简单、性能可接受,也足够覆盖事务、缓存、日志、权限这类常见企业应用场景。
但这也决定了它的边界: Spring AOP 不是全能织入。它只能拦截“通过代理对象发起的外部方法调用”。
这句话比任何切面语法都重要。
一个最小模型:代理对象是怎么工作的
先看一个简化版业务类:
@Service
public class OrderService {
public void createOrder() {
System.out.println("创建订单");
}
}
如果给它加一个切面:
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.service..*(..))")
public void before() {
System.out.println("方法执行前记录日志");
}
}
最终容器里暴露给外部的,通常不是原始 OrderService,而是一个代理对象。调用链类似这样:
proxy.createOrder()
-> Advisor / Interceptor 链
-> @Before 通知
-> 目标对象 OrderService#createOrder()
-> @After / @AfterReturning / @AfterThrowing
也就是说:
- 目标对象:真正写业务逻辑的对象
- 代理对象:外层包装器,负责在调用前后插入增强逻辑
- 通知链:一个方法可能被多个切面共同增强,Spring 会把它们组织成拦截器链
所以 AOP 的本质不是“自动执行注解里的代码”,而是“方法调用先经过代理,再按链路执行”。
Spring AOP 的两种代理方式
Spring AOP 常见的代理实现有两种:
- JDK 动态代理
- CGLIB 代理
两者都能实现 AOP,但机制不同,限制也不同。
JDK 动态代理:基于接口的代理
如果目标对象实现了接口,Spring 默认优先使用 JDK 动态代理。
它的特点
- 代理对象实现和目标对象相同的接口
- 代理类不是目标类的子类
- 方法调用通过
InvocationHandler分发
示意代码:
public interface UserService {
void save();
}
@Service
public class UserServiceImpl implements UserService {
@Override
public void save() {
System.out.println("保存用户");
}
}
JDK 代理的调用大致类似:
public class JdkProxyHandler implements InvocationHandler {
private final Object target;
public JdkProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置增强");
Object result = method.invoke(target, args);
System.out.println("后置增强");
return result;
}
}
它的关键限制
JDK 动态代理只能代理接口方法。 这意味着:
- 如果你按接口类型注入,通常没有问题
- 如果你强行按实现类类型去获取 Bean,可能出现类型不匹配
- 没有定义在接口中的方法,JDK 代理天然不擅长处理
一个常见误区
很多人以为“类实现了接口,就一定和原类一模一样”。不是。 JDK 代理出来的是一个 实现了接口的代理类,它不是 UserServiceImpl 本身。
这也是为什么某些场景下你会看到类似这种现象:
System.out.println(bean.getClass());
输出并不是你的业务类,而是类似:
class jdk.proxy2.$Proxyxx
CGLIB 代理:基于继承的代理
如果目标类没有实现接口,或者你显式指定使用类代理,Spring 会使用 CGLIB。
它的特点
- 通过生成目标类的子类来实现代理
- 重写父类方法,在重写方法中织入增强逻辑
- 不依赖接口
示意理解:
public class UserService {
public void save() {
System.out.println("保存用户");
}
}
CGLIB 生成出来的代理,本质上更像:
public class UserService$$EnhancerBySpringCGLIB extends UserService {
@Override
public void save() {
System.out.println("前置增强");
super.save();
System.out.println("后置增强");
}
}
当然,真实实现会复杂很多,但理解到这个层面已经够用了。
CGLIB 的限制更值得注意
既然是“生成子类 + 重写方法”,那就意味着:
final类不能被继承,不能代理final方法不能被重写,不能增强private方法不是可重写方法,AOP 对它无能为力static方法不属于实例方法调用链,也不在常规代理拦截范围内
这几个限制都不是 Spring 故意设计出来的,而是代理实现机制天然决定的。
Spring 到底什么时候选 JDK,什么时候选 CGLIB
默认规则通常可以概括为:
- 目标类实现了接口:默认使用 JDK 动态代理
- 目标类没有实现接口:使用 CGLIB
- 显式指定
proxyTargetClass = true:即使有接口,也使用 CGLIB
例如:
@EnableAspectJAutoProxy(proxyTargetClass = true)
或者在某些配置场景下:
spring.aop.proxy-target-class=true
该怎么选
经验上可以这样理解:
- 项目里接口设计清晰,按接口编程比较规范,JDK 动态代理完全够用
- 想统一代理行为,避免接口和实现类混用带来的差异,可以直接使用 CGLIB
- 但即使用了 CGLIB,也不要幻想它能解决所有 AOP 失效问题,
final、private、自调用这些坑依旧存在
真正容易出问题的,从来不是“选 JDK 还是 CGLIB”,而是 你有没有意识到自己调用的是代理对象。
一次完整的方法调用链长什么样
以 @Transactional 为例,很多人知道它基于 AOP,但不知道调用链到底怎么串起来。
可以把它简化成下面这样:
client
-> proxy
-> TransactionInterceptor.invoke()
-> 开启事务
-> 调用下一个拦截器 / 目标方法
-> 正常则提交
-> 异常则回滚
如果同时还有日志切面、权限切面、耗时统计切面,那么一次调用实际上会变成拦截器链:
client
-> proxy
-> 日志拦截器
-> 权限拦截器
-> 事务拦截器
-> 目标方法
所以 AOP 不是“某个注解单独生效”,而是一组拦截器围绕方法调用形成责任链。
这也是为什么切面的执行顺序会影响最终行为。 比如你希望:
- 先鉴权,避免无意义开启事务
- 再开事务,保证业务一致性
- 最后打印耗时,统计完整执行过程
这时候就需要明确切面顺序,而不是把所有 @Aspect 一股脑交给 Spring。
自调用为什么会让 AOP 失效
这是 Spring AOP 里最典型,也最容易踩坑的问题。
看一段代码:
@Service
public class UserService {
public void methodA() {
System.out.println("A 方法");
this.methodB();
}
@Transactional
public void methodB() {
System.out.println("B 方法,带事务");
}
}
很多人以为 methodA() 调用了 methodB(),所以事务会生效。实际上往往不会。
原因不在事务注解,而在调用路径
事务增强加在代理对象上。 但是 this.methodB() 是目标对象内部直接调用,不会经过代理。
也就是说:
外部调用:proxy.methodA()
进入代理没问题。可一旦执行到目标对象内部:
this.methodB()
就变成了 目标对象内部普通方法调用,绕过了代理,自然也绕过了 AOP。
这类问题最常见的表现
@Transactional不生效@Cacheable不生效- 自定义日志切面只在外部调用时触发
- 权限切面在内部调用链中失效
解决方式
方式一:拆分到另一个 Bean
这是最稳妥的做法。
@Service
public class UserService {
@Autowired
private InnerService innerService;
public void methodA() {
innerService.methodB();
}
}
方式二:通过代理对象调用自己
@EnableAspectJAutoProxy(exposeProxy = true)
public void methodA() {
((UserService) AopContext.currentProxy()).methodB();
}
这种方式能用,但不够优雅,也让业务代码和 AOP 框架耦合得更重。除非确实没法拆 Bean,否则不建议作为常规方案。
为什么 final、private 方法经常增强失败
这个问题背后其实不是 Spring 特殊处理,而是代理机制决定的。
final 方法
CGLIB 依赖子类重写方法来插入增强。 final 方法不能被重写,所以没法增强。
private 方法
private 方法不参与子类多态,也不能被外部代理正常拦截。 Spring AOP 拦截的是“可通过代理暴露的方法调用”,不是类内部所有字节码指令。
final 类
CGLIB 需要继承目标类生成代理子类。 final 类不能被继承,自然无法用 CGLIB 代理。
一个容易误判的点
有些人看到切面表达式写的是:
execution(* com.example..*(..))
就以为包下所有方法都能织进去。 不是。表达式匹配只是“逻辑上选中了哪些连接点”,真正能不能增强,还要看 代理机制能不能拦截到这个调用。
Spring AOP 和静态代理、动态代理的关系
很多文章讲 Spring AOP 时,会先讲静态代理和动态代理,这没问题,但容易讲偏。
静态代理
代理类手写出来,一个目标类往往对应一个代理类。
public class UserServiceProxy implements UserService {
private final UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
@Override
public void save() {
System.out.println("前置增强");
target.save();
System.out.println("后置增强");
}
}
缺点很明显:
- 类太多
- 代码重复
- 可维护性差
动态代理
代理类在运行期生成,不需要手写大量代理类。 Spring AOP 正是把这种能力工程化了,再叠加 IoC、切点、通知、Advisor、拦截器链这些机制,最终形成完整 AOP 体系。
所以可以这样理解:
- 静态代理:手工版
- JDK / CGLIB 动态代理:运行期生成版
- Spring AOP:在动态代理基础上封装出的企业级增强框架
Spring AOP 不是 AspectJ:这两个别混了
很多人一看到 @Aspect、@Before、@Around,就默认 Spring AOP 和 AspectJ 是一回事。语法相似,但能力边界差很多。
Spring AOP
- 基于代理
- 只拦截 Spring Bean 方法调用
- 只能处理运行期通过代理进入的调用链
AspectJ
- 可以做编译期织入或类加载期织入
- 不局限于代理对象
- 能处理构造方法、字段访问、类内部调用等更底层的连接点
所以当你遇到下面这些需求时,Spring AOP 往往不够:
- 希望拦截非 Spring 管理对象
- 希望增强构造方法
- 希望拦截类内部调用
- 希望对字段读写做增强
这时候就不是“切点表达式怎么写”的问题,而是 你选错了 AOP 技术路线。
一个更贴近实战的例子:事务为什么依赖代理
看下面的 Service:
@Service
public class AccountService {
@Transactional
public void transfer() {
deduct();
add();
}
public void deduct() {
System.out.println("扣款");
}
public void add() {
System.out.println("加款");
}
}
外部这样调用:
accountService.transfer();
如果 accountService 是代理对象,那么调用链会先进入事务拦截器,事务生效。
但如果你自己在类内部写:
public void outer() {
this.transfer();
}
那 transfer() 就很可能绕过代理,事务失效。
这也是为什么很多事务问题表面看像“数据库没回滚”,本质上其实是“根本没进事务代理”。
排查这类问题,先别急着看数据库隔离级别,先确认三件事:
- 当前拿到的是不是代理对象
- 调用有没有经过代理
- 目标方法是否满足代理拦截条件
很多问题到这里就已经定位完了。
如何确认当前 Bean 是不是代理对象
开发里排查 AOP 问题时,这几个工具方法很实用:
import org.springframework.aop.support.AopUtils;
System.out.println(AopUtils.isAopProxy(bean));
System.out.println(AopUtils.isJdkDynamicProxy(bean));
System.out.println(AopUtils.isCglibProxy(bean));
System.out.println(bean.getClass());
如果输出表明它不是代理对象,那说明:
- 切面没生效
- Bean 没被 Spring 管理
- 切点没匹配上
- 代理创建过程被绕开了
如果它是代理对象,但切面依旧没执行,优先怀疑:
- 方法是不是
final/private - 是否发生了自调用
- 切点表达式是否真的命中
- 调用时机是否发生在 Bean 初始化阶段,导致代理还没参与
Spring 创建代理对象的大致时机
从容器启动流程上看,代理通常不是在你第一次调用方法时才临时生成,而是在 Bean 创建阶段,由后置处理器决定是否包装。
关键角色通常是 BeanPostProcessor 体系,尤其是和自动代理创建相关的后置处理器。它们会在 Bean 初始化前后检查:
- 这个 Bean 是否命中切点
- 是否需要织入 Advisor
- 是否需要返回代理对象替代原始 Bean
所以从容器视角看:
- 你定义的是一个普通 Bean
- Spring 在创建过程中判断它是否应该被增强
- 最终放进容器里的,可能已经不是原始对象,而是代理对象
这也是为什么你平时 @Autowired 注入的对象,看起来是业务类,实际上拿到的是代理实例。
JDK 动态代理和 CGLIB 该怎么理解差异
真正有用的不是背结论,而是知道它们对项目设计意味着什么。
JDK 动态代理更强调接口边界
如果你的服务层天然就是接口驱动,比如:
public interface OrderService {
void create();
}
那 JDK 代理非常自然,设计上也更干净。
CGLIB 更像是兜底方案
很多项目并不会严格给每个 Service 都写接口,这时 CGLIB 更方便,Spring 也确实大量使用它。
但方便不等于没有代价。 CGLIB 是靠继承工作的,因此会受到继承模型限制,这一点一定要记住。
实际项目建议
如果团队规范明确、接口边界清晰,JDK 动态代理完全足够。 如果项目以实现类为主,或者经常需要按具体类注入,统一使用 CGLIB 更省心。
但不管用哪种代理,下面这条都不会变:
AOP 只对经过代理对象的方法调用生效。
这才是核心规则。
常见误区汇总
1. 只要加了注解,AOP 就一定生效
错。 注解只是触发增强的元信息,真正生效取决于是否经过代理调用、切点是否匹配、方法是否可拦截。
2. 类内部方法调用也会被切面拦截
通常不会。 this.xxx() 是目标对象内部直接调用,绕开代理。
3. CGLIB 可以代理一切
也不对。 final 类、final 方法、private 方法都有限制。
4. Spring AOP 可以替代 AspectJ 所有能力
不能。 Spring AOP 主要是容器级、方法级增强,不是全场景字节码织入。
5. AOP 问题主要是切点表达式写错
有时候是,但大量实战问题其实出在 代理边界没搞清楚。
写切面时更实用的几个建议
切面别贪大
日志、权限、事务、幂等、审计,不要全部揉在一个超级切面里。职责拆清楚,后面排查顺序问题才不痛苦。
不要依赖自调用触发增强
能拆到另一个 Bean,就拆出去。 这是比 AopContext.currentProxy() 更稳的方案。
对代理边界要有预期
看到 private、final、类内部调用时,第一反应就应该是: 这段代码可能根本进不了代理链。
事务问题优先查调用路径
很多所谓“事务失效”问题,最后根源都是:
- 从非 Spring 对象发起调用
- 内部自调用
- 方法可见性不对
- 异常类型不触发回滚规则
其中前两项,本质都和代理边界有关。
总结
Spring AOP 的代理模式,重点不在“代理”这个词本身,而在它建立了一条明确的调用规则:
- Spring 不直接修改目标类
- 它通过代理对象拦截方法调用
- 增强逻辑通过拦截器链织入
- 只有经过代理的外部方法调用,AOP 才能真正生效
理解了这一点,很多常见问题都会变得很直白:
- 自调用为什么失效
@Transactional为什么有时不生效- 为什么
final方法织不进去 - 为什么 JDK 代理和 CGLIB 会有差异
Spring AOP 真正难的地方,从来不是注解怎么写,而是你是否清楚: 当前这次方法调用,到底有没有经过代理。