@Autowired 注入的是代理对象?Spring AOP 代理选择原理一次讲透

@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 初始化的最后阶段创建的。这意味着:

  1. 构造函数里拿到的 this 不是代理
  2. @PostConstruct 里拿到的 this 也不是代理
  3. 只有其他 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 需要手动配 自动配置

迁移建议

  1. 如果之前依赖 JDK 代理(接口 + proxyTargetClass=false),升级后自动切到 CGLIB,一般不会有问题
  2. 如果之前通过接口类型注入 Bean,CGLIB 也兼容,因为代理类同样实现了该接口
  3. 检查是否有 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 篇,系列目录:

  1. Spring 循环依赖到底怎么解的?三级缓存源码拆解
  2. @Transactional 注了等于没用?Spring 事务失效的 7 种场景
  3. Spring Boot 自动配置到底怎么做到的?源码拆解 @EnableAutoConfiguration 全流程
  4. 本文:@Autowired 注入的是代理对象?Spring AOP 代理选择原理一次讲透
相关推荐
@insist1231 小时前
信息系统管理工程师-任务、标准、EDM 过程与关键域
java·大数据·数据库
木子杳衫1 小时前
Java高级进阶 | 单元测试JUnit + 反射机制实战详解
java·junit·单元测试
qq_366086221 小时前
关于接口路径中中文值解码问题
java·开发语言·数据库
大白菜和MySQL1 小时前
elk部署和kibana图形化展示
java·笔记·elasticsearch
Zane19942 小时前
JMM 与 happens-before:一次搞懂 Java 内存模型
java·后端
An_s2 小时前
Java Spring Boot+vue3文件管理系统
java·开发语言
wuqingshun3141592 小时前
接口和抽象类有什么区别?
java
SimonKing2 小时前
别再写 setter 了!MapStruct Plus vs MapStruct,谁才是 Bean 转换的真神?
java·后端·程序员
倒流时光三十年2 小时前
第一阶段 05 · Java 客户端查询类详解(Query / SearchCriteria / Response 与复杂拼接)
java·开发语言·python