【Spring】面试突击系列(一):IoC 与 DI 深度解析

Spring 面试突击系列(一):IoC 与 DI 深度解析

目录

一、面试考点速览

考点 面试频率 难度 核心考察点
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 执行");
    }
}

初始化顺序

  1. @PostConstruct(JSR-250 标准)
  2. InitializingBean.afterPropertiesSet(Spring 接口)
  3. init-method(XML 配置)

销毁顺序

  1. @PreDestroy(JSR-250 标准)
  2. DisposableBean.destroy(Spring 接口)
  3. 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?有什么好处?

标准回答结构

  1. 概念定义:IoC(控制反转)是一种设计思想,将对象的创建、依赖管理从程序代码转移到外部容器
  2. 对比说明 :传统方式是 new Object(),IoC 是容器创建好对象后注入
  3. 核心价值
    • 解耦:组件间依赖接口而非具体实现
    • 可测试:便于单元测试(可注入 Mock 对象)
    • 可维护:配置集中管理,修改实现只需改配置
    • 生命周期管理:容器统一管理 Bean 的创建、初始化、销毁
  4. 举例说明:以 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 在什么场景使用?

作用域列表

  1. singleton(默认):单例,容器中只有一个实例
  2. prototype:原型,每次获取都新建实例
  3. request:每次 HTTP 请求新建
  4. session:每次 HTTP 会话新建
  5. application:ServletContext 生命周期
  6. websocket:WebSocket 会话生命周期

prototype 使用场景

  1. 有状态 Bean:如计数器、购物车
  2. 线程不安全对象:如 SimpleDateFormat
  3. 需要独立实例的场景:如审计日志记录器(每个操作独立记录)
  4. 性能敏感场景:避免单例的锁竞争

示例:审计日志记录器用 prototype,确保每个操作日志独立,避免并发写入冲突。

五、常见错误与避坑指南

5.1 循环依赖问题

症状BeanCurrentlyInCreationException

原因 :A 依赖 B,B 又依赖 A

解决方案

  1. 重构设计,提取公共逻辑到第三个类
  2. 使用 @Lazy 延迟加载(治标不治本)
  3. 使用 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 最终存入 DefaultListableBeanFactorybeanDefinitionMap(ConcurrentHashMap)。

第五层:BeanFactoryPostProcessor 和 BeanPostProcessor 有什么区别?
对比维度 BeanFactoryPostProcessor BeanPostProcessor
操作对象 BeanDefinition(元数据) Bean 实例(对象)
执行时机 invokeBeanFactoryPostProcessors Bean 初始化前后
典型用途 修改 Bean 定义(占位符替换) 代理增强(AOP)、属性注入
经典实现 ConfigurationClassPostProcessorPropertySourcesPlaceholderConfigurer 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 不支持

源码层面@AutowiredpopulateBean 阶段由 AutowiredAnnotationBeanPostProcessor.postProcessProperties() 处理,通过反射扫描字段和方法的 @Autowired 注解,调用 beanFactory.resolveDependency() 解析依赖,解析时可能触发依赖 Bean 的创建(getBean),形成注入链。

第四层:多个同类型 Bean 时 Spring 怎么选?

决策链(优先级从高到低):

  1. 检查 @Primary 标记的 Bean
  2. 检查 @Qualifier 指定的名称
  3. 按参数字段名匹配
  4. 以上都失败 → 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?

三个原因:

  1. 非侵入性:不依赖 Spring 接口,纯 Java 标准,可移植到其他容器
  2. 单一职责:业务类不应该暴露"我是 Spring Bean"的实现细节
  3. 语义清晰 :方法可以自由命名(如 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 结尾,如 BeanNameAwareApplicationContextAware。实现对应的 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 判断并调用 ------ 处理 BeanNameAwareBeanClassLoaderAwareBeanFactoryAware
  • 路径二ApplicationContextAwareProcessor(一个 BeanPostProcessor)在 postProcessBeforeInitialization 中拦截 ------ 处理 ApplicationContextAwareEnvironmentAware
第五层: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

七、本章总结

核心要点回顾

  1. IoC 是思想,DI 是实现:IoC 强调控制反转,DI 是依赖注入的具体方式
  2. 构造器注入是最佳实践:不可变、线程安全、易于测试
  3. @Autowired 按类型,@Resource 按名称:优先使用 @Autowired
  4. 作用域根据状态选择:无状态用 singleton,有状态用 prototype
  5. 生命周期回调用 @PostConstruct/@PreDestroy:标准 Java 注解,不依赖 Spring

面试 checklist

  • 能说清 IoC 和 DI 的区别
  • 能对比三种注入方式的优劣
  • 能解释 @Autowired 和 @Resource 的区别
  • 能列举 Bean 的 6 种作用域
  • 能画出 Bean 生命周期的完整流程图
  • 能解决简单的循环依赖问题

下一步学习建议

  1. 动手实践:在 EduLearn 案例基础上,添加用户登录、权限验证功能
  2. 深入原理 :阅读 Spring 源码中 DefaultListableBeanFactory 的 Bean 创建流程
  3. 扩展知识:了解 Spring 5 对 IoC 容器的性能优化(如 bean 定义缓存)

下期第2期《SpringBoot 入门与自动配置原理》,将深入讲解 @SpringBootApplication 背后的魔法,以及如何自定义 Starter。


相关推荐
于先生吖1 小时前
前后端分离体育服务项目,场馆计费+线下赛事排行小程序部署开发教程
java·小程序·uni-app
RemainderTime1 小时前
Spring Boot脚手架集成 Spring Security实现生产级RBAC鉴权
spring boot·后端·spring
闪电悠米1 小时前
黑马点评-秒杀优化-01_async_seckill_idea
java·数据库·ide·redis·分布式·缓存·intellij-idea
摇滚侠1 小时前
IDEA 创建 Java 项目 lib 和 resources
java·ide·intellij-idea
I Promise341 小时前
智驾APA_HPA可行驶区域检测算法工程师面试问题整理可参考
算法·面试·职场和发展
宸津-代码粉碎机2 小时前
Spring AI企业级Agent实战|多工具自动规划+并行调度落地,彻底解决复杂业务AI任务编排问题
java·大数据·人工智能·spring boot·python·spring
lixia0417mul22 小时前
flink接入spring体系
java·spring·flink
biubiubiu07062 小时前
自定义starter 可以导入SpringBoot直接使用
java·spring boot·spring
TFHoney2 小时前
当 AI 真正走进你的终端:Claude Code 使用指南
java·人工智能·ai编程