Spring IoC 与 DI:控制反转、依赖注入、BeanFactory 和 ApplicationContext(081)

Spring与Spring Boot约 10 分钟阅读5,271 字符
知识库文档 82 / 251

一、IoC 不是"Spring 发明的",而是 Spring 让它普及的

控制反转(IoC, Inversion of Control)和依赖注入(DI, Dependency Injection)是 Spring 框架的基石。理解它们的本质,不只是为了"会用 Spring",更是为了理解为什么 Spring 选择这样的设计。

IoC 的核心思想:把"对象创建和依赖管理"的控制权从代码中反转给容器。在传统编程模式中,对象自己创建它依赖的对象(new);在 IoC 模式中,对象的依赖由容器注入。DI 是 IoC 的一种实现方式——容器通过构造器、Setter 或字段将依赖注入到对象中。

java
// 传统模式:对象自己控制依赖的创建
public class OrderService {
    private OrderRepository repo = new OrderRepositoryImpl();
}

// IoC 模式:容器注入依赖
public class OrderService {
    private final OrderRepository repo;
    public OrderService(OrderRepository repo) { this.repo = repo; }
}

IoC 带来的好处:解耦、可测试、可配置、生命周期管理。

[图6:Spring IoC核心概念全景图——IoC/DI/BeanFactory/ApplicationContext/生命周期/作用域的关系]

文章配图
文章配图

二、依赖注入的三种方式

构造器注入(推荐,Spring 4.3+ 单构造器自动注入):依赖不可变、强制依赖、易于测试、不需要 @Autowired。Setter 注入:可选依赖、可运行时修改。字段注入(不推荐):代码简洁但隐藏依赖、难以测试、依赖反射。

[图4:三种依赖注入方式对比图——构造器注入/Setter注入/字段注入的优缺点]

文章配图
文章配图

三、BeanFactory:Spring IoC 的基础容器

BeanFactory 是 Spring IoC 容器的根接口。核心职责:加载 BeanDefinition(从 XML/注解/Java 配置)、创建 Bean 实例(反射)、注入依赖(自动装配)、管理生命周期。

BeanFactory 采用延迟初始化(Lazy Initialization):Bean 在第一次 getBean() 时才创建,减少启动时间。BeanDefinition 是 Bean 的"蓝图"——记录类名、作用域、构造器参数、属性值、依赖引用等元数据。

[图1:BeanFactory架构图——BeanFactory→BeanDefinition→Bean实例的创建流程]

文章配图
文章配图

四、ApplicationContext:BeanFactory 的企业级扩展

ApplicationContext 继承 BeanFactory,提供更丰富的功能:国际化(MessageSource)、事件机制(ApplicationEvent)、资源加载(ResourceLoader)、环境抽象(Profile/Property)、AOP 自动创建代理、BeanPostProcessor 自动注册。

与 BeanFactory 不同,ApplicationContext 在启动时预初始化所有非懒加载的 singleton Bean。启动时间更长,但运行时不需要等待 Bean 创建。常见实现:AnnotationConfigApplicationContext(Java 配置)、ClassPathXmlApplicationContext(XML)、GenericWebApplicationContext(Web)等。

[图2:ApplicationContext层次结构图——继承BeanFactory,多个子实现的对比]

文章配图
文章配图

五、Bean 的生命周期

完整生命周期:1. 实例化(构造器)-> 2. 属性填充(@Autowired 注入)-> 3. BeanNameAware -> 4. BeanFactoryAware -> 5. ApplicationContextAware -> 6. BeanPostProcessor.postProcessBeforeInitialization -> 7. @PostConstruct / InitializingBean.afterPropertiesSet -> 8. BeanPostProcessor.postProcessAfterInitialization(AOP 代理在此创建)-> 9. Bean 就绪 -> 10. @PreDestroy / DisposableBean.destroy()(容器关闭时)。

理解生命周期对于初始化缓存/注册监听器、清理资源、理解 AOP 代理创建时机至关重要。

[图3:Bean生命周期图——实例化→属性填充→Aware回调→BeanPostProcessor→init→就绪→destroy]

文章配图
文章配图

[图5:IoC容器启动流程图——加载BeanDefinition→创建Bean→注入依赖→初始化→容器就绪]

文章配图
文章配图

六、Bean 的作用域

singleton(默认,容器中只有一个实例)、prototype(每次获取创建新实例)、request(每个 HTTP 请求一个实例)、session(每个 HTTP 会话一个实例)。

singleton 和 prototype 的陷阱:如果一个 singleton Bean 注入了 prototype Bean,prototype Bean 只在 singleton Bean 创建时注入一次。如果需要每次获取新的 prototype Bean,使用 @Lookup 注解或 ObjectFactory。

七、面试常见误区

  • "IoC 就是 DI" —— IoC 是思想,DI 是 IoC 的一种实现方式。
  • "ApplicationContext 就是 BeanFactory" —— ApplicationContext 是 BeanFactory 的子接口,提供更多企业级功能。
  • "singleton 就是单例模式" —— Spring 的 singleton 是"容器级别单例",与 GoF 的"JVM 级别单例"不同。
  • "构造器注入和 Setter 注入区别不大" —— 构造器注入更安全,是 Spring 官方推荐的方式。

八、总结

Spring IoC 的核心是"将控制权从代码中反转给容器":IoC 容器(BeanFactory/ApplicationContext)管理 Bean 的创建和依赖,BeanDefinition 是 Bean 的元数据蓝图,构造器注入是推荐的 DI 方式,Bean 生命周期从实例化到销毁有 10 个阶段。

理解 IoC 和 DI 的本质,是为了理解 Spring 的设计哲学——"你的代码不应该关心对象是怎么创建的,容器来帮你管理"。

延伸阅读

  • Spring 官方文档:Core Technologies - IoC Container
  • 《Spring 实战》第 1-3 章
  • 本系列相关篇目:082(Bean 生命周期)、083(BeanDefinition 与容器刷新)、084(Spring Bean 作用域)

面试官视角:提问与回答

以下问题从面试官视角出发,考察你对 Spring IoC 的深度理解和实战经验。

Q1:你说 Spring 推荐构造器注入,但如果一个 Bean 有 10 个依赖,构造器注入会让构造器参数列表很长。这是否意味着应该用 Setter 注入或字段注入?Spring 是如何处理这种情况的?

构造器参数太多说明这个类违反了单一职责原则——它承担了太多责任。解决方法是重构代码,将相关依赖聚合为一个新的 Bean(如将多个 Repository 聚合为一个 Facade)。Spring 本身不限制构造器参数数量,但 10 个参数的构造器是代码异味。如果确实无法重构,Spring 的 @Autowired 支持构造器注入,Lombok 的 @RequiredArgsConstructor 可以生成全参构造器,代码上不会太冗长。但 Setter 注入和字段注入不是解决方案——它们只是隐藏了问题,让依赖"不可见"而已。依赖多时要审视设计,而不是改变注入方式。

Q2:BeanFactory 和 ApplicationContext 都是 Spring 容器,但它们的启动行为不同——BeanFactory 延迟初始化,ApplicationContext 预初始化。如果一个 Bean 的创建需要 5 秒(比如需要连接数据库),在 ApplicationContext 中会发生什么?如何优化?

ApplicationContext 在启动时会预初始化所有非懒加载的 singleton Bean。如果某个 Bean 的创建需要 5 秒,启动时间就会延长 5 秒。优化方案:1)将该 Bean 标记为 @Lazy——容器启动时不创建,第一次使用时才创建。2)缩短 Bean 的初始化时间——比如数据库连接池的初始化通常很快,5 秒可能是在测试连接或执行初始化 SQL,可以优化这些操作。3)使用异步初始化——Spring Boot 2.2+ 支持 spring.main.lazy-initialization=true,全局延迟初始化。4)如果 Bean 之间有依赖关系,确保被依赖的 Bean 先初始化完成。

Q3:Spring 的 singleton Bean 是如何保证线程安全的?如果多个线程同时访问同一个 singleton Bean,会发生什么?

Spring 容器本身不保证 singleton Bean 的线程安全——singleton 只是"容器中只有一个实例",不代表这个实例的代码是线程安全的。线程安全由 Bean 的代码自己保证:1)如果 Bean 是无状态的(没有可变字段),天然线程安全——这是大多数 Service 和 Repository 的默认状态。2)如果 Bean 有可变字段,需要使用 synchronized、ReentrantLock、AtomicReference 等机制保证线程安全。3)Spring 的 request 和 session 作用域通过代理(ScopedProxyMode)在多线程环境中安全地注入 singleton Bean。如果多个线程同时访问同一个无状态的 singleton Bean,不会有任何问题——每个线程在各自的栈上执行方法,局部变量不共享。

Q4:你有一个 singleton Bean A 注入了 prototype Bean B。每次调用 A 的方法时,你期望拿到一个新的 B 实例,但实际上总是拿到同一个。为什么?怎么解决?

这是 Spring 的经典陷阱。singleton Bean A 在创建时,Spring 会注入它的所有依赖——包括 prototype Bean B。B 在 A 创建时被注入一次,之后 A 一直持有同一个 B 的引用。Spring 不会再为 A 创建新的 B 实例。解决方案:1)使用 @Lookup 注解——在 A 中定义一个抽象方法,返回 B 类型,Spring 通过 CGLIB 代理每次调用该方法时从容器中获取新的 B 实例。2)使用 ObjectFactory 或 ObjectProvider 来获取 B。3)使用 ApplicationContext.getBean(B.class)(不推荐,耦合到容器)。4)重新设计——如果 A 需要每次获取新的 B,说明 B 的职责与 A 不同,考虑将 B 的创建逻辑提取到独立的工厂中。

Q5:Spring 的 BeanPostProcessor 和 BeanFactoryPostProcessor 有什么区别?分别在什么时机执行?AOP 代理是哪个创建的?

BeanFactoryPostProcessor 在 Bean 实例化之前执行——在容器加载 BeanDefinition 之后、创建 Bean 实例之前。它用于修改 BeanDefinition(如 PropertySourcesPlaceholderConfigurer 替换 ${} 占位符)。BeanPostProcessor 在 Bean 实例化之后、初始化前后执行——postProcessBeforeInitialization(@PostConstruct 之前)和 postProcessAfterInitialization(@PostConstruct 之后、Bean 就绪之前)。AOP 代理由 AbstractAutoProxyCreator(一个 BeanPostProcessor)在 postProcessAfterInitialization 阶段创建——它检查 Bean 是否需要 AOP 代理,如果需要则创建 JDK 动态代理或 CGLIB 代理并返回代理对象。Spring 容器中最终持有的 Bean 引用就是这个代理对象,而不是原始 Bean。