跳到主要内容
约 6 分钟2 张卡

IoC 容器

Spring IoC 容器根据配置创建对象、装配依赖,并管理对象的初始化与销毁。装配依赖,就是把对象需要的协作者传给它。本文先比较对象内部创建依赖与外部装配,再解释 IoC 和 DI,最后讨论测试替换、容器管理范围和单例线程安全。

1. 面试核心​

先记住这几条,再读正文
  • IoC 与 DI。 IoC 将对象创建与装配交给外部容器,DI 则通过构造器、工厂方法参数或属性把依赖传给对象。卡 · 依赖创建与外部装配
  • 定义与受管对象。 BeanDefinition 描述对象的创建方式和管理规则,Bean 是容器按定义创建并管理的对象。
  • 依赖取得方式。 构造器注入在签名中声明依赖,Service Locator 则由业务对象主动查询所需依赖。
  • 单例与线程安全。 Spring singleton 按容器和 Bean 定义共享实例,并发修改业务状态仍需同步。卡 · 单例与线程安全
  • 容器管理范围。 实体和值对象可以直接创建,需要依赖注入、代理或生命周期管理的组件才注册为 Bean。

2. 依赖创建与外部装配​

业务对象需要调用其他组件时,可以自己创建依赖,也可以接收外部传入的依赖。两种方式的区别在于谁决定具体实现。下面把同一个订单服务的两种写法放在一起比较:

// 写法一:业务类内部创建依赖
public final class OrderService {
private final PaymentClient paymentClient =
new HttpPaymentClient("https://pay.example.com");
}

// 写法二:通过构造器接收依赖,与上面的写法分别使用
public final class OrderService {
private final PaymentClient paymentClient;

public OrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
}
  • 第一种写法让 OrderService 同时决定业务流程、客户端实现和支付地址。测试时若要换成本地桩,或生产环境要更换地址,都需修改业务代码。
  • 第二种写法只声明需要一个 PaymentClient。创建哪种实现、使用哪个地址,由调用构造器的一方决定。

两种写法都通过 PaymentClient 调用支付能力。变化的是实现选择的位置:构造器注入把它移到应用组装对象的地方,也就是装配处。

3. IoC 与 DI​

**控制反转(IoC)**指创建对象和装配协作关系的控制权,从业务对象转移到外部容器。**依赖注入(DI)**是实现这种控制反转的方式。容器可以通过构造器、工厂方法参数或属性,把依赖传给对象,Setter 就是写入属性的一种方式。

业务类声明需要什么,Spring 再根据配置完成装配。容器管理包含以下工作:

  • 从 Java 配置、组件扫描等来源读取对象定义。
  • 根据类型、名称和限定符选择依赖。限定符用于进一步指定需要哪个实现。
  • 按作用域管理实例,并执行初始化和销毁回调。作用域决定哪些调用共享同一个对象。
  • 通过后处理器处理生命周期注解,或为事务、异步、方法校验等功能加入代理。

4. BeanDefinition 与实例​

BeanDefinition 保存创建对象所需的类型、依赖和生命周期配置,容器先注册定义,再按作用域创建 Bean 实例。

定义字段、创建时机,以及 @Bean 工厂方法的参数注入见 BeanDefinition。

5. 替换与测试​

依赖由外部传入后,单元测试就能直接替换它。桩对象用预设结果代替真实服务,下面让支付调用始终返回成功:

PaymentClient stub = request -> PaymentResult.approved();
OrderService service = new OrderService(stub);

测试通过构造器传入桩对象,就能验证业务对支付结果的处理,无需为这个单元测试启动 Spring。集成测试再检查组件扫描、条件配置和代理是否生效,以及数据库等真实资源能否正常使用。

集中装配也会增加容器的启动与诊断成本。可以借助启动记录和诊断工具查看 Bean 创建耗时、依赖关系和配置来源:

  • Bean 缺失时,检查定义来源,以及条件配置是否匹配。
  • 同一依赖出现多个候选 Bean 时,检查注册位置和选择规则。
  • 依赖成环时,沿依赖关系检查组件职责。

这些问题发生在容器组装对象时,因此需要查看定义与装配信息,单看业务类中的 new 调用无法定位完整原因。

6. 容器管理范围​

容器管理适合需要统一装配的组件,业务对象应通过清楚的接口声明依赖。业务类若主动调用 applicationContext.getBean(...) 查找对象,就是 **Service Locator(服务定位)**模式。此时依赖藏在方法内部,从构造器签名中看不到,测试替换和静态分析也更困难。

需要动态选择实现时,可以注入策略集合、ObjectProvider 或工厂接口:

  • Map<String, Strategy> 提供多个策略,让业务按名称选择。
  • ObjectProvider<T> 按需获取依赖,适合延迟获取或可选依赖。
  • 明确的工厂接口把创建或选择规则封装起来。

框架扩展和插件系统确实需要查询容器时,可以直接使用容器 API。实体、值对象和方法内的临时对象则可以直接 new。需要装配、代理、作用域或生命周期管理的组件,再注册为 Bean。

7. 单例与线程安全​

一个 Bean 被多个请求共用时,需要区分共享范围与状态保护。Spring singleton 表示同一容器内按 Bean 定义共享实例。同一类型可以注册多个 Bean,多个容器也可以各自创建实例。

容器会安全发布初始化结果,也就是让取得 Bean 的线程看见已完成的初始化。发布之后,业务字段的并发修改仍需按 Java 并发规则保护。比如 singleton Controller 的字段会被并发请求共享,把当前用户写入字段就会互相覆盖。

请求数据应保存在方法参数、局部变量或请求作用域对象中。共享的计数、缓存和集合,则需使用锁、原子变量或合适的并发容器。

prototype 表示每次向容器获取时创建新实例。如果它被注入 singleton 并长期持有,后续请求仍会使用那一个实例。因此,改作用域后还要检查实际获取和持有对象的方式。

8. 常见问题​

8.1 使用 Spring 后还可以直接 new 对象吗?​

可以。短生命周期的实体、值对象和方法内临时对象可以直接创建。需要容器注入、代理或生命周期回调的组件,则交给容器管理,才能获得这些功能。

8.2 IoC 能消除对象之间的耦合吗?​

9 · 面试题卡片​

9.1 Spring IoC 和依赖注入分别是什么​

出现公司:美团

考察重点:区分对象创建和装配的控制权与依赖传入方式,并说明定义、实例和生命周期之间的关系。

相关内容:依赖创建与外部装配至BeanDefinition 与实例。

参考回答

IoC 把对象创建、实现选择和生命周期管理交给外部容器,DI 则是容器把依赖传给对象的方式。

注入可以通过构造器、工厂方法参数或属性完成,因此业务类只需声明需要哪些协作者。

Spring 先读取并注册 BeanDefinition,再解析依赖、按作用域创建实例,并执行初始化等扩展点。实现选择集中到装配处后,更容易替换和测试。各组件仍需遵守相同的业务契约,应用也需要承担容器启动和诊断成本。

9.2 Spring singleton Bean 是否天然线程安全?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释 singleton 的共享范围,并区分初始化结果的可见性与运行时字段修改的线程安全。

相关内容:单例与线程安全。

参考回答

Spring singleton Bean 并不天然线程安全,多个请求会共享同一实例中的业务字段。

容器负责安全发布初始化结果,后续字段修改则由业务代码同步。

让 Bean 保持无状态,并把请求数据放进局部变量,可以减少共享状态。确需共享计数或集合时,要使用锁、原子变量或并发数据结构,按实际操作保证线程安全。

10. 参考资料​