Spring 面试突击系列(一):IoC 与 DI 深度解析
目录
- 一、面试考点速览
- 二、核心概念解析
- [2.1 什么是 IoC?(控制反转)](#2.1 什么是 IoC?(控制反转))
- [2.2 DI 的三种注入方式](#2.2 DI 的三种注入方式)
- [2.3 @Autowired vs @Resource](#2.3 @Autowired vs @Resource)
- [2.4 Bean 的作用域](#2.4 Bean 的作用域)
- [2.5 Bean 的生命周期](#2.5 Bean 的生命周期)
- [三、案例实战:EduLearn 用户注册模块](#三、案例实战:EduLearn 用户注册模块)
- [3.1 项目结构](#3.1 项目结构)
- [3.2 核心代码实现](#3.2 核心代码实现)
- [3.3 单元测试示例](#3.3 单元测试示例)
- 四、面试真题解析
- 五、常见错误与避坑指南
- 六、面试追问链设计
- 七、本章总结
一、面试考点速览
| 考点 | 面试频率 | 难度 | 核心考察点 |
|---|---|---|---|
| IoC 概念理解 | ⭐⭐⭐⭐⭐ | 初级 | 控制反转思想、解耦价值 |
| DI 三种注入方式 | ⭐⭐⭐⭐ | 初级 | 构造器/Setter/字段注入优劣 |
| @Autowired vs @Resource | ⭐⭐⭐⭐ | 初级 | 注解来源、注入策略、使用场景 |
| Bean 作用域 | ⭐⭐⭐ | 中级 | singleton/prototype 实际影响 |
| Bean 生命周期 | ⭐⭐⭐⭐ | 中级 | 实例化→属性赋值→初始化→销毁全流程 |
二、核心概念解析
2.1 什么是 IoC?(控制反转)
传统开发模式:
java
// 传统方式:自己创建依赖
public class UserService {
private PasswordEncoder passwordEncoder;
public UserService() {
this.passwordEncoder = new BCryptPasswordEncoder(); // 硬编码依赖
}
}
IoC 模式:
java
// IoC 方式:容器注入依赖
public class UserService {
private PasswordEncoder passwordEncoder;
// 构造器注入:容器负责创建 PasswordEncoder 并传入
public UserService(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
}
面试回答模板:
"IoC(控制反转)是一种设计思想,将对象的创建、依赖管理从程序代码中转移到外部容器。传统方式是我们主动 new 对象,IoC 是容器创建好对象后注入给我们。这样做的好处是解耦------UserService 不再关心 PasswordEncoder 的具体实现,只需声明依赖接口,容器负责提供具体实例。"
2.2 DI 的三种注入方式
方式一:构造器注入(Spring 官方推荐)
java
@Component
public class UserService {
private final PasswordEncoder passwordEncoder;
private final EmailSender emailSender;
// 构造器注入:依赖不可变,保证线程安全
public UserService(PasswordEncoder passwordEncoder, EmailSender emailSender) {
this.passwordEncoder = passwordEncoder;
this.emailSender = emailSender;
}
}
优点:
- 依赖不可变(
final字段) - 保证依赖完整(构造时即完成注入)
- 避免循环依赖(构造器注入会直接报错)
- 易于单元测试(直接传 mock 对象)
方式二:Setter 注入
java
@Component
public class UserService {
private PasswordEncoder passwordEncoder;
private EmailSender emailSender;
// Setter 注入:依赖可变
@Autowired
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Autowired
public void setEmailSender(EmailSender emailSender) {
this.emailSender = emailSender;
}
}
适用场景:
- 可选依赖(非必需)
- 需要重新配置的场景
方式三:字段注入(不推荐)
java
@Component
public class UserService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private EmailSender emailSender;
}
缺点:
- 依赖可变,非
final - 隐藏依赖关系(无法从构造器看出需要哪些依赖)
- 难以单元测试(需反射或 Spring 容器)
- 可能引发空指针(如果容器未正确初始化)
面试对比表:
| 特性 | 构造器注入 | Setter 注入 | 字段注入 |
|---|---|---|---|
| 不可变性 | ✅ (final) | ❌ | ❌ |
| 循环依赖检测 | ✅ (立即报错) | ⚠️ (运行时) | ⚠️ (运行时) |
| 单元测试便利性 | ✅ (直接传参) | ✅ (直接调用 setter) | ❌ (需反射) |
| Spring 官方推荐 | ✅ | ⚠️ | ❌ |
| 代码简洁度 | ⚠️ | ⚠️ | ✅ |
2.3 @Autowired vs @Resource
@Autowired(Spring 原生)
java
@Component
public class UserService {
@Autowired // 默认按类型注入
private PasswordEncoder passwordEncoder;
@Autowired
@Qualifier("bcryptEncoder") // 按名称注入
private PasswordEncoder specificEncoder;
}
- 来源:Spring 框架
- 默认策略:按类型(byType)注入
- required 属性 :
@Autowired(required = false)表示可选依赖 - 配合注解 :
@Qualifier指定具体 Bean 名称
@Resource(JSR-250)
java
@Component
public class UserService {
@Resource // 默认按名称注入
private PasswordEncoder passwordEncoder;
@Resource(name = "bcryptEncoder") // 显式指定名称
private PasswordEncoder specificEncoder;
}
- 来源:Java EE(JSR-250),Spring 也支持
- 默认策略:按名称(byName)注入,找不到则回退到按类型
- 无 required 属性:找不到会抛异常
- 无 Qualifier 配合 :直接使用
name属性
面试对比表:
| 特性 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring 框架 | Java EE 标准(JSR-250) |
| 默认注入策略 | 按类型(byType) | 按名称(byName) |
| 名称指定方式 | @Qualifier("name") |
name = "name" 属性 |
| 可选依赖支持 | required = false |
不支持(找不到即报错) |
| 推荐使用场景 | Spring 项目内部 | 需要兼容其他容器的项目 |
一句话总结 :@Autowired 是 Spring 亲儿子,按类型注入更符合 Spring 哲学;@Resource 是 Java 标准,按名称注入更直观。实际开发中统一使用 @Autowired 即可,除非有特殊兼容需求。
2.4 Bean 的作用域
Spring 支持 6 种 Bean 作用域:
| 作用域 | 说明 | 适用场景 |
|---|---|---|
| singleton | 单例(默认) | 无状态服务、工具类、配置类 |
| prototype | 原型(每次获取新建) | 有状态对象、线程不安全对象 |
| request | 每次 HTTP 请求新建 | Web 应用,请求级别数据 |
| session | 每次 HTTP 会话新建 | Web 应用,用户会话数据 |
| application | ServletContext 生命周期 | Web 应用,全局共享数据 |
| websocket | WebSocket 会话生命周期 | WebSocket 连接数据 |
singleton vs prototype 实战对比
java
// 配置类
@Configuration
public class BeanConfig {
@Bean
@Scope("singleton") // 默认,可不写
public AuditLogger singletonLogger() {
return new AuditLogger("SINGLETON");
}
@Bean
@Scope("prototype")
public AuditLogger prototypeLogger() {
return new AuditLogger("PROTOTYPE-" + System.currentTimeMillis());
}
}
// 测试类
@Component
public class ScopeTest {
@Autowired
private AuditLogger singletonLogger1;
@Autowired
private AuditLogger singletonLogger2; // 与 singletonLogger1 是同一实例
@Autowired
private ApplicationContext context;
public void test() {
AuditLogger proto1 = context.getBean(AuditLogger.class); // 新建实例
AuditLogger proto2 = context.getBean(AuditLogger.class); // 再新建实例
// proto1 != proto2
}
}
面试常见问题:
- Q:什么时候用 prototype?
- A:当 Bean 有状态(如计数器)、线程不安全(如 SimpleDateFormat),或每次需要新实例时(如审计日志记录器,每个操作独立记录)。
2.5 Bean 的生命周期
Bean 从创建到销毁的完整流程:

关键阶段代码演示
java
@Component
public class UserService implements
BeanNameAware, // 阶段1:Aware 接口回调
ApplicationContextAware,
InitializingBean, // 阶段2:InitializingBean 接口
DisposableBean { // 阶段3:DisposableBean 接口
private String beanName;
private ApplicationContext context;
// === 1. Aware 接口注入 ===
@Override
public void setBeanName(String name) {
this.beanName = name;
System.out.println("1. BeanNameAware: " + name);
}
@Override
public void setApplicationContext(ApplicationContext context) {
this.context = context;
System.out.println("2. ApplicationContextAware");
}
// === 2. 属性注入 ===
@Autowired
private PasswordEncoder passwordEncoder;
// === 3. @PostConstruct(推荐) ===
@PostConstruct
public void postConstruct() {
System.out.println("3. @PostConstruct 执行");
}
// === 4. InitializingBean 接口 ===
@Override
public void afterPropertiesSet() {
System.out.println("4. InitializingBean.afterPropertiesSet");
}
// === 5. init-method(XML 配置方式) ===
public void initMethod() {
System.out.println("5. init-method 执行");
}
// === 6. 业务方法 ===
public void registerUser(String username, String password) {
String encoded = passwordEncoder.encode(password);
System.out.println("注册用户: " + username);
}
// === 7. @PreDestroy ===
@PreDestroy
public void preDestroy() {
System.out.println("7. @PreDestroy 执行");
}
// === 8. DisposableBean 接口 ===
@Override
public void destroy() {
System.out.println("8. DisposableBean.destroy");
}
// === 9. destroy-method ===
public void destroyMethod() {
System.out.println("9. destroy-method 执行");
}
}
初始化顺序:
@PostConstruct(JSR-250 标准)InitializingBean.afterPropertiesSet(Spring 接口)init-method(XML 配置)
销毁顺序:
@PreDestroy(JSR-250 标准)DisposableBean.destroy(Spring 接口)destroy-method(XML 配置)
面试要点:
- 推荐使用
@PostConstruct和@PreDestroy:标准 Java 注解,不依赖 Spring 接口 - 避免混合使用:选择一种方式即可,避免执行顺序混淆
三、案例实战:EduLearn 用户注册模块
3.1 项目结构
src/main/java/com/edulearn/
├── user/
│ ├── UserController.java # REST 控制器
│ ├── UserService.java # 业务服务(构造器注入示例)
│ ├── PasswordEncoder.java # 密码编码接口
│ ├── BCryptPasswordEncoder.java # 具体实现
│ ├── EmailSender.java # 邮件发送接口
│ ├── AuditLogger.java # 审计日志(prototype 示例)
│ └── User.java # 实体类
└── Application.java # SpringBoot 启动类
3.2 核心代码实现
UserService(构造器注入最佳实践)
java
@Service
public class UserService {
private final PasswordEncoder passwordEncoder;
private final EmailSender emailSender;
private final AuditLogger auditLogger;
// 构造器注入:所有依赖通过参数传入
public UserService(
PasswordEncoder passwordEncoder,
EmailSender emailSender,
AuditLogger auditLogger) {
this.passwordEncoder = passwordEncoder;
this.emailSender = emailSender;
this.auditLogger = auditLogger;
}
public User register(String username, String rawPassword, String email) {
// 1. 密码加密
String encodedPassword = passwordEncoder.encode(rawPassword);
// 2. 创建用户
User user = new User(username, encodedPassword, email);
// 3. 发送欢迎邮件(异步)
emailSender.sendWelcomeEmail(email, username);
// 4. 记录审计日志(每个注册独立实例)
auditLogger.log("USER_REGISTER", username);
return user;
}
}
PasswordEncoder 接口与实现
java
public interface PasswordEncoder {
String encode(String rawPassword);
boolean matches(String rawPassword, String encodedPassword);
}
@Component("bcryptEncoder") // 指定 Bean 名称
public class BCryptPasswordEncoder implements PasswordEncoder {
private final BCryptPasswordEncoder delegate = new BCryptPasswordEncoder();
@Override
public String encode(String rawPassword) {
return delegate.encode(rawPassword);
}
@Override
public boolean matches(String rawPassword, String encodedPassword) {
return delegate.matches(rawPassword, encodedPassword);
}
}
// 备用实现:测试环境使用
@Component("simpleEncoder")
@Profile("test") // 仅在 test 环境生效
public class SimplePasswordEncoder implements PasswordEncoder {
@Override
public String encode(String rawPassword) {
return "encoded_" + rawPassword; // 简单编码,仅测试用
}
@Override
public boolean matches(String rawPassword, String encodedPassword) {
return encode(rawPassword).equals(encodedPassword);
}
}
AuditLogger(prototype 作用域演示)
java
@Component
@Scope("prototype") // 每次获取都是新实例
public class AuditLogger {
private final String instanceId;
private final List<String> logs = new ArrayList<>();
public AuditLogger() {
this.instanceId = "AUDIT-" + UUID.randomUUID().toString().substring(0, 8);
System.out.println("创建 AuditLogger 实例: " + instanceId);
}
public void log(String action, String username) {
String log = String.format("[%s] %s - %s",
LocalDateTime.now(), action, username);
logs.add(log);
System.out.println(log);
}
public List<String> getLogs() {
return new ArrayList<>(logs); // 返回副本,保护内部状态
}
}
循环依赖示例与解决方案
java
// ❌ 错误示例:构造器循环依赖
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; }
}
@Service
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) { this.serviceA = serviceA; }
}
// 启动报错:BeanCurrentlyInCreationException
// ✅ 解决方案1:使用 @Lazy 延迟加载
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(@Lazy ServiceB serviceB) { this.serviceB = serviceB; }
}
// ✅ 解决方案2:使用 Setter 注入(不推荐,隐藏问题)
@Service
public class ServiceA {
private ServiceB serviceB;
@Autowired
public void setServiceB(ServiceB serviceB) { this.serviceB = serviceB; }
}
// ✅ 解决方案3:重构设计,提取公共逻辑到第三个类
3.3 单元测试示例
java
@SpringBootTest
class UserServiceTest {
@MockBean
private PasswordEncoder passwordEncoder;
@MockBean
private EmailSender emailSender;
@MockBean
private AuditLogger auditLogger;
@Autowired
private UserService userService;
@Test
void testRegisterUser() {
// 1. 准备 Mock 行为
when(passwordEncoder.encode("password123"))
.thenReturn("encoded_password");
// 2. 执行测试
User user = userService.register("testuser", "password123", "test@example.com");
// 3. 验证结果
assertNotNull(user);
assertEquals("testuser", user.getUsername());
assertEquals("encoded_password", user.getPassword());
// 4. 验证依赖调用
verify(passwordEncoder).encode("password123");
verify(emailSender).sendWelcomeEmail("test@example.com", "testuser");
verify(auditLogger).log("USER_REGISTER", "testuser");
}
}
四、面试真题解析
真题1:什么是 Spring IoC?有什么好处?
标准回答结构:
- 概念定义:IoC(控制反转)是一种设计思想,将对象的创建、依赖管理从程序代码转移到外部容器
- 对比说明 :传统方式是
new Object(),IoC 是容器创建好对象后注入 - 核心价值 :
- 解耦:组件间依赖接口而非具体实现
- 可测试:便于单元测试(可注入 Mock 对象)
- 可维护:配置集中管理,修改实现只需改配置
- 生命周期管理:容器统一管理 Bean 的创建、初始化、销毁
- 举例说明:以 UserService 依赖 PasswordEncoder 为例,展示硬编码 vs IoC 的区别
真题2:@Autowired 和 @Resource 有什么区别?
对比表回答法:
| 维度 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring 框架 | Java EE 标准(JSR-250) |
| 默认策略 | 按类型注入 | 按名称注入(找不到则按类型) |
| 名称指定 | 需配合 @Qualifier | 直接使用 name 属性 |
| 可选依赖 | 支持(required=false) | 不支持(找不到即报错) |
| 推荐场景 | Spring 项目内部 | 需兼容其他容器的项目 |
补充说明:
- 实际开发中统一使用
@Autowired即可 - 按类型注入更符合 Spring 的"面向接口编程"哲学
- 当有多个同类型 Bean 时,用
@Qualifier指定具体名称
真题3:Bean 的作用域有哪些?prototype 在什么场景使用?
作用域列表:
- singleton(默认):单例,容器中只有一个实例
- prototype:原型,每次获取都新建实例
- request:每次 HTTP 请求新建
- session:每次 HTTP 会话新建
- application:ServletContext 生命周期
- websocket:WebSocket 会话生命周期
prototype 使用场景:
- 有状态 Bean:如计数器、购物车
- 线程不安全对象:如 SimpleDateFormat
- 需要独立实例的场景:如审计日志记录器(每个操作独立记录)
- 性能敏感场景:避免单例的锁竞争
示例:审计日志记录器用 prototype,确保每个操作日志独立,避免并发写入冲突。
五、常见错误与避坑指南
5.1 循环依赖问题
症状 :BeanCurrentlyInCreationException
原因 :A 依赖 B,B 又依赖 A
解决方案:
- 重构设计,提取公共逻辑到第三个类
- 使用
@Lazy延迟加载(治标不治本) - 使用 Setter 注入(不推荐,隐藏设计问题)
5.2 @Autowired required=false 的陷阱
java
@Component
public class UserService {
@Autowired(required = false) // 可选依赖
private PremiumFeature premiumFeature;
public void someMethod() {
if (premiumFeature != null) { // 必须判空!
premiumFeature.doSomething();
}
}
}
注意 :required=false 时,依赖可能为 null,使用时必须判空。
5.3 字段注入的单元测试困境
java
@Component
public class UserService {
@Autowired
private PasswordEncoder passwordEncoder; // 字段注入
public void encodePassword(String raw) {
passwordEncoder.encode(raw); // 单元测试时需反射注入 Mock
}
}
单元测试代码:
java
@Test
void testEncodePassword() throws Exception {
UserService service = new UserService();
PasswordEncoder mockEncoder = mock(PasswordEncoder.class);
// 需要反射设置私有字段
Field field = UserService.class.getDeclaredField("passwordEncoder");
field.setAccessible(true);
field.set(service, mockEncoder);
// 这才开始测试...
}
结论:使用构造器注入,单元测试直接传参即可。
六、面试追问链设计
面试官不会只问表面问题------他们会从一个简单概念切入,然后逐层深挖,直到问到你不会为止。本节针对本章涉及的 6 个核心考点,每个准备 3~5 层追问答案,帮你建立完整的知识树。
6.1 IoC 控制反转(5 层追问)
第一层:什么是 IoC?
标准回答 :IoC(Inversion of Control,控制反转)是一种设计原则,核心是把对象的创建权和依赖管理权从调用方转移给容器 。传统方式下对象自己 new 依赖;IoC 方式下容器负责创建和注入。一句话概括:Don't call us, we'll call you。
第二层:IoC 容器有哪几种?有什么区别?
| 容器 | 类型 | 加载时机 | 适用场景 |
|---|---|---|---|
| BeanFactory | 接口 | 延迟加载(首次 getBean 时) |
内存敏感场景 |
| ApplicationContext | 接口(继承 BeanFactory) | 预加载(启动时初始化所有单例 Bean) | 99% 日常开发 |
关键差异 :ApplicationContext 额外支持国际化(MessageSource)、事件发布(ApplicationEvent)、资源加载(ResourceLoader)、自动 BeanPostProcessor 注册和 AOP 集成。常用的实现包括 AnnotationConfigApplicationContext(注解驱动)和 Spring Boot 内置的 AnnotationConfigServletWebServerApplicationContext。
第三层:ApplicationContext 启动时做了什么?
从 new AnnotationConfigApplicationContext(AppConfig.class) 开始,核心是 refresh() 方法的 13 个步骤:
1. prepareRefresh() --- 准备上下文环境
2. obtainFreshBeanFactory() --- 获取 BeanFactory
3. prepareBeanFactory() --- 配置 BeanFactory(类加载器、SPEL)
4. postProcessBeanFactory() --- 子类扩展点(空实现)
5. invokeBeanFactoryPostProcessors() --- ★ 执行 BFPP(解析 @Configuration、@ComponentScan)
6. registerBeanPostProcessors() --- 注册 BPP(不执行,只注册)
7. initMessageSource() --- 国际化
8. initApplicationEventMulticaster()--- 事件广播器
9. onRefresh() --- 子类扩展(Spring Boot 在此启动 Tomcat)
10. registerListeners() --- 注册事件监听器
11. finishBeanFactoryInitialization() --- ★ 实例化所有非懒加载单例 Bean
12. finishRefresh() --- 发布 ContextRefreshedEvent
记住第 5 步和第 11 步最关键:一个处理 Bean 的"图纸"(BeanDefinition),一个处理 Bean 的"成品"(实例)。
第四层:BeanDefinition 是怎么被注册的?
Spring 通过 BeanDefinition 描述 Bean 元数据(类名、作用域、是否懒加载、构造器参数等)。注册路径有四种:
| 方式 | 解析器 | 说明 |
|---|---|---|
@Component / @Service |
ClassPathBeanDefinitionScanner |
扫描指定包路径 |
@Bean 方法 |
ConfigurationClassBeanDefinitionReader |
解析 @Configuration 类 |
@Import |
ConfigurationClassParser |
导入配置类或 ImportSelector |
XML <bean> |
XmlBeanDefinitionReader |
解析 XML 配置 |
所有 BeanDefinition 最终存入 DefaultListableBeanFactory 的 beanDefinitionMap(ConcurrentHashMap)。
第五层:BeanFactoryPostProcessor 和 BeanPostProcessor 有什么区别?
| 对比维度 | BeanFactoryPostProcessor | BeanPostProcessor |
|---|---|---|
| 操作对象 | BeanDefinition(元数据) | Bean 实例(对象) |
| 执行时机 | invokeBeanFactoryPostProcessors |
Bean 初始化前后 |
| 典型用途 | 修改 Bean 定义(占位符替换) | 代理增强(AOP)、属性注入 |
| 经典实现 | ConfigurationClassPostProcessor、PropertySourcesPlaceholderConfigurer |
AutowiredAnnotationBeanPostProcessor、AOP 代理创建器 |
一句话区分:BFPP 处理的是"图纸",BPP 处理的是"成品"。
6.2 DI 依赖注入(5 层追问)
第一层:DI 有哪几种注入方式?
构造器注入、Setter 注入、字段注入。前文 2.2 节已详述,此处不再重复。
第二层:构造器注入为什么最好?
(本章 2.2 节已对比过优劣,这里补充面试官的心理视角)
面试官追问这个问题时,他要的不是你背出表格,而是看你是否理解 "不可变性"的价值:
- 构造器注入可以
private final,对象一旦创建就不能变------这是函数式编程思想在 Java 中的体现 - Spring 官方文档明确说:"use constructors for mandatory dependencies and setter methods for optional dependencies"
- 字段注入唯一的优势是代码短,但这牺牲了可测试性和安全性
第三层:@Autowired 和 @Resource 的底层实现有什么区别?
| 对比维度 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring 原生 | JSR-250(javax.annotation) |
| 默认策略 | byType | byName(找不到再 byType) |
| 多 Bean 处理 | @Primary → @Qualifier → 参数名 |
直接按名称,没有回退链 |
| 处理器 | AutowiredAnnotationBeanPostProcessor |
CommonAnnotationBeanPostProcessor |
| required | 支持 required = false |
不支持 |
源码层面 :@Autowired 在 populateBean 阶段由 AutowiredAnnotationBeanPostProcessor.postProcessProperties() 处理,通过反射扫描字段和方法的 @Autowired 注解,调用 beanFactory.resolveDependency() 解析依赖,解析时可能触发依赖 Bean 的创建(getBean),形成注入链。
第四层:多个同类型 Bean 时 Spring 怎么选?
决策链(优先级从高到低):
- 检查
@Primary标记的 Bean - 检查
@Qualifier指定的名称 - 按参数字段名匹配
- 以上都失败 →
NoUniqueBeanDefinitionException
追问 :"@Primary 和 @Qualifier 同时存在时谁生效?"→ @Qualifier 优先级更高,因为它更精确。
第五层:@Autowired 为什么能注入?源码是怎么做的?
AutowiredAnnotationBeanPostProcessor.postProcessProperties()
→ findAutowiringMetadata() 反射扫描 @Autowired 字段/方法 → 缓存
→ metadata.inject()
→ beanFactory.resolveDependency()
→ doResolveDependency() 按类型查找候选 Bean
→ 多 Bean 时走 @Primary / @Qualifier / 参数名匹配
→ beanFactory.getBean(beanName) ← 可能触发依赖 Bean 的创建
关键点:元数据会被缓存 到 injectionMetadataCache,同一个类的后续实例不再反射。
6.3 循环依赖(5 层追问)
第一层:什么是循环依赖?Spring 能解决吗?
循环依赖指两个或多个 Bean 互相依赖形成闭环。Spring 只解决单例 Bean 的 setter/字段注入循环依赖 ,构造器注入和 prototype 作用域的循环依赖无法解决。
第二层:为什么构造器注入无法解决循环依赖?
因为构造器注入要求在实例化的同时完成注入,中间不存在"半成品"状态:
创建 CourseService → 需要 UserService(构造器参数)
→ 创建 UserService → 需要 CourseService(构造器参数)
→ 发现 CourseService 正在创建中,没有半成品可用
→ BeanCurrentlyInCreationException
构造器的实例化+注入是原子操作,三级缓存派不上用场。
第三层:Spring 三级缓存怎么解决 setter 循环依赖?
这是面试的深水分水岭,必须精准复述三级缓存:
java
// DefaultSingletonBeanRegistry
一级缓存 singletonObjects --- 完全初始化好的 Bean(成品)
二级缓存 earlySingletonObjects --- 提前暴露的半成品
三级缓存 singletonFactories --- ObjectFactory,用于生成早期引用/代理
完整流程(以 A ↔ B 循环为例):
1. getBean("A")
→ createBean → doCreateBean
→ 实例化 A(new A(),半成品)
→ ★ 放入三级缓存:singletonFactories.put("A", () -> getEarlyBeanReference("A"))
→ populateBean("A") → 需要注入 B
→ getBean("B") → createBean → doCreateBean
→ 实例化 B(半成品)
→ ★ 放入三级缓存:singletonFactories.put("B", ...)
→ populateBean("B") → 需要注入 A
→ getBean("A") → getSingleton("A")
→ 三级缓存中找到 A 的 ObjectFactory
→ 调用 getEarlyBeanReference → 获取早期引用
→ 移入二级缓存,从三级缓存移除
→ B 注入 A 成功
→ initializeBean("A")
→ ★ 放入一级缓存,从二/三级移除
第四层:为什么要有三级缓存?二级不行吗?
核心原因:AOP 代理的时机问题。 如果只有二级缓存,直接放半成品实例:
A ↔ B 循环依赖:
1. 创建 A 实例,直接放二级缓存(原始对象)
2. 创建 B,注入 A → 拿到原始 A
3. A 初始化时发现需要 AOP → 创建 A 代理对象
4. 结果:B 里的 A 是原始对象,容器里是代理 → 不一致!
三级缓存的 ObjectFactory 通过 getEarlyBeanReference() 延迟决定是否需要生成代理,确保循环依赖中拿到的对象和最终对象是同一个。
第五层:循环依赖有什么坑?怎么避免?
| 坑 | 现象 | 原因 |
|---|---|---|
| 构造器循环依赖 | BeanCurrentlyInCreationException |
无法暴露半成品 |
@Async + 循环依赖 |
代理类型不匹配 | 早期引用拿到原始对象 |
@Transactional + 循环依赖 |
事务失效 | AOP 代理顺序问题 |
最佳方案 :重构设计消除循环依赖。治标方案:@Lazy 延迟注入或改用 Setter 注入。
6.4 @PostConstruct vs InitializingBean(5 层追问)
第一层:有什么区别?
| 对比 | @PostConstruct | InitializingBean |
|---|---|---|
| 来源 | JSR-250 | Spring 原生 |
| 侵入性 | 低(注解,可任意命名方法) | 高(实现接口,耦合 Spring) |
| 推荐 | ✅ | ❌ |
第二层:和 init-method 一起用时,执行顺序是什么?
@PostConstruct ← 第 1 执行(BPP.postProcessBeforeInitialization)
InitializingBean.afterPropertiesSet() ← 第 2 执行
init-method ← 第 3 执行
源码位置 :AbstractAutowireCapableBeanFactory.initializeBean() → applyBeanPostProcessorsBeforeInitialization()(@PostConstruct)→ invokeInitMethods()(afterPropertiesSet → init-method)。
第三层:为什么 Spring 推荐 @PostConstruct 而不是 InitializingBean?
三个原因:
- 非侵入性:不依赖 Spring 接口,纯 Java 标准,可移植到其他容器
- 单一职责:业务类不应该暴露"我是 Spring Bean"的实现细节
- 语义清晰 :方法可以自由命名(如
initCache()、loadConfig()),可读性远优于afterPropertiesSet()
第四层:Spring 是怎么扫描到 @PostConstruct 的?
CommonAnnotationBeanPostProcessor 继承 InitDestroyAnnotationBeanPostProcessor,在其构造函数中设置 setInitAnnotationType(PostConstruct.class)。执行时通过 findLifecycleMetadata() 反射扫描类的所有方法,找到 @PostConstruct 注解的方法并缓存,然后 method.invoke(target) 执行。
第五层:@PostConstruct 方法里抛异常会怎样?
Bean 创建失败,容器可能无法启动。异常传播链:@PostConstruct 方法抛出 → InitDestroyAnnotationBeanPostProcessor 不捕获 → AbstractAutowireCapableBeanFactory 包装为 BeanCreationException → 核心 Bean 失败则容器启动失败。
最佳实践 :初始化方法中用 try-catch 包裹外部调用,不应让初始化异常向上传播。
6.5 Aware 接口(5 层追问)
第一层:什么是 Aware 接口?
Aware 是一组回调接口 ,让 Bean 感知 Spring 容器的底层组件。名字以 Aware 结尾,如 BeanNameAware、ApplicationContextAware。实现对应的 setXxx() 方法,Spring 在初始化阶段自动调用。
第二层:常用 Aware 接口有哪些?
| Aware 接口 | 注入内容 | 典型场景 |
|---|---|---|
BeanNameAware |
Bean 的 ID/名称 | 日志输出 |
BeanFactoryAware |
当前 BeanFactory |
编程式获取 Bean |
ApplicationContextAware |
ApplicationContext |
获取容器、发布事件 |
EnvironmentAware |
Environment |
读取配置属性 |
ApplicationEventPublisherAware |
事件发布器 | 发布自定义事件 |
第三层:Aware 回调在生命周期哪个阶段?
在 @PostConstruct 之前。 准确位置:属性填充完成之后,BeanPostProcessor 的 postProcessBeforeInitialization 阶段,@PostConstruct 执行之前。
属性填充完成
→ invokeAwareMethods() ← BeanNameAware、BeanClassLoaderAware、BeanFactoryAware
→ ApplicationContextAwareProcessor ← ApplicationContextAware 等
→ @PostConstruct ← 从这里开始才是"初始化方法"
→ InitializingBean.afterPropertiesSet()
→ init-method
第四层:Aware 是怎么注入的------它不用 @Autowired 为什么也能拿到?
两条路径:
- 路径一 :
AbstractAutowireCapableBeanFactory.invokeAwareMethods()直接 instanceof 判断并调用 ------ 处理BeanNameAware、BeanClassLoaderAware、BeanFactoryAware - 路径二 :
ApplicationContextAwareProcessor(一个 BeanPostProcessor)在postProcessBeforeInitialization中拦截 ------ 处理ApplicationContextAware、EnvironmentAware等
第五层:Aware 有什么坑?
最大风险 :让 Bean 依赖容器上下文,本质是 Service Locator 模式,破坏了 IoC 原则。
java
// ❌ 反面教材:通过 ApplicationContext 主动拉取 Bean
ctx.getBean(UserService.class); // 破坏 DI
// ✅ 正确姿势:直接注入需要的组件
@Autowired
private ApplicationEventPublisher eventPublisher;
- Aware 注入的组件在单元测试中难以 Mock
- Aware 回调发生在 Bean 完全初始化之前,此时其他 Bean 可能未就绪
6.6 Bean 生命周期全景(终极串联)
当面试官问"说一下 Spring Bean 的完整生命周期",你需要把所有知识点串联起来:
【实例化】
反射调用构造器 newInstance() → 半成品对象
【属性填充】
→ populateBean()
→ AutowiredAnnotationBeanPostProcessor 处理 @Autowired、@Value
【初始化】(最复杂的阶段)
→ invokeAwareMethods() ★ Aware 回调
→ BeanNameAware / BeanClassLoaderAware / BeanFactoryAware
→ BeanPostProcessor.postProcessBeforeInitialization()
→ ApplicationContextAwareProcessor ★ Aware 回调(ApplicationContext 等)
→ InitDestroyAnnotationBeanPostProcessor ★ @PostConstruct 在此执行
→ invokeInitMethods()
→ InitializingBean.afterPropertiesSet()
→ init-method
→ BeanPostProcessor.postProcessAfterInitialization()
→ AOP 代理创建 ★
【就绪】
→ Bean 放入 singletonObjects(一级缓存)
→ 等待业务调用
【销毁】
→ @PreDestroy → DisposableBean.destroy() → destroy-method
面试话术:
"Spring Bean 的生命周期可以分为实例化、属性填充、初始化、就绪、销毁五个阶段。Aware 接口回调在属性填充之后、初始化方法之前。@PostConstruct 由 CommonAnnotationBeanPostProcessor 在 postProcessBeforeInitialization 中执行,排在 InitializingBean 和 init-method 之前。AOP 代理创建在初始化之后的 postProcessAfterInitialization 中完成。销毁阶段按 @PreDestroy → DisposableBean.destroy() → destroy-method 的顺序执行。"
面试追问链速查表
| 入口问题 | 追问方向 | 深度 |
|---|---|---|
| 什么是 IoC? | → 容器类型 → refresh() 13 步 → BeanDefinition → BFPP vs BPP | 5 层 |
| DI 有几种方式? | → 优劣对比 → @Autowired/@Resource 区别 → 多 Bean 选择 → 源码 | 5 层 |
| 循环依赖? | → 构造器为何不行 → 三级缓存 → 为什么需要 ObjectFactory → 项目坑 | 5 层 |
| @PostConstruct? | → 与 InitializingBean 区别 → 执行顺序 → 推荐原因 → 源码扫描 → 异常处理 | 5 层 |
| Aware 接口? | → 常用 Aware → 生命周期位置 → 注入机制 → 项目风险 | 5 层 |
| Bean 生命周期? | → 5 阶段串联 → BFPP/BPP 位置 → 销毁顺序 | 4 层 |
准备建议 :面试前把上面每个追问链用自己的话说一遍,重点理解为什么这样设计 ,而不是死记硬背。面试官更在意你能否解释 Spring 的设计意图和 trade-off。
七、本章总结
核心要点回顾
- IoC 是思想,DI 是实现:IoC 强调控制反转,DI 是依赖注入的具体方式
- 构造器注入是最佳实践:不可变、线程安全、易于测试
- @Autowired 按类型,@Resource 按名称:优先使用 @Autowired
- 作用域根据状态选择:无状态用 singleton,有状态用 prototype
- 生命周期回调用 @PostConstruct/@PreDestroy:标准 Java 注解,不依赖 Spring
面试 checklist
- 能说清 IoC 和 DI 的区别
- 能对比三种注入方式的优劣
- 能解释 @Autowired 和 @Resource 的区别
- 能列举 Bean 的 6 种作用域
- 能画出 Bean 生命周期的完整流程图
- 能解决简单的循环依赖问题
下一步学习建议
- 动手实践:在 EduLearn 案例基础上,添加用户登录、权限验证功能
- 深入原理 :阅读 Spring 源码中
DefaultListableBeanFactory的 Bean 创建流程 - 扩展知识:了解 Spring 5 对 IoC 容器的性能优化(如 bean 定义缓存)
下期 :第2期《SpringBoot 入门与自动配置原理》,将深入讲解 @SpringBootApplication 背后的魔法,以及如何自定义 Starter。