Spring(二):依赖注入、Bean 生命周期与循环依赖
Spring(二):依赖注入、Bean 生命周期与循环依赖
导语:本篇讲 Bean 从"出生到死亡"的全过程——依赖注入的四种方式与三种注解、Bean 作用域与完整生命周期(含所有扩展点顺序),然后集中攻下 Spring 面试最经典的难题:循环依赖与三级缓存(为什么必须是"三级"、为什么构造器注入无解、生产上怎么避免),共 15 题。
一、依赖注入方式与注解
1. Spring 有哪几种依赖注入方式?为什么推荐构造器注入?
答:
| 方式 | 写法 | 优点 | 缺点 |
|---|---|---|---|
| 构造器注入 | 构造方法参数 | 依赖不可变(可 final)、必填项在创建时即完整、便于测试、能暴露循环依赖 | 依赖多时构造器冗长 |
| setter 注入 | setter 方法 | 可选依赖灵活、可在创建后重新配置 | 依赖可被外部改写,存在"半初始化"风险 |
| 字段注入 | @Autowired 标在字段 | 代码最简洁 | 隐藏依赖、无法注入 final、脱离容器难测试、易掩盖设计问题 |
| 工厂方法注入 | @Bean 方法 / 静态工厂 | 适合第三方类、复杂创建逻辑 | 一般用于配置而非业务类 |
为什么官方推荐构造器注入:
- 不可变 + 强制完整:依赖为
final,对象创建后不会处于"缺依赖"状态; - fail-fast:构造器循环依赖会在启动期直接报错,逼出设计问题而不是隐藏它;
- 易测:无需容器也能
new出来。
易错点:Spring 官方文档明确说明——如果依赖是必填的,就用构造器注入;只有当依赖可选或存在循环依赖需要打破时才用 setter。字段注入只适合测试类或极简单的场景。
2. @Autowired、@Resource、@Inject 有什么区别?
答:
| 注解 | 来源 | 默认匹配 | 是否支持按名指定 | 备注 |
|---|---|---|---|---|
@Autowired | Spring 提供 | 按类型(byType) | 配合 @Qualifier("name") | 支持 required(默认 true),可注入集合/Map(同类型多 Bean 时注入全部) |
@Resource | JSR-250(Java 标准) | 按名称(byName),找不到再 byType | name / type 属性 | 不支持 @Qualifier;由 CommonAnnotationBeanPostProcessor 处理 |
@Inject | JSR-330(Java 标准) | 按类型 | 配合 @Named | 无 required 属性,需引入 javax.inject/jakarta.inject 依赖 |
可选依赖的三种表达:
@Autowired(required = false) private A a; // 可为 null
@Autowired private Optional<A> a; // 推荐:显式可选
@Autowired @Nullable private A a;3. @Autowired 的匹配策略是什么?多个同类型 Bean 时如何注入?
答: 解析由 AutowiredAnnotationBeanPostProcessor 完成,DefaultListableBeanFactory#determineAutowireCandidate 按以下顺序决策:
@Primary标注的 Bean 优先;@Qualifier("beanName")精确指定;@Priority/PriorityOrdered排序;- 按字段名/参数名与 Bean 名称匹配(兜底);
- 仍无法唯一确定 → 抛
NoUniqueBeanDefinitionException;一个都没有 →NoSuchBeanDefinitionException。
特殊能力(常被追问): 若注入目标是数组、List、Set、Map<String, T>,Spring 会注入所有同类型 Bean(Map 的 key 为 Bean 名称),这正是"策略模式 + 注入集合"的常用写法。
4. @Autowired 注入的底层是怎么实现的?
答:
AutowiredAnnotationBeanPostProcessor实现了InstantiationAwareBeanPostProcessor与MergedBeanDefinitionPostProcessor;postProcessMergedBeanDefinition():在实例化后扫描类中的@Autowired字段/方法并缓存注入元数据(InjectionMetadata);postProcessProperties():在属性填充阶段遍历注入点,调用DefaultListableBeanFactory#resolveDependency()找到依赖;- 解析时经过
resolveDependency→findAutowireCandidates(按类型找候选)→determineAutowireCandidate(消歧); - 找到后通过反射
Field.set()/Method.invoke()写入值。
因此
@Autowired本质是"BPP 在属性填充阶段用反射赋值",而不是编译期/构造期行为——这也是"字段注入无法注入final、且不经过构造器"的原因。
二、Bean 作用域与生命周期
5. Spring Bean 有哪些作用域?
答:
| 作用域 | 说明 |
|---|---|
| singleton(默认) | 容器内唯一实例 |
| prototype | 每次 getBean() 新建;容器不管理其销毁 |
| request | 每个 HTTP 请求一个实例(Web 环境) |
| session | 每个 HTTP Session 一个实例 |
| application | 每个 ServletContext 一个实例 |
| websocket | 每个 WebSocket 会话一个实例 |
易错点:
singleton指"IoC 容器内单例",不是 JVM 级单例——父子容器、多个容器可以各有一个同名 Bean。
6. 请完整描述 Spring Bean 的生命周期。
答: 完整链路(面试建议按此顺序答,并点出扩展点):
① 实例化(构造器 / 工厂方法 / Supplier)
② 属性填充(@Autowired、@Value 注入)
③ Aware 回调:BeanNameAware → BeanClassLoaderAware → BeanFactoryAware → ... → ApplicationContextAware
④ BeanPostProcessor.postProcessBeforeInitialization
⑤ 初始化:@PostConstruct → InitializingBean.afterPropertiesSet() → init-method
⑥ BeanPostProcessor.postProcessAfterInitialization ← AOP 代理在此生成
⑦ Bean 就绪,放入一级缓存 singletonObjects,可被使用
⑧ 销毁:@PreDestroy → DisposableBean.destroy() → destroy-method几个关键细节:
③ Aware在属性填充之后、初始化之前;ApplicationContextAware的注入实际由ApplicationContextAwareProcessor(一个 BPP)在④之前完成。⑥ AOP 代理在postProcessAfterInitialization生成**(AbstractAutoProxyCreator`),所以最终放进容器的是代理对象**。BeanPostProcessor是对所有 Bean 都生效的"全局钩子",注意别在其中做重活。
7. BeanPostProcessor 和 BeanFactoryPostProcessor 有什么区别?
答:
| BeanFactoryPostProcessor | BeanPostProcessor | |
|---|---|---|
| 执行时机 | BeanDefinition 注册后、Bean 实例化前 | 每个 Bean 的实例化与属性填充之后(初始化前后) |
| 作用对象 | BeanDefinition 元数据 | Bean 实例 |
| 典型实现 | ConfigurationClassPostProcessor、PropertySourcesPlaceholderConfigurer | AutowiredAnnotationBeanPostProcessor、AbstractAutoProxyCreator |
| 注册时机 | refresh() 第 5 步(invokeBeanFactoryPostProcessors) | refresh() 第 6 步(registerBeanPostProcessors) |
一句话:前者改"图纸"(定义),后者改"成品"(实例)。BPP 的注册顺序很重要(
PriorityOrdered→Ordered→ 无序),因为后注册的可能影响先注册的加工结果。
8. @PostConstruct、InitializingBean、init-method 的执行顺序?
答: 初始化阶段(依次):@PostConstruct → InitializingBean#afterPropertiesSet() → 自定义 init-method。
销毁阶段:@PreDestroy → DisposableBean#destroy() → destroy-method。
原理:
@PostConstruct由CommonAnnotationBeanPostProcessor#postProcessBeforeInitialization触发;afterPropertiesSet与init-method由AbstractAutowireCapableBeanFactory#invokeInitMethods调用。推荐使用@PostConstruct(标准注解、语义清晰),InitializingBean会与 Spring API 耦合。
9. 什么是懒加载(lazy-init)?单例 Bean 的实例化时机有何不同?
答: 默认单例 Bean 在容器 refresh() 的 finishBeanFactoryInitialization() 阶段就预实例化;配置 lazy-init=true(或 @Lazy)后,延迟到首次 getBean() 时才创建。
- 正常预实例化:启动慢一点,但能在启动期暴露配置错误(fail-fast),运行期首次调用更快。
- 懒加载:启动更快,但错误被推迟到运行期,且首次请求有创建开销(在 Web 场景可能造成首请求抖动)。
实践:生产环境不建议全局懒加载(Boot 2.2 的
spring.main.lazy-initialization=true要谨慎),因为它会把"启动期失败"变成"运行期故障"。
10. 单例 Bean 和原型(prototype)Bean 在生命周期管理上有何不同?
答:
- singleton:容器完整管理其生命周期——创建、缓存、销毁都会回调(
DisposableBean/@PreDestroy/destroy-method有效)。 - prototype:容器只负责创建与初始化,不缓存、不负责销毁——销毁方法不会被回调,需要调用方自行释放资源(典型坑:prototype Bean 里开了连接/线程未释放导致泄漏)。
另一个高频追问:单例 Bean 注入 prototype Bean 会怎样? 由于单例只初始化一次,它持有的 prototype 也只会被注入一次,之后不再变化。要每次拿到新实例,需要
@Lookup、ObjectProvider<T>、或ApplicationContext#getBean()。
11. 单例 Bean 是线程安全的吗?
答: Spring 不保证单例 Bean 的线程安全——"单例"只表示容器中只有一个实例,并发安全取决于这个实例自身是否有可变的共享状态。
- 无状态 Bean(只提供方法、无可写成员变量)→ 天然线程安全,放心用单例(这也是 Service/DAO 通常无状态的原因)。
- 有状态 Bean → 需自行保证线程安全:避免可写共享字段、用
ThreadLocal、局部变量、不可变对象,或改用prototype作用域。 - 注意框架组件的隐含状态:
SimpleDateFormat(非线程安全)、HttpServletRequest(请求级)、Model等。
一句话:Bean 的线程安全性不是 Spring 的责任,而是使用者的设计责任。
三、循环依赖与三级缓存
12. 什么是循环依赖?Spring 能解决哪些情况?
答: 循环依赖指 Bean 之间形成依赖环(A 依赖 B,B 依赖 A,甚至自依赖)。
| 情况 | 能否解决 | 原因 |
|---|---|---|
| 单例 + 字段/setter 注入 | ✅ 能 | 可以"先实例化、后补依赖",用三级缓存提前暴露引用 |
单例 + @Lazy 注入 | ✅ 能 | 注入的是代理,真正使用时才去容器取,延迟了调用时机 |
| 构造器注入的循环依赖 | ❌ 不能(抛 BeanCurrentlyInCreationException) | 构造依赖必须在实例化前就绪,谁也无法先"出生" |
| prototype 作用域的循环依赖 | ❌ 不能 | 每次新建、无缓存,没有可复用的"早期对象" |
@Async 代理的循环依赖 | ❌ 常见报错 | 异步代理在初始化完成后生成,早期引用拿不到代理 → 需 @Lazy 打破 |
重要提醒:Spring Boot 2.6 起默认禁止循环依赖(
spring.main.allow-circular-references=false),启动直接报错。官方态度很明确:循环依赖是设计问题,应该重构,而不是依赖容器兜底。
13. Spring 是如何用"三级缓存"解决单例循环依赖的?
答: 三级缓存在 DefaultSingletonBeanRegistry 中:
| 级别 | 名称 | 存什么 |
|---|---|---|
| 一级 | singletonObjects | 完全初始化好的单例 Bean |
| 二级 | earlySingletonObjects | 早期暴露的 Bean(已实例化、未完成填充/初始化,可能是原始对象或早期 AOP 代理) |
| 三级 | singletonFactories | ObjectFactory(工厂),用于按需生成早期引用 |
流程(A → B → A):
1. 创建 A:实例化 A(createBeanInstance),把"能产出 A 早期引用的 ObjectFactory"放入三级缓存
2. 填充 A 的属性 → 发现需要 B → 去创建 B
3. 创建 B:实例化 B,同样把 B 的 ObjectFactory 放入三级缓存
4. 填充 B 的属性 → 发现需要 A
5. getSingleton("A", true):
查一级(无) → 查二级(无) → 查三级:调用 A 的 ObjectFactory#getObject()
→ 得到 A 的早期引用(若 A 需要 AOP,则在此生成早期代理)
→ 放入二级缓存,并从三级缓存移除
6. B 拿到 A 的早期引用,完成填充 + 初始化 → B 进入一级缓存
7. A 拿到 B 完成填充 + 初始化 → A 进入一级缓存,同时清理二级缓存中的早期 A关键点:A 的引用在创建 B 的过程中就被"提前暴露"了,从而打破了"必须先完成 A 才能创建 B、必须先完成 B 才能创建 A"的死锁。
14. 为什么必须用"三级"缓存?二级缓存不够吗?
答: 这是本题的杀手锏追问,答案是:三级缓存的真正价值是"把 AOP 代理的生成延迟到被引用那一刻"。
- 若只有一、二级缓存:在实例化 A 之后就必须立刻决定并创建一个早期引用放入二级缓存。但此时 AOP 是否要生成代理、生成什么代理,尚未走到
postProcessAfterInitialization;更严重的是——如果这里放了原始对象,而最终成品是代理对象,就会出现"B 注入的是原始 A、容器里的却是代理 A"的不一致(@Transactional、@Async这类增强就会失效)。 - 有了三级缓存的
ObjectFactory:把"是否需要代理、生成哪个代理"的判断延后到getObject()被调用时执行;- 不需要 AOP → 直接返回原始对象;
- 需要 AOP → 返回早期代理,且该代理会被缓存并与最终成品保持同一个对象(
getEarlyBeanReference中登记,后续不再重复创建)。
// AbstractAutowireCapableBeanFactory#getEarlyBeanReference
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor ibp) {
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}一句话:二级缓存解决"提前暴露",三级缓存解决"暴露的是不是一个正确的(可能被代理的)对象";用工厂是为了把代理决策推迟,保证早期引用与最终成品一致。
15. 构造器循环依赖为什么无法解决?生产上如何避免/排查循环依赖?
答:构造器无解的原因:构造器注入要求在 new 之前就把依赖准备好——A 的构造需要 B,B 的构造需要 A,两个对象都还没实例化,也就没有任何"早期引用"可以暴露,三级缓存完全没有用武之地,只能抛异常。
避免与排查:
| 手段 | 说明 |
|---|---|
| 优先用构造器注入 | 让循环依赖在启动期 fail-fast,逼出设计问题(Spring 官方推荐) |
| 重构解环 | 抽取公共逻辑到第三个类、用事件/接口/中介者解耦、重新划分职责(首选) |
@Lazy 打破 | 在注入点加 @Lazy,注入代理、延迟到首次使用时解析(@Async 场景常需) |
@Autowired + setter/字段 | 换注入方式规避(但只是掩盖问题,Boot 2.6+ 还需打开 allow-circular-references) |
| 排查工具 | BeanCurrentlyInCreationException 的堆栈会打印依赖环;也可用 spring.main.allow-circular-references 之外的 ApplicationContext#getBean 分析;静态扫描用 Sonar/ArchUnit 检测环 |
架构视角:循环依赖几乎总是职责划分不清的信号——两个 Service 互相注入,通常意味着"共享的业务逻辑应该下移到更底层(Domain/Manager)",或者应该通过事件解耦。能重构就不要用
@Lazy掩盖。
本章小结:Spring 面试在 Bean 这块的"标准答案链"是:注入方式(推荐构造器)→ 生命周期(8 步 + 扩展点)→ 循环依赖(三级缓存 + 二三级差异 + Boot 2.6 默认禁止)。能把"为什么必须是三级"讲清楚,这一块就稳了。
