详解 Spring AOP 代理模式:JDK 动态代理与 CGLIB 原理
- 发布时间:2026-05-22 02:21:47
- 本文热度:浏览 3 赞 0 评论 0
- 文章标签: Spring AOP Spring 代理模式
- 全文共1字,阅读约需1分钟
Spring AOP 的代理本质到底是什么
Spring AOP 并不是直接“修改目标类的字节码逻辑”来织入增强,而是在目标对象外面再包一层代理对象。业务代码实际拿到的,通常不是原始对象,而是代理对象。调用流程本质上是:
调用方 -> 代理对象 -> 拦截器链/通知逻辑 -> 目标对象方法
这也是为什么很多注解能力本质都依赖代理,例如:
@Transactional@Async@Cacheable@Validated(部分场景)- 自定义切面
@Aspect
理解 Spring AOP,核心就是理解两个问题:
- Spring 怎么创建代理对象
- 方法调用为什么有时能增强,有时又会失效
先搞清楚:Spring AOP 和 AspectJ 不是一回事
很多人一提 AOP,就默认以为是“代码插进去执行”。这在 Spring 里并不完全准确。
Spring AOP
Spring AOP 基于运行时代理实现,只对 Spring 容器管理的 Bean 生效,并且主要针对方法执行连接点。
特点:
- 基于代理
- 运行时生效
- 只支持方法级别增强
- 不会直接改目标类源码
- 集成简单,适合业务开发
AspectJ
AspectJ 支持编译期织入、类加载期织入等方式,能力更强,可以织入构造器、字段访问等更丰富的连接点。
特点:
- 不局限于代理
- 功能更强
- 配置和接入复杂度更高
所以,平时说“Spring AOP 的代理模式”,讨论的对象通常是 JDK 动态代理 和 CGLIB 代理。
Spring AOP 的两种代理方式
Spring 创建代理主要有两种方式:
- JDK 动态代理
- CGLIB 动态代理
1. JDK 动态代理
JDK 动态代理基于接口生成代理对象,因此目标类必须实现接口。
假设有一个业务接口和实现类:
public interface UserService {
void createUser();
}
@Service
public class UserServiceImpl implements UserService {
@Override
public void createUser() {
System.out.println("执行创建用户逻辑");
}
}
如果 Spring 采用 JDK 动态代理,那么容器里暴露出来的对象,实际上是一个实现了 UserService 接口的代理类实例,而不是 UserServiceImpl 原始对象。
它的特点是:
- 基于接口
- 不需要继承目标类
- 代理类由 JVM 在运行时生成
- 目标类必须实现接口
2. CGLIB 动态代理
CGLIB 通过继承目标类的方式生成子类代理,因此不要求目标类实现接口。
例如:
@Service
public class OrderService {
public void createOrder() {
System.out.println("执行下单逻辑");
}
}
如果 Spring 采用 CGLIB,那么会生成一个 OrderService 的子类代理对象,在子类中重写可代理的方法,并在方法前后织入增强逻辑。
它的特点是:
- 基于继承
- 不要求接口
- 不能代理
final类 - 不能增强
final方法 - 不能代理
private方法
Spring 到底什么时候用 JDK,什么时候用 CGLIB
这是面试和实战里最常见的问题之一。
默认规则
通常情况下:
- 目标类实现了接口:优先使用 JDK 动态代理
- 目标类没有实现接口:使用 CGLIB
强制使用 CGLIB
可以通过配置强制 Spring 使用 CGLIB:
@EnableAspectJAutoProxy(proxyTargetClass = true)
或者在 Spring Boot 配置中:
spring:
aop:
proxy-target-class: true
这表示即使目标类实现了接口,也仍然使用 CGLIB 代理。
实际开发建议
如果你是基于接口编程,JDK 动态代理通常已经够用。 如果你希望代理具体类,或者某些框架场景要求代理类本身,则会选择 CGLIB。
不过要注意一点:无论 JDK 还是 CGLIB,本质都是代理对象拦截外部调用。很多 AOP 失效问题,根因和使用哪种代理并没有直接关系,而是和“调用有没有经过代理对象”有关。
Spring AOP 的调用链到底发生了什么
以事务为例:
@Service
public class AccountService {
@Transactional
public void transfer() {
System.out.println("执行转账");
}
}
调用时并不是直接执行 transfer(),而是类似下面的逻辑:
proxy.transfer() {
// 事务开启
try {
target.transfer();
// 提交事务
} catch (Exception e) {
// 回滚事务
throw e;
}
}
这说明:
- 事务逻辑不在业务方法内部
- 而是在代理对象的方法拦截逻辑中
- 目标方法只是被“包起来”执行
所以 Spring AOP 可以理解为:把横切逻辑放进代理层,由代理统一调度目标方法调用。
一个最小可理解的代理示例
JDK 动态代理示意
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JdkProxyDemo {
interface UserService {
void createUser();
}
static class UserServiceImpl implements UserService {
@Override
public void createUser() {
System.out.println("执行 createUser");
}
}
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@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;
}
}
);
proxy.createUser();
}
}
输出结果:
方法执行前
执行 createUser
方法执行后
这段代码说明了什么
它说明代理对象做了三件事:
- 拦截方法调用
- 在目标方法前后增加通用逻辑
- 再把调用转发给真实目标对象
Spring AOP 本质上也是这套思路,只是它做得更完整,支持:
- 拦截器链
- 切点匹配
- 前置通知、后置通知、环绕通知、异常通知
- 自动创建 Bean 代理
- 与事务、缓存、异步等模块集成
Spring AOP 中常见通知类型和代理的关系
Spring AOP 常见通知包括:
@Before@After@AfterReturning@AfterThrowing@Around
其中最核心的是 @Around,因为它能完整包裹目标方法。
示例:
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.example.service..*(..))")
public Object around(org.aspectj.lang.ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try {
System.out.println("方法开始: " + joinPoint.getSignature().getName());
Object result = joinPoint.proceed();
System.out.println("方法成功结束");
return result;
} finally {
long end = System.currentTimeMillis();
System.out.println("耗时: " + (end - start) + " ms");
}
}
}
这里的 joinPoint.proceed() 可以理解成:
- 调用下一个拦截器
- 如果已经没有下一个拦截器,就调用目标方法
所以 Spring AOP 不是简单“前后打印日志”,而是维护了一条拦截器责任链。
Spring AOP 的代理对象是怎么生成的
Spring 在 Bean 初始化过程中,会有一批 BeanPostProcessor 参与处理。AOP 的关键工作,就是在 Bean 创建完成前后判断:
- 这个 Bean 是否匹配某个切面
- 是否需要创建代理
- 如果需要,就用代理对象替换原始 Bean 放入容器
也就是说,最终注入到别处的,常常不是原始 Bean,而是代理 Bean。
一个重要结论
@Autowired
private UserService userService;
这里注入进来的 userService,很多时候其实已经是代理对象。
你可以直接打印看一下:
System.out.println(userService.getClass());
可能会看到类似:
class com.sun.proxy.$ProxyXX
或者:
class com.example.service.UserService$$SpringCGLIB$$0
这就分别对应:
- JDK 动态代理
- CGLIB 代理
为什么自调用会导致 AOP 失效
这是 Spring AOP 最经典的坑。
看代码:
@Service
public class UserService {
public void methodA() {
methodB();
}
@Transactional
public void methodB() {
System.out.println("执行事务方法 methodB");
}
}
很多人以为 methodA() 调用 methodB() 时,事务会生效。实际上通常不会生效。
原因
因为 methodA() 内部调用 methodB(),调用路径是:
this.methodB()
也就是当前对象内部直接调用,没有经过 Spring 代理对象。既然没有经过代理,就没有机会进入事务拦截器,自然增强失效。
正确理解
Spring AOP 生效的前提是:
调用必须经过代理对象,而不是目标对象内部直接调用。
自调用失效怎么解决
方案一:拆分到另一个 Bean
这是最推荐的方式。
@Service
public class UserTxService {
@Transactional
public void methodB() {
System.out.println("执行事务方法 methodB");
}
}
@Service
public class UserService {
@Autowired
private UserTxService userTxService;
public void methodA() {
userTxService.methodB();
}
}
这样调用路径变成:
UserService -> 代理对象(UserTxService) -> methodB
增强可以正常生效。
方案二:获取当前代理对象再调用
需要开启代理暴露:
@EnableAspectJAutoProxy(exposeProxy = true)
然后:
@Service
public class UserService {
public void methodA() {
((UserService) AopContext.currentProxy()).methodB();
}
@Transactional
public void methodB() {
System.out.println("执行事务方法 methodB");
}
}
这个方案能用,但耦合 Spring AOP 运行环境,不适合作为常规业务设计方案。
方案三:通过容器获取自身代理 Bean
@Autowired
private ApplicationContext applicationContext;
public void methodA() {
UserService proxy = applicationContext.getBean(UserService.class);
proxy.methodB();
}
也能解决,但同样不够优雅,一般不作为首选。
哪些方法无法被 Spring AOP 正常代理
这部分很关键,尤其是 CGLIB 场景。
1. final 类不能被 CGLIB 代理
因为 CGLIB 通过继承生成子类,final 类无法被继承。
public final class OrderService {
}
这种类不能走 CGLIB 代理。
2. final 方法不能被 CGLIB 增强
因为子类无法重写 final 方法。
public class OrderService {
public final void createOrder() {
}
}
3. private 方法无法被代理增强
private 方法本质上不能被子类重写,也不会通过接口暴露,因此通常不能被 Spring AOP 拦截。
@Service
public class DemoService {
@Transactional
private void doSomething() {
}
}
这种写法事务不会生效。
4. static 方法不属于对象实例调用链
Spring AOP 面向的是 Bean 实例代理,static 方法不走对象实例代理链,因此不适合作为 AOP 增强目标。
5. 非 Spring 容器管理对象不会生效
如果对象是自己 new 出来的,而不是 Spring 容器创建和管理的,那么也不会被 Spring AOP 代理。
错误示例:
UserService userService = new UserService();
userService.methodB();
因为这个对象不是 Spring Bean,没有代理。
JDK 代理和 CGLIB 代理的核心区别
下面这个表最适合在脑子里长期记住。
| 对比项 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 代理基础 | 接口 | 继承目标类 |
| 是否要求目标类实现接口 | 要求 | 不要求 |
| 是否代理类本身 | 否,主要面向接口 | 是 |
final 类支持 |
不涉及 | 不支持 |
final 方法支持 |
不涉及接口重写 | 不支持 |
| 生成代理方式 | JVM 运行时生成接口代理类 | 生成目标类子类 |
| 典型代理类名 | com.sun.proxy.$Proxyxx |
$$SpringCGLIB$$ |
需要注意的是,表格描述的是“代理生成方式”的差异,不代表业务效果一定不同。对大多数 Spring AOP 使用者来说,更关键的问题始终是:
- 调用有没有经过代理
- 被增强的方法是否满足代理条件
- 目标对象是否由 Spring 容器管理
Spring AOP 在事务场景中的典型误区
误区一:给任意方法加 @Transactional 都能生效
不对。至少要满足:
- 方法是 Spring Bean 的公开业务方法
- 调用经过代理对象
- 事务管理器配置正常
- 异常传播方式符合回滚规则
误区二:类内部调用事务方法也能生效
不对。内部调用默认不会走代理。
误区三:private 方法加事务注解没问题
不对。通常不会生效。
误区四:自己 new 出来的对象也能享受 AOP
不对。必须由 Spring 容器托管。
代理模式在 Spring AOP 中的设计价值
Spring 选择代理模式不是偶然,而是非常符合企业应用开发诉求。
1. 对业务代码侵入小
业务方法本身不需要写事务开启、日志埋点、权限校验这些重复逻辑,交给代理统一处理即可。
2. 横切逻辑集中管理
例如日志、事务、监控、缓存、限流,本质上都属于横切关注点,放在代理层更容易统一维护。
3. 可以组合拦截器链
Spring AOP 并不是“一层代理只做一件事”,而是可以把多个通知组织成责任链,按顺序包裹目标方法。
4. 与 IoC 天然结合
Spring 本来就接管了对象创建过程,因此它最适合在 Bean 生命周期中自动决定是否生成代理。
一个完整的 Spring AOP 示例
1. 定义业务类
@Service
public class ProductService {
public void save() {
System.out.println("保存商品");
}
}
2. 定义切面
@Aspect
@Component
public class ExecutionTimeAspect {
@Around("execution(* com.example.service..*(..))")
public Object recordTime(org.aspectj.lang.ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
try {
return joinPoint.proceed();
} finally {
long end = System.nanoTime();
System.out.println(joinPoint.getSignature() + " 执行耗时: " + (end - start) + " ns");
}
}
}
3. 开启 AOP
@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
}
4. 调用业务方法
@Autowired
private ProductService productService;
public void test() {
productService.save();
}
执行流程实际上类似:
调用 test()
-> 获取到的是 ProductService 代理对象
-> 进入 around 通知
-> 执行目标方法 save()
-> 返回并打印耗时
判断一个 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));
例如:
boolean aopProxy = AopUtils.isAopProxy(productService);
boolean jdkProxy = AopUtils.isJdkDynamicProxy(productService);
boolean cglibProxy = AopUtils.isCglibProxy(productService);
这可以快速定位:
- 当前 Bean 是否被代理
- 使用的是 JDK 还是 CGLIB
Spring AOP 代理模式的几个实战建议
面向接口并不是必须,但要理解其意义
如果你的系统天然是接口驱动设计,JDK 动态代理非常自然。 如果业务大量直接注入具体类,或者没有接口,则 CGLIB 更常见。
不要把事务方法写成内部自调用结构
这是最常见的事务失效原因之一。能拆 Bean 就拆 Bean,不要依赖奇技淫巧补救。
不要在 private、final、static 方法上期待 AOP 生效
这些方法从代理机制上就不适合作为增强入口。
调试问题先看“拿到的是不是代理对象”
很多 AOP 问题不是“切面没写对”,而是根本没走代理。
明确 Spring AOP 只能处理方法执行层面的横切逻辑
如果你需要更强的织入能力,例如字段访问、构造器切入、非 Spring 管理对象织入,那就不是 Spring AOP 的适用边界了。
常见面试题可以怎么回答
问:Spring AOP 为什么基于代理模式?
因为 Spring 需要在不修改业务代码的前提下,为方法调用统一织入横切逻辑。代理模式可以在目标对象外层包装增强逻辑,把日志、事务、缓存等能力从业务中解耦出来。
问:Spring AOP 默认用哪种代理?
通常是:
- 有接口优先 JDK 动态代理
- 无接口使用 CGLIB
也可以通过配置强制使用 CGLIB。
问:为什么事务有时失效?
根因通常是调用没有经过代理对象。典型场景包括:
- 同类内部自调用
private方法final方法- 非 Spring Bean
- 直接
new对象调用
问:Spring AOP 和 AspectJ 的区别是什么?
Spring AOP 基于运行时代理,只支持方法执行连接点,集成简单。AspectJ 支持更强的织入能力,不局限于代理。
总结
Spring AOP 的核心不是“切面语法”,而是代理模式。
只要抓住这几点,很多问题就能迅速想明白:
- Spring AOP 通过代理对象而不是原始对象织入增强
- 代理方式主要有 JDK 动态代理和 CGLIB
- 增强能否生效,关键看调用是否经过代理
- 自调用、
private、final、非 Spring Bean 是最常见失效场景 @Transactional、@Async、@Cacheable等能力,本质都依赖这套代理机制
真正理解 Spring AOP,重点不在记注解,而在于脑子里始终有这条调用链:
调用方 -> 代理对象 -> 增强逻辑 -> 目标对象
一旦把这条链路想清楚,Spring AOP 的大部分原理、限制和实战问题都会变得非常直观。