ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/10/2 6:59:40 拓冰建站 浏览量
Spring 依赖注入与循环依赖详解 Spring 依赖注入与循环依赖详解定位讲透依赖注入的三种方式与选型、依赖解析规则、作用域、三级缓存解决循环依赖的完整机制与边界适用版本Spring Framework 6.xJDK 17目录一、注入方式二、依赖解析三、作用域四、循环依赖与三级缓存五、特殊注入场景六、总结七、常见高频面试题一、注入方式1.1 三种方式对比方式写法优点缺点构造器注入依赖作为构造参数可配 Autowired单构造器可省略依赖不可变final、必填性显式、对象创建即完整、便于单测依赖多时参数长这是信号不是缺陷字段注入Autowired private Xxx xxx;最简洁无法 final、必须依赖容器才能构造、依赖数量被掩盖Setter 注入Autowired public void setXxx(...)可选依赖、可后期重设对象可能处于不完整状态1.2 官方推荐构造器注入的四个理由不可变性final 字段注入后不可篡改完整性保证对象构造完成即依赖齐全不存在半初始化状态可测试性单测直接new Service(mockDao)不需要容器依赖报警构造器参数超过五六个说明类职责过重——字段注入把这个信号藏起来了。循环依赖视角构造器注入的循环依赖无法被三级缓存解决对象都没实例化就互相要对方启动直接报错——这反而是优点把设计问题暴露在启动期而非运行期。二、依赖解析2.1 按类型与按名称AutowiredSpring默认按类型 → 同类型多个候选 ① Qualifier(name) 指定 ② Primary 标记优先候选 ③ 按字段名匹配兜底 → 仍无法确定启动报错 ResourceJakarta/JDK默认按名称找不到再按类型2.2 多候选的三个处理手段手段用法场景QualifierAutowired Qualifier(mysqlUserDao)注入点明确要哪个Primary实现类上标注全局默认首选集合注入ListPayChannel channels策略模式一次拿全部实现自行分发集合注入 策略分发是 Spring 生态最优雅的可扩展模式之一新增实现类即自动进入列表调用方零改动。2.3 可选与延迟可选依赖Autowired(required false) 或 Nullable 延迟获取ObjectProviderXxx注入 Provider用时 getObject() 配置值 Value(${app.name}) / Value(#{systemProperties[user.home]})ObjectProvider的三个用途可选依赖的安全获取、延迟到运行期获取、配合stream()遍历全部候选。三、作用域作用域语义注意singleton默认容器内一个定义一个实例有状态单例有并发问题Bean 应保持无状态prototype每次获取新建销毁容器不管01 篇request / sessionWeb 环境按请求/会话注入单例时实际注入的是作用域代理自定义实现Scope注册如按租户作用域作用域不匹配问题单例依赖 prototype注入只发生一次——单例持有的永远是第一次那个实例。解法见第五节。四、循环依赖与三级缓存4.1 问题定义A 依赖 BB 依赖 Asetter/字段注入 若等对方完全就绪才注入 → 死锁谁都创建不完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 单例注入 prototypeComponentpublicclassOrderService{// 错误示范注入只发生一次永远是同一个 PriceCalculatorAutowiredprivatePriceCalculatorcalc;// prototype// 正确每次用时现取AutowiredprivateObjectProviderPriceCalculatorcalcProvider;publicvoiduse(){calcProvider.getObject().run();}// 或用 Lookup容器生成覆盖方法的子类LookupprotectedabstractPriceCalculatornewCalc();}5.2 Async 与循环依赖的经典冲突Async 的代理在 BeanPostProcessor 中生成 若该 Bean 已被提前暴露二级缓存存的是原始对象 → 最终代理与已暴露引用不一致 → 启动报循环依赖错误解法打破循环重构/Lazy、或将 Async 职责拆出被循环引用的 Bean。5.3 Lazy 的两个用途启动期延迟创建重资源 Bean 用到再建注入点加Lazy注入的是代理打破启动期循环首次调用才解析真实对象。六、总结注入方式构造器注入是默认推荐不可变、完整、可测、暴露依赖过多字段注入掩盖问题构造器循环依赖启动即报错是设计问题的早期暴露。解析规则Autowired 按类型多候选用 Qualifier/Primary/集合注入Resource 按名称可选依赖用 requiredfalse/ObjectProvider。作用域默认无状态单例单例注入 prototype 只注入一次用 ObjectProvider/Lookup 每次现取。三级缓存一级成品、二级半成品、三级工厂流程是实例化→提前暴露工厂→对方取用→补齐成品三级的意义是延迟代理生成决策。边界与态度构造器环/prototype 环/Async 环无法兜底循环依赖多为设计气味优先重构。七、常见高频面试题1. 构造器注入、字段注入、Setter 注入怎么选要点推荐构造器注入——依赖可 final 不可变、对象创建即完整、单测可直接 new 传入 mock、依赖过多时参数列表长是职责过重的信号。字段注入写法最简但无法 final、脱离容器无法构造、掩盖依赖数量易养出上帝类。Setter 适合可选依赖或后期重配。官方明确推荐构造器注入。2. Autowired 和 Resource 的区别要点来源与解析规则不同。Autowired 是 Spring 注解默认按类型注入同类型多候选时配合 Qualifier 或 Primary支持 requiredfalseResource 是 Jakarta原 JSR-250规范注解默认按名称查找找不到再按类型。需要可移植性不绑定 Spring用 Resource需要 Spring 特性Qualifier、集合注入、构造器注入用 Autowired。3. 同一个接口有多个实现注入时如何区分要点三种手段。Qualifier(“beanName”) 在注入点指定Primary 在实现类标注全局优先候选集合注入List/Map接口一次拿到全部实现自行分发——这是策略模式的优雅实现新增实现自动入列调用方零改动。都不满足时启动报错NoUniqueBeanDefinitionException是配置问题的显性暴露。4. Spring 如何解决循环依赖详细说说三级缓存。要点仅支持单例的字段/Setter 注入循环。三级缓存一级存成品、二级存提前暴露的半成品、三级存对象工厂。流程A 实例化后工厂入三级A 注入 B 时先创建 BB 注入 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 避免被循环引用。这是代理生成时机与提前暴露机制冲突的典型案例。