为什么代理对象不做注入,只持有原始target的引用?

核心问题:Spring 为什么不给代理对象做依赖注入,而是只让它持有原始 target 对象?

核心结论:为了让 AOP 仅仅是“增强”,而非“入侵”;为了保证 Bean 生命周期的纯洁性、一致性与可扩展性,这是 Spring AOP 设计的灵魂所在。

一、代理对象的本质:只是“壳”,不是真正的 Bean

1. Spring 对 Bean 的定义:由 Spring 完整管理生命周期(实例化 → 依赖注入 → 初始化 → 销毁)的对象,承担具体业务逻辑,拥有自身状态。

2. AOP 代理的本质:运行时动态生成的类,核心职责只有一个——拦截方法调用 → 执行切面逻辑 → 转发调用到原始 target 对象。

3. 关键禁忌:如果给代理对象做注入,会导致同一个 Bean 的属性被注入两次(代理一套、target 一套),出现状态不一致、内存浪费、行为不可控的混乱场景,Spring 坚决避免这种情况。

二、真正的 Bean 只有一个:原始 target 对象

Spring 严格遵循一个核心原则:一个 beanName,永远只对应一个真正的、完整经历生命周期的 Bean。

这个“真身”就是 target 对象,具体表现为:

  • 真正被 Spring 实例化的是 target 对象

  • 真正被执行依赖注入的是 target 对象

  • 真正被执行初始化、销毁逻辑的是 target 对象

  • 真正被 Spring 管理、承担业务逻辑的是 target 对象

代理对象只是一层“外衣”,不是 Bean 真身,但需要重点注意:我们从 Spring 容器中获取到的 Bean,其实是这个代理对象,而非原始 target 对象。Spring 会自动将代理对象返回给开发者,开发者无需感知代理的存在,只需正常调用方法即可触发切面增强然后再调用target的被代理方法;若给代理注入,会直接破坏单例语义、依赖注入语义和 Bean 生命周期的一致性。

三、契合 AOP 设计哲学:无侵入式增强

AOP 的核心初衷:不修改原始类代码、不改变原始 Bean 的结构和逻辑,仅为其添加日志、事务、权限等横切逻辑,实现“横切逻辑与业务逻辑解耦”。

若让代理对象参与注入,会完全违背这一初衷:

代理对象必须知晓原始类的所有字段、复制所有属性,还要跟随原始类的修改而修改,同时处理继承、循环依赖、@Value、@Resource 等复杂场景——这就变成了侵入式增强,与 AOP 的设计思想背道而驰。

因此 Spring 选择最简洁的方式:代理不复制任何属性、不参与任何注入,仅持有 target 对象的引用,只做“方法增强”,不触碰“字段属性”。

四、代理的唯一职责:方法拦截(不负责字段访问)

无论是 JDK 动态代理(针对接口)还是 CGLIB 代理(针对类),其设计目的都非常单一:拦截方法调用,不负责字段访问。

Spring 严格遵循这一底层设计:

  • 当你访问代理对象的字段时,代理无法拦截(即使反射也不行),所以代理对象的字段默认是 null;

  • 当你调用代理对象的方法时,代理会拦截调用,转发到 target 对象,此时会使用 target 中已注入的属性(有值)。

注意:这不是 Spring 的缺陷,而是刻意设计——让代理只做它擅长的事,不越界、不冗余。这里需要明确:我们从容器中获取到的 Bean 是代理对象,访问其字段时默认 null,但调用其方法时,代理会转发到 target,从而使用到已注入的属性

五、这种设计带来的核心好处(工程师可直接感知)

  1. Bean 生命周期清晰可控:真正的实例化、注入、初始化只有一次,不会出现重复操作,避免生命周期混乱;

  2. 不破坏原有业务逻辑:原始 Bean 的编写、注入方式完全不变,开发者无需关注代理的存在,专注于业务本身;

  3. 内存高效:代理只是一个薄薄的“拦截壳”,不复制任何属性,不会造成内存浪费;

  4. 行为可预测:永远只有一个真实的 target 对象,不会出现两份状态,避免线程安全、状态不一致问题;

  5. 切面可任意叠加:事务、缓存、日志等多层切面可叠加使用,无论多少层代理,都只围绕 target 对象增强,不影响核心逻辑。

灵魂总结(必记)

Spring 不把代理对象当作 Bean,只把它当作原始 target 对象的“方法拦截器”。

真正的 Bean 永远是 target,代理只是一个“调用中介”——只增强方法,不增强字段;只持有 target,不参与注入;只做拦截,不承担业务逻辑。

这就是 Spring AOP 最干净、最稳定、最贴合其设计本质的实现方式。

延伸思考(同根源问题)

以下两个常见问题,本质和本文核心一致(代理仅作为方法拦截器、不参与注入,真正的逻辑由target执行),结合前文知识点,具体解析如下:

  • 为什么 Service 内部用 this 调用方法,AOP 切面(如 @Transactional)不会生效?

    核心原因:this 指向的是 原始 target 对象,而非 Spring 生成的代理对象。前文已明确,AOP 切面逻辑只织入到代理对象中,只有调用代理对象的方法,才能触发切面拦截、执行横切逻辑(如事务)。而 Service 内部用 this 调用,本质是直接调用 target 的方法,跳过了代理对象,切面自然无法生效。

    通俗理解:this 相当于“跳过中介(代理),直接找真身(target)”,而切面逻辑只在“中介”身上,真身本身没有切面增强,所以切面不生效。

  • 为什么 @Transactional 注解有时会失效?

    最常见的根源的和上一个问题一致——调用者不是代理对象,导致切面(事务切面)无法被触发。

    具体场景包括:

    1. Service 内部 this 调用(如上文所述,跳过代理,切面不生效);

    2. 注解标注在非 public 方法上(Spring AOP 默认只拦截 public 方法,非 public 方法无法被代理拦截);

    3. 异常被手动 catch 且未重新抛出(事务切面需要捕获异常才能触发回滚,手动 catch 后未抛出,切面无法感知异常)。

    本质:@Transactional 是 Spring AOP 的典型应用,其生效依赖代理对象的方法拦截;一旦跳过代理,事务切面无法织入,注解自然失效。

文章作者: Z
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 微博客
笔记 Java spring framework spring 笔记 java
喜欢就支持一下吧