☰
Spring 依赖注入与循环依赖详解
2026/10/2 6:59:18 网站建设 项目流程

Spring 依赖注入与循环依赖详解

定位:讲透依赖注入的三种方式与选型、依赖解析规则、作用域、三级缓存解决循环依赖的完整机制与边界
适用版本:Spring Framework 6.x(JDK 17+)


目录

  • 一、注入方式
  • 二、依赖解析
  • 三、作用域
  • 四、循环依赖与三级缓存
  • 五、特殊注入场景
  • 六、总结
  • 七、常见高频面试题

一、注入方式

1.1 三种方式对比

方式写法优点缺点
构造器注入依赖作为构造参数(可配 @Autowired,单构造器可省略)依赖不可变(final)、必填性显式、对象创建即完整、便于单测依赖多时参数长(这是信号不是缺陷)
字段注入@Autowired private Xxx xxx;最简洁无法 final、必须依赖容器才能构造、依赖数量被掩盖
Setter 注入@Autowired public void setXxx(...)可选依赖、可后期重设对象可能处于不完整状态

1.2 官方推荐构造器注入的四个理由

  1. 不可变性:final 字段,注入后不可篡改;
  2. 完整性保证:对象构造完成即依赖齐全,不存在"半初始化"状态;
  3. 可测试性:单测直接new Service(mockDao),不需要容器;
  4. 依赖报警:构造器参数超过五六个,说明类职责过重——字段注入把这个信号藏起来了。

循环依赖视角:构造器注入的循环依赖无法被三级缓存解决(对象都没实例化就互相要对方),启动直接报错——这反而是优点,把设计问题暴露在启动期而非运行期。


二、依赖解析

2.1 按类型与按名称

@Autowired(Spring):默认按类型 → 同类型多个候选? ① @Qualifier("name") 指定 ② @Primary 标记优先候选 ③ 按字段名匹配(兜底) → 仍无法确定:启动报错 @Resource(Jakarta/JDK):默认按名称,找不到再按类型

2.2 多候选的三个处理手段

手段用法场景
@Qualifier@Autowired @Qualifier("mysqlUserDao")注入点明确要哪个
@Primary实现类上标注全局默认首选
集合注入List<PayChannel> channels策略模式:一次拿全部实现自行分发

集合注入 + 策略分发是 Spring 生态最优雅的可扩展模式之一:新增实现类即自动进入列表,调用方零改动。

2.3 可选与延迟

可选依赖:@Autowired(required = false) 或 @Nullable 延迟获取:ObjectProvider<Xxx>(注入 Provider,用时 getObject()) 配置值: @Value("${app.name}") / @Value("#{systemProperties['user.home']}")

ObjectProvider的三个用途:可选依赖的安全获取、延迟到运行期获取、配合stream()遍历全部候选。


三、作用域

作用域语义注意
singleton(默认)容器内一个定义一个实例有状态单例有并发问题,Bean 应保持无状态
prototype每次获取新建销毁容器不管(01 篇)
request / sessionWeb 环境,按请求/会话注入单例时实际注入的是作用域代理
自定义实现Scope注册如"按租户"作用域

作用域不匹配问题:单例依赖 prototype,注入只发生一次——单例持有的永远是第一次那个实例。解法见第五节。


四、循环依赖与三级缓存

4.1 问题定义

A 依赖 B,B 依赖 A(setter/字段注入) 若等对方完全就绪才注入 → 死锁,谁都创建不完

Spring 的解法:先创建半成品(实例化但不注入),提前暴露引用,让对方先拿到引用完成创建,再回头补齐自己。三级缓存就是支撑这个过程的存储结构。

4.2 三级缓存结构

缓存名称存放
一级singletonObjects完成品:初始化完毕的 Bean
二级earlySingletonObjects半成品:已实例化、提前暴露的对象
三级singletonFactories对象工厂:ObjectFactory,可生成半成品(必要时生成代理)

4.3 解决流程(A↔B,字段注入)

① 创建 A:实例化(构造器)→ 把 A 的 ObjectFactory 放入【三级缓存】 ② A 填充属性:发现需要 B → 去创建 B ③ 创建 B:实例化 → B 的工厂入三级 → B 填充属性发现需要 A ④ B 获取 A:从一级二级都没有 → 三级缓存取出工厂 → 工厂生成 A 的早期引用(若 A 需 AOP 则此处生成代理) → 放入【二级缓存】,删除三级中 A 的工厂 ⑤ B 拿到 A 的引用 → B 完成初始化 → 放入一级缓存 ⑥ 回到 A:注入 B → A 完成初始化 → 放入一级缓存,二级清理

4.4 为什么需要三级而不是两级

关键在于代理的时机。AOP 代理正常在生命周期末尾(初始化后)生成;但循环依赖要求提前暴露引用,若 A 需要代理,提前暴露的就必须是代理而不是原始对象。

  • 三级缓存的工厂延迟决定:没人循环引用 A 时,代理按正常时机在末尾生成;一旦 B 提前取 A,工厂此时生成代理放入二级;
  • 若只有两级缓存(提前暴露固定为原始对象),要么破坏代理时机,要么所有 Bean 都提前生成代理(破坏设计)。

一句话:三级缓存用"工厂"把"是否/何时生成代理"的决策延迟到真正需要时。

4.5 无法解决的情况

场景原因
构造器注入成环实例化阶段就要对方,无"半成品"可提前暴露
prototype 成环不缓存、不管生命周期,直接抛异常
异步/后置处理中新增的依赖成环创建时机脱离主流程(@Async 的著名坑:代理生成时机与提前暴露冲突)

实践态度:循环依赖多数是设计气味(职责纠缠),能重构就重构;框架兜底不等于鼓励依赖成环。


五、特殊注入场景

5.1 单例注入 prototype

@ComponentpublicclassOrderService{// 错误示范:注入只发生一次,永远是同一个 PriceCalculator@AutowiredprivatePriceCalculatorcalc;// prototype// 正确:每次用时现取@AutowiredprivateObjectProvider<PriceCalculator>calcProvider;publicvoiduse(){calcProvider.getObject().run();}// 或用 @Lookup(容器生成覆盖方法的子类)@LookupprotectedabstractPriceCalculatornewCalc();}

5.2 @Async 与循环依赖的经典冲突

@Async 的代理在 BeanPostProcessor 中生成 若该 Bean 已被提前暴露(二级缓存存的是原始对象) → 最终代理与已暴露引用不一致 → 启动报循环依赖错误

解法:打破循环(重构/@Lazy)、或将 @Async 职责拆出被循环引用的 Bean。

5.3 @Lazy 的两个用途

  1. 启动期延迟创建(重资源 Bean 用到再建);
  2. 注入点加@Lazy注入的是代理,打破启动期循环(首次调用才解析真实对象)。

六、总结

  1. 注入方式:构造器注入是默认推荐(不可变、完整、可测、暴露依赖过多);字段注入掩盖问题;构造器循环依赖启动即报错,是设计问题的早期暴露。
  2. 解析规则:@Autowired 按类型(多候选用 @Qualifier/@Primary/集合注入),@Resource 按名称;可选依赖用 required=false/ObjectProvider。
  3. 作用域:默认无状态单例;单例注入 prototype 只注入一次,用 ObjectProvider/@Lookup 每次现取。
  4. 三级缓存:一级成品、二级半成品、三级工厂;流程是"实例化→提前暴露工厂→对方取用→补齐成品";三级的意义是延迟代理生成决策。
  5. 边界与态度:构造器环/prototype 环/@Async 环无法兜底;循环依赖多为设计气味,优先重构。

七、常见高频面试题

1. 构造器注入、字段注入、Setter 注入怎么选?

要点:推荐构造器注入——依赖可 final 不可变、对象创建即完整、单测可直接 new 传入 mock、依赖过多时参数列表长是职责过重的信号。字段注入写法最简但无法 final、脱离容器无法构造、掩盖依赖数量,易养出上帝类。Setter 适合可选依赖或后期重配。官方明确推荐构造器注入。

2. @Autowired 和 @Resource 的区别?

要点:来源与解析规则不同。@Autowired 是 Spring 注解,默认按类型注入,同类型多候选时配合 @Qualifier 或 @Primary,支持 required=false;@Resource 是 Jakarta(原 JSR-250)规范注解,默认按名称查找,找不到再按类型。需要可移植性(不绑定 Spring)用 @Resource;需要 Spring 特性(@Qualifier、集合注入、构造器注入)用 @Autowired。

3. 同一个接口有多个实现,注入时如何区分?

要点:三种手段。@Qualifier(“beanName”) 在注入点指定;@Primary 在实现类标注全局优先候选;集合注入(List/Map<接口>)一次拿到全部实现自行分发——这是策略模式的优雅实现,新增实现自动入列,调用方零改动。都不满足时启动报错(NoUniqueBeanDefinitionException),是配置问题的显性暴露。

4. Spring 如何解决循环依赖?详细说说三级缓存。

要点:仅支持单例的字段/Setter 注入循环。三级缓存:一级存成品、二级存提前暴露的半成品、三级存对象工厂。流程:A 实例化后工厂入三级;A 注入 B 时先创建 B;B 注入 A 时从三级取工厂生成 A 的早期引用(如需代理则此时生成代理)放入二级;B 完成后入一级;A 补齐注入后入一级并清理二级。核心思想:先给"半成品引用"打破等待死锁。

5. 为什么需要三级缓存,两级不行吗?

要点:关键在 AOP 代理的生成时机。代理正常在初始化后生成,但循环依赖要求提前暴露引用;若 A 需要代理,提前暴露的必须是代理。三级缓存的 ObjectFactory 把"是否生成代理"延迟到真正被循环引用时:无循环则代理按正常时机生成,有循环则工厂提前生成代理放入二级。若只有两级(提前暴露固定为原始对象),只能让所有 Bean 都提前生成代理,破坏 AOP 设计。

6. 哪些循环依赖 Spring 无法解决?为什么?

要点:① 构造器注入成环——实例化阶段就需要对方,连半成品都创建不出来,启动直接报错;② prototype 成环——prototype 不缓存、无生命周期管理,无法提前暴露;③ @Async 等场景——代理生成时机与提前暴露的引用冲突。这些限制反过来说明:循环依赖多数是设计问题,框架兜底有限,优先通过重构(抽接口、事件、@Lazy)消除。

7. @Autowired 注入的集合(List<接口>)是怎么工作的?

要点:注入点声明为集合类型时,容器把该类型的全部 Bean 收集注入(可配合 @Order/Ordered 排序)。这是策略模式/插件机制的标准做法:定义策略接口,各实现注册为 Bean,调用方注入 List 自行遍历或按条件分发;新增策略实现只需加一个 @Component,调用方无需改动,符合开闭原则。

8. 单例 Bean 依赖 prototype Bean 会有什么问题?怎么解?

要点:注入只在单例创建时发生一次,之后单例持有的永远是第一次注入的 prototype 实例,"每次都是新的"语义失效。解法:注入 ObjectProvider,每次使用时 getObject() 现取;或用 @Lookup 注解方法让容器生成返回新实例的子类;或改造设计(如把 prototype 职责改为显式工厂)。

9. @Lazy 有哪些用途?

要点:两个。① 延迟初始化:标注的 Bean 不在启动时创建,首次使用时才实例化,适合重资源对象加快启动;② 打破启动期循环依赖:在注入点加 @Lazy 注入的是代理,真实对象延迟到首次方法调用才解析,从而让启动期互相依赖的双方都能完成创建。注意 @Lazy 不能解决构造器循环的本质设计问题,只是延后触发。

10. 为什么 @Async 的 Bean 容易在循环依赖时报错?

要点:@Async 代理由 BeanPostProcessor 在初始化后生成;若该 Bean 因循环依赖被提前暴露(二级缓存存的是原始对象),后续生成的代理与已暴露给对方的引用不一致,Spring 检测到此冲突直接抛循环依赖异常。解法:重构消除循环、用 @Lazy 延迟一方、或将 @Async 方法拆到独立 Bean 避免被循环引用。这是"代理生成时机"与"提前暴露机制"冲突的典型案例。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询