@Autowired 注入的是代理对象?Spring AOP 代理选择原理一次讲透
你有没有遇到过这些诡异现象:
- 同一个类里方法 A 调方法 B,B 上的
@Transactional怎么都不生效?@Autowired注入的对象,debug 发现是个$Proxy开头的类?- 有些类用 JDK 动态代理,有些用 CGLIB,Spring 到底怎么选的?
今天把 Spring AOP 代理选择的底层逻辑拆清楚------不仅是"面试八股",更是排查这类 Bug 的必备知识。
一、两种代理:JDK 动态代理 vs CGLIB
Spring AOP 的底层就是代理。不管你加 @Transactional、@Async、@Cacheable 还是自定义切面,最终都是给 Bean 包一层代理。
1.1 JDK 动态代理
php
JDK 动态代理
│
├── 原理:java.lang.reflect.Proxy
│
├── 条件:目标类必须实现至少一个接口
│
├── 生成:运行时创建接口的实现类
│ └── $Proxy0 implements UserService { ... }
│
├── 限制:只能代理接口方法,类方法代理不到
│
└── 性能:JDK 8+ 已大幅优化,不再慢于 CGLIB
java
// JDK 动态代理核心代码
Object proxy = Proxy.newProxyInstance(
targetClass.getClassLoader(),
targetClass.getInterfaces(), // 基于接口
invocationHandler
);
1.2 CGLIB 代理
swift
CGLIB 代理
│
├── 原理:字节码生成,创建目标类的子类
│
├── 条件:目标类不能是 final,方法不能是 final/private
│
├── 生成:运行时创建目标类的子类
│ └── TargetClass$$EnhancerBySpringCGLIB$$xxx extends TargetClass { ... }
│
├── 优势:不依赖接口,能代理类方法
│
└── 依赖:spring-boot-starter 内置 cglib,无需额外引入
java
// CGLIB 核心代码(简化)
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetClass); // 基于类继承
enhancer.setCallback(methodInterceptor);
Object proxy = enhancer.create();
1.3 对比表
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 代理方式 | 基于接口 | 基于类继承 |
| 要求 | 必须实现接口 | 类不能 final |
| 代理范围 | 只代理接口方法 | 代理所有非 final 方法 |
| 生成的类 | $Proxy0 implements Xxx |
Xxx$$EnhancerByCGLIB$$ |
| Spring 默认(2.x) | 优先使用 | 接口不够时降级 |
| Spring 默认(3.x) | 已废弃 | 默认只用 CGLIB |
二、Spring 的代理选择规则
2.1 Spring Boot 2.x 的规则
swift
Spring Boot 2.x 代理选择流程
│
├── 目标类实现了接口?
│ ├── 是 → 使用 JDK 动态代理
│ └── 否 → 使用 CGLIB
│
└── 注意:即使你只加了一个 @Transactional,只要实现了接口,
Spring 默认就给你用 JDK 代理!
这就是踩坑的根源:你写了一个 Service 实现了接口,但切面想拦截的是实现类的方法(不是接口方法),JDK 代理代理不到。
2.2 Spring Boot 3.x 的规则
objectivec
Spring Boot 3.x 代理选择流程
│
├── 默认:spring.aop.proxy-target-class=true
│
└── 全部使用 CGLIB,不管有没有接口
│
└── 原因:JDK 代理的"接口限制"让太多人踩坑,
Spring 团队干脆默认用 CGLIB
2.3 手动控制
yaml
# application.yml
spring:
aop:
proxy-target-class: true # true=CGLIB, false=JDK(仅对接口生效)
java
// 或者用注解
@EnableAspectJAutoProxy(proxyTargetClass = true) // 强制 CGLIB
三、为什么"同类方法调用"切面不生效
这是 Spring AOP 最经典的问题:
java
@Service
public class OrderService {
public void placeOrder(Order order) {
// 内部调用,this 不是代理对象!
this.validateOrder(order); // @Transactional 不生效!
// ... 下单逻辑
}
@Transactional
public void validateOrder(Order order) {
// 事务注解无效------因为绕过了代理
}
}
3.1 原因图解
scss
外部调用(经过代理):
Controller → Proxy.validateOrder() → 切面拦截 → 开启事务 → Target.validateOrder()
✅ 事务生效
内部调用(绕过代理):
placeOrder() → this.validateOrder()
↑
this 是原始对象,不是代理!
切面根本没机会介入
❌ 事务不生效
3.2 解决方案
方案 1:自注入(推荐)
java
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入代理对象
public void placeOrder(Order order) {
self.validateOrder(order); // 通过代理调用
// ...
}
@Transactional
public void validateOrder(Order order) {
// ✅ 事务生效
}
}
方案 2:AopContext 获取当前代理
java
@Service
public class OrderService {
public void placeOrder(Order order) {
// 从 AopContext 获取当前代理
((OrderService) AopContext.currentProxy()).validateOrder(order);
}
@Transactional
public void validateOrder(Order order) {
// ✅ 事务生效
}
}
需要
@EnableAspectJAutoProxy(exposeProxy = true)开启。
方案 3:拆成两个类(最干净)
java
@Service
public class OrderService {
@Autowired
private OrderValidator validator;
public void placeOrder(Order order) {
validator.validateOrder(order); // 跨 Bean 调用,天然经过代理
// ...
}
}
@Service
public class OrderValidator {
@Transactional
public void validateOrder(Order order) {
// ✅ 事务生效
}
}
| 方案 | 优点 | 缺点 |
|---|---|---|
| 自注入 | 最简单 | 循环依赖风险(Spring 3.x 已解决) |
| AopContext | 不需要额外字段 | 需要配置 exposeProxy |
| 拆类 | 职责清晰,最好维护 | 多一个类 |
四、代理对象的创建过程
Spring 在 Bean 生命周期中的哪一步创建代理?
swift
Bean 生命周期(简化)
│
├── 1. 实例化(createBeanInstance)
│
├── 2. 属性注入(populateBean)
│
├── 3. 初始化(initializeBean)
│ │
│ ├── 3.1 BeanPostProcessor.before
│ │
│ ├── 3.2 InitializingBean.afterPropertiesSet
│ │
│ ├── 3.3 自定义 init-method
│ │
│ └── 3.4 BeanPostProcessor.after ← 代理在这里创建!
│ │
│ └── AbstractAutoProxyCreator.postProcessAfterInitialization()
│ │
│ ├── 检查是否有切面匹配该 Bean
│ ├── 匹配 → 创建代理(JDK 或 CGLIB)
│ └── 不匹配 → 返回原始 Bean
│
└── 4. 放入容器(是代理对象,不是原始对象)
关键点:代理是在 Bean 初始化的最后阶段创建的。这意味着:
- 构造函数里拿到的
this不是代理 @PostConstruct里拿到的this也不是代理- 只有其他 Bean 通过
@Autowired注入你时,拿到的是代理
五、JDK 代理 vs CGLIB:性能与实战差异
5.1 性能对比
| 操作 | JDK 8+ | CGLIB |
|---|---|---|
| 代理创建 | 较快 | 较慢(生成字节码) |
| 方法调用 | 快(已优化) | 快 |
| 内存占用 | 低 | 较高(生成子类) |
结论:JDK 8+ 后性能差异可忽略。选 CGLIB 不是因为性能,而是因为"功能"------CGLIB 能代理类方法,JDK 不行。
5.2 实战差异:接口方法 vs 实现类方法
java
public interface UserService {
void addUser(); // 接口方法
}
@Service
public class UserServiceImpl implements UserService {
@Override
public void addUser() { ... } // 实现接口方法
public void validateUser() { ... } // 类方法,不在接口中
}
scss
JDK 代理行为:
├── addUser() → ✅ 被代理(接口方法)
└── validateUser() → ❌ 调不到!代理对象根本没有这个方法
CGLIB 代理行为:
├── addUser() → ✅ 被代理
└── validateUser() → ✅ 被代理(子类继承了父类方法)
这就是为什么 Spring Boot 3.x 默认 CGLIB------它不会漏掉类方法。
六、常见踩坑与排查
坑 1:@Transactional 标在 private 方法上
java
@Transactional // ❌ 无效!代理无法拦截 private 方法
private void updateOrder() { ... }
原因:不管是 JDK 还是 CGLIB,都代理不到 private 方法。CGLIB 通过子类覆盖父类方法实现拦截,private 方法无法被覆盖。
坑 2:final 方法上注解不生效
java
@Transactional
public final void processPayment() { ... } // ❌ CGLIB 无法覆盖 final 方法
坑 3:接口方法没有声明异常,实现类 throws 了
java
public interface OrderService {
void placeOrder(); // 没有 throws
}
@Service
public class OrderServiceImpl implements OrderService {
@Transactional(rollbackFor = Exception.class)
@Override
public void placeOrder() throws BusinessException { ... }
}
注意:JDK 代理在生成接口实现时,方法签名必须和接口一致。如果接口没声明 throws,代理方法也不能 throws,可能导致运行时异常。
排查工具:判断当前是不是代理对象
java
@Autowired
private UserService userService;
@PostConstruct
public void check() {
System.out.println("Is AOP Proxy: " + AopUtils.isAopProxy(userService));
System.out.println("Is JDK Proxy: " + AopUtils.isJdkDynamicProxy(userService));
System.out.println("Is CGLIB Proxy: " + AopUtils.isCglibProxy(userService));
System.out.println("Actual class: " + userService.getClass().getName());
}
输出示例:
vbnet
Is AOP Proxy: true
Is JDK Proxy: false
Is CGLIB Proxy: true
Actual class: com.example.UserService$$EnhancerBySpringCGLIB$$a1b2c3d4
七、Spring Boot 3.x 迁移注意事项
如果你的项目从 2.x 升级到 3.x:
| 变化点 | 2.x | 3.x |
|---|---|---|
| 默认代理 | 有接口→JDK,无接口→CGLIB | 统一 CGLIB |
proxyTargetClass |
false(JDK 优先) | true(CGLIB 优先) |
spring.aop.proxy-target-class |
默认 false | 默认 true |
| 自注入 | 可能触发循环依赖 | Spring 3.x 支持 |
@EnableAspectJAutoProxy |
需要手动配 | 自动配置 |
迁移建议:
- 如果之前依赖 JDK 代理(接口 +
proxyTargetClass=false),升级后自动切到 CGLIB,一般不会有问题 - 如果之前通过接口类型注入 Bean,CGLIB 也兼容,因为代理类同样实现了该接口
- 检查是否有
final方法被切面拦截,升级后 CGLIB 会代理不到
八、总结
swift
Spring AOP 代理选择全景
│
├── 代理类型
│ ├── JDK → 基于接口,只能代理接口方法
│ └── CGLIB → 基于继承,代理所有非 final 方法
│
├── 选择规则
│ ├── Boot 2.x → 有接口用 JDK,无接口用 CGLIB
│ └── Boot 3.x → 全部用 CGLIB
│
├── 同类调用问题
│ ├── 原因:this 是原始对象,不走代理
│ └── 解决:自注入 / AopContext / 拆类
│
├── 创建时机
│ └── BeanPostProcessor.after → 初始化最后阶段
│
└── 不代理的情况
├── private / final / static 方法
├── 构造函数和 @PostConstruct 中的 this
└── 目标类是 final(CGLIB 无法继承)
| 关键结论 | 说明 |
|---|---|
| 代理的本质 | 运行时给 Bean 套一层"拦截器" |
| Boot 3.x 默认 CGLIB | 不再区分有无接口,统一 CGLIB |
| 同类调用不经过代理 | 这是 @Transactional 失效最常见的原因 |
| 自注入是最简解法 | @Autowired 注入代理对象,内部调用走代理 |
| private/final 不可代理 | 任何代理技术都拦截不到 |
理解了代理选择机制,以后遇到"注解不生效""注入的是代理对象"这类问题,不再靠猜,直接定位。
本文是 Java 技术系列第 4 篇,系列目录:
- Spring 循环依赖到底怎么解的?三级缓存源码拆解
- @Transactional 注了等于没用?Spring 事务失效的 7 种场景
- Spring Boot 自动配置到底怎么做到的?源码拆解 @EnableAutoConfiguration 全流程
- 本文:@Autowired 注入的是代理对象?Spring AOP 代理选择原理一次讲透