Note 01. Spring 设计哲学与 IoC 容器一文带你理解spring框架的设计理念


Note 01. IoC 容器与依赖注入:Spring 的设计基石

摘要:为什么我们不能直接 new 对象?Spring 的 IoC 容器到底在帮我们做什么?本章将从一个 “坏代码” 的案例出发,逐步揭示控制反转(IoC)的设计哲学,深入理解依赖注入(DI)的三种方式及其设计权衡,并剖析 Spring 容器的内部结构。读完本章,你将建立起对 Spring 核心设计的完整心智模型。

本章学习路径

  1. 问题驱动:通过 “坏代码” 案例,理解传统对象创建方式的致命缺陷。
  2. 思想升级:掌握 IoC 的本质——一场对象创建责任的转移。
  3. 落地方式:深入理解 DI 的三种注入方式及 Spring 官方的推荐理由。
  4. 容器认知:区分 BeanFactory 与 ApplicationContext,理解容器的层级设计。

重要信息: 概念章节一共有五章,我们的起步是您至少对于 Springboot 已经有过了使用经验,我们的核心意图在于为您规划一个体系化的梳理路线,而不是纯零基础友好笔记


1.1. 从一个 “坏代码” 说起:为什么不能直接 new 对象

在正式进入 Spring 的世界之前,我们先来看一段 “看起来没问题” 的代码。这段代码在很多初学者的项目中随处可见,但它却隐藏着严重的设计缺陷。

1.1.1. 一个典型的订单服务

假设我们正在开发一个电商系统,需要实现一个订单服务。订单创建时,需要校验库存、计算价格、保存订单、发送通知。一个 “直觉式” 的实现可能是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 订单服务 - 一个典型的 "坏代码" 示例
* 问题:所有依赖都通过 new 直接创建
*/
public class OrderService {

// 直接 new 出所有依赖对象
private InventoryService inventoryService = new InventoryService();
private PriceCalculator priceCalculator = new PriceCalculator();
private OrderRepository orderRepository = new OrderRepository();
private NotificationService notificationService = new EmailNotificationService();

public Order createOrder(OrderRequest request) {
// 1. 校验库存
if (!inventoryService.checkStock(request.getProductId())) {
throw new BusinessException("库存不足");
}

// 2. 计算价格
BigDecimal totalPrice = priceCalculator.calculate(request);

// 3. 创建并保存订单
Order order = new Order(request, totalPrice);
orderRepository.save(order);

// 4. 发送通知
notificationService.sendOrderConfirmation(order);

return order;
}
}

这段代码能正常运行,业务逻辑也很清晰。但当项目逐渐复杂,你会发现自己陷入了一个又一个的困境。

1.1.2. 困境一:无法进行单元测试

“我想测试 createOrder 方法的业务逻辑,但每次运行测试都会真的去查数据库、发邮件,怎么办?”

问题的根源在于:OrderService 内部直接 new 出了所有依赖。我们无法在测试时将 OrderRepository 替换成一个假的(Mock)实现。

1
2
3
4
5
6
7
// 我们期望的测试方式
@Test
void shouldCreateOrderSuccessfully() {
// 我想用一个假的 Repository,让它直接返回成功
// 但 OrderService 内部已经写死了 new OrderRepository()
// 我根本无法替换它!
}

1.1.3. 困境二:修改需求导致代码 “牵一发动全身”

产品经理说:“我们要支持短信通知了,不只是邮件。”

现在你需要修改 OrderService 的代码:

1
2
3
4
5
// 修改前
private NotificationService notificationService = new EmailNotificationService();

// 修改后 - 但这要求修改 OrderService 的源代码!
private NotificationService notificationService = new SmsNotificationService();

更糟糕的是,如果系统中有 50 个地方都 new EmailNotificationService(),你需要改 50 处。这直接违反了软件设计的 开闭原则(OCP):对扩展开放,对修改关闭。

1.1.4. 困境三:对象创建逻辑散落各处

随着系统演进,InventoryService 的构造变得复杂了——它需要一个数据库连接池、一个缓存客户端、一个配置对象:

1
2
3
4
5
6
// InventoryService 的构造变复杂了
public class InventoryService {
public InventoryService(DataSource dataSource, RedisClient redis, InventoryConfig config) {
// ...
}
}

现在,所有 new InventoryService() 的地方都要改成:

1
2
3
4
5
private InventoryService inventoryService = new InventoryService(
new HikariDataSource(hikariConfig), // 还要先创建 hikariConfig
new RedisClient(redisConfig), // 还要先创建 redisConfig
new InventoryConfig()
);

对象的创建逻辑像病毒一样扩散到了业务代码的每个角落。

1.1.5. 问题的本质:职责错位

让我们用一张图来总结这段 “坏代码” 的问题:

img

OrderService 本应只关心 “如何处理订单” 这一件事,但现在它还要操心:

  • 每个依赖对象怎么创建
  • 每个依赖对象需要什么参数
  • 应该选择哪个具体实现类

这就是 职责错位——业务类承担了本不属于它的 “对象创建” 职责。


1.2. 控制反转(IoC):一场责任的转移

上一节我们看到了直接 new 对象带来的种种问题。解决这些问题的核心思想,就是 控制反转(Inversion of Control,IoC)

1.2.1. IoC 的本质:从 “主动索取” 到 “被动接收”

“控制反转” 这个名字听起来很抽象,但它的核心思想其实非常简单:

不要让业务类自己去创建依赖,而是让外部把依赖 “送” 进来。

我们来对比一下 “反转前” 和 “反转后” 的代码:

反转前:主动索取

1
2
3
4
5
6
public class OrderService {
// OrderService 主动创建自己需要的依赖
private OrderRepository repository = new OrderRepository();

// "我需要什么,我自己去拿"
}

反转后:被动接收

1
2
3
4
5
6
7
8
9
10
11
public class OrderService {
// OrderService 只声明自己需要什么,不关心怎么来的
private OrderRepository repository;

// 依赖从外部 "注入" 进来
public OrderService(OrderRepository repository) {
this.repository = repository;
}

// "我需要什么,你给我送来"
}

这个看似微小的改变,带来了质的飞跃:

对比维度反转前(主动索取)反转后(被动接收)
依赖关系编译时确定,写死在代码里运行时确定,可灵活替换
可测试性无法 Mock,必须用真实依赖轻松注入 Mock 对象
扩展性换实现要改源码换实现只需改配置
职责分离业务类兼任 “对象工厂”业务类只关心业务

1.2.2. IoC 容器的角色:对象的创建者、管理者、协调者

既然业务类不再负责创建依赖,那 “谁” 来负责呢?答案是:IoC 容器

Spring 的 IoC 容器扮演着三重角色:

mermaid-diagram-2026-01-03-101330

角色一:对象创建者

容器负责实例化所有被管理的对象(在 Spring 中称为 Bean)。我们不再写 new OrderService(),而是向容器 “要” 一个 OrderService

角色二:生命周期管理者

容器不仅创建对象,还管理对象的整个生命周期——初始化、使用、销毁。这让我们可以在对象生命周期的关键节点插入自定义逻辑(如资源初始化、连接池预热、资源释放等)。

角色三:依赖关系协调者

容器会分析所有对象之间的依赖关系,并按照正确的顺序创建和装配它们。比如,容器知道要先创建 OrderRepository,再创建 OrderService,然后把前者注入到后者中。

1.2.3. 设计模式视角:IoC 与工厂模式、服务定位器的对比

IoC 并不是凭空出现的,它与几种经典设计模式有着密切的关系。理解它们的异同,能帮助我们更深刻地把握 IoC 的设计精髓。

方案一:简单工厂模式

1
2
3
4
5
6
7
8
9
10
public class ServiceFactory {
public static OrderRepository createOrderRepository() {
return new OrderRepository(dataSource);
}
}

// 使用方式
public class OrderService {
private OrderRepository repository = ServiceFactory.createOrderRepository();
}

工厂模式将对象创建逻辑集中到了工厂类中,解决了 “创建逻辑散落各处” 的问题。但 OrderService 仍然 主动调用 工厂来获取依赖,依赖关系仍然是硬编码的。

方案二:服务定位器模式

1
2
3
4
5
6
7
8
9
10
11
12
public class ServiceLocator {
private static Map<Class<?>, Object> services = new HashMap<>();

public static <T> T getService(Class<T> type) {
return (T) services.get(type);
}
}

// 使用方式
public class OrderService {
private OrderRepository repository = ServiceLocator.getService(OrderRepository.class);
}

服务定位器提供了一个全局的 “服务注册表”,对象可以从中查找自己需要的依赖。但问题在于:

  • OrderService 仍然 主动查找 依赖
  • 依赖关系被隐藏在代码内部,从类的接口(构造函数、方法签名)上看不出来
  • 难以进行单元测试(需要预先配置好 ServiceLocator)

方案三:IoC / 依赖注入

1
2
3
4
5
6
7
8
public class OrderService {
private final OrderRepository repository;

// 依赖通过构造函数 "注入" 进来
public OrderService(OrderRepository repository) {
this.repository = repository;
}
}

IoC 的革命性在于:对象完全不知道依赖从哪里来。它只是声明 “我需要一个 OrderRepository”,至于这个对象是怎么创建的、是真实的还是 Mock 的,它完全不关心。

三种方案的对比:

image-20260103101748973

对比维度简单工厂服务定位器IoC/DI
依赖获取方式主动调用主动查找被动接收
依赖可见性隐藏在实现中隐藏在实现中显式声明在接口上
可测试性需要 Mock 工厂需要配置定位器直接传入 Mock
代码侵入性依赖工厂类依赖定位器类无任何依赖

1.3. 依赖注入(DI):IoC 思想的落地方式

IoC 是一种设计思想,而 依赖注入(Dependency Injection,DI) 是实现这种思想最主流的方式。但在深入讨论注入方式之前,我们需要先回答一个更基础的问题:Spring 到底在 “注入” 什么?

1.3.1. 前置知识:什么是 Bean?

在 Spring 的世界里,Bean 是一个被反复提及的核心概念。但很多初学者对它的理解仅停留在 “就是一个 Java 对象” 的层面。这种理解虽然不算错,但过于肤浅,无法帮助我们真正理解 Spring 的设计。

Bean 的精确定义

Bean 是 由 Spring IoC 容器创建、管理和装配的对象

这个定义包含三个关键动作:

  • 创建:对象的实例化由容器负责,而不是我们手动 new
  • 管理:对象的整个生命周期(从出生到死亡)由容器掌控
  • 装配:对象之间的依赖关系由容器负责建立

Bean 与普通 Java 对象的本质区别

1
2
3
4
5
// 这是一个普通的 Java 对象
User user = new User();

// 这是一个 Bean(由容器创建和管理)
User user = applicationContext.getBean(User.class);

表面上看,两者都是 User 类型的对象。但它们的 “身份” 完全不同:

对比维度普通 Java 对象Spring Bean
创建者开发者手动 newSpring 容器通过反射创建
生命周期由 JVM 垃圾回收决定由容器管理(可配置单例、原型等)
依赖关系手动建立容器自动装配
增强能力可被 AOP 代理、事务管理等增强
可见性仅在创建它的作用域内可见在整个容器范围内可被获取

一个形象的比喻

如果把 Spring 容器比作一个 “人才市场”,那么:

  • 普通 Java 对象:就像你自己在街上随便找的临时工,你要自己面试、自己培训、自己管理,用完就散
  • Spring Bean:就像通过正规人才市场招聘的员工,市场帮你筛选、培训、建立档案,你只需要说 “我要一个会 Java 的”,市场就给你送来一个合格的

什么样的对象应该成为 Bean?

并不是所有对象都适合交给 Spring 管理。一般来说,以下类型的对象适合成为 Bean:

image-20260103112359301

简单的判断原则

  • 应该成为 Bean:需要被多处复用、需要依赖其他组件、需要被容器管理生命周期的对象
  • 不应该成为 Bean:纯粹承载数据的对象(如实体类、DTO)、无状态的工具类

1.3.2. 依赖注入的本质:容器如何 “送货上门”

理解了 Bean 的概念后,我们再来看依赖注入就清晰多了。

依赖注入的定义

依赖注入是指:容器在创建 Bean 的过程中,自动将该 Bean 所依赖的其他 Bean “注入” 到它内部

用更通俗的话说:你只需要告诉容器 “我需要什么”,容器就会把你需要的东西 “送货上门”。

从 “自己去拿” 到 “送货上门” 的转变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ❌ 传统方式:自己去 "拿" 依赖
public class OrderService {
private OrderRepository repository;

public OrderService() {
// 我要自己去创建依赖
this.repository = new OrderRepository();
}
}

// ✅ 依赖注入:等着 "送货上门"
public class OrderService {
private OrderRepository repository;

// 容器会把 OrderRepository 送过来
public OrderService(OrderRepository repository) {
this.repository = repository;
}
}

这个转变看似简单,但它彻底改变了对象之间的协作方式:

mermaid-diagram-2026-01-03-112556

1.3.3. 三种注入方式的演进与设计权衡

Spring 支持三种依赖注入方式,它们的出现有其历史背景,各有适用场景。

方式一:Setter 注入(Spring 早期的主流方式)

历史背景

在 Spring 1.x 和 2.x 时代,Setter 注入是最主流的方式。这与当时的技术背景有关:

  • 早期 Spring 主要使用 XML 配置,XML 中配置 <property> 标签来调用 setter 方法非常直观
  • JavaBean 规范深入人心,getter/setter 是标准的属性访问方式
  • 当时的 IDE 对构造器参数的支持不如现在完善

XML 时代的 Setter 注入配置

1
2
3
4
5
6
7
8
9
10
<!-- Spring 早期的典型配置方式 -->
<bean id="orderRepository" class="com.example.repository.OrderRepository">
<property name="dataSource" ref="dataSource"/>
</bean>

<bean id="orderService" class="com.example.service.OrderService">
<!-- 通过 property 标签调用 setOrderRepository 方法 -->
<property name="orderRepository" ref="orderRepository"/>
<property name="inventoryService" ref="inventoryService"/>
</bean>

对应的 Java 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class OrderService {

private OrderRepository orderRepository;
private InventoryService inventoryService;

// Spring 容器会调用这个方法注入依赖
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}

// Spring 容器会调用这个方法注入依赖
public void setInventoryService(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}

public void createOrder(OrderRequest request) {
// 使用注入的依赖
inventoryService.checkStock(request.getProductId());
orderRepository.save(new Order(request));
}
}

注解时代的 Setter 注入

随着 Spring 2.5 引入注解支持,Setter 注入也可以用注解方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class OrderService {

private OrderRepository orderRepository;
private InventoryService inventoryService;

@Autowired
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}

@Autowired
public void setInventoryService(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
}

Setter 注入的特点分析

优点缺点
可以注入可选依赖(依赖可以为 null)无法保证依赖的不可变性(没有 final)
可以在对象创建后重新注入(灵活)对象可能处于 “部分初始化” 状态
XML 配置时语义清晰依赖关系分散在多个 setter 中,不够直观
可以解决循环依赖问题容易遗漏必需的依赖

现代开发中的定位

在现代 Spring 开发中,Setter 注入主要用于 可选依赖 的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Service
public class NotificationService {

// 必需依赖:使用构造器注入
private final MessageSender primarySender;

// 可选依赖:使用 Setter 注入
private MessageSender backupSender;

public NotificationService(MessageSender primarySender) {
this.primarySender = primarySender;
}

// 备用发送器是可选的,可能不配置
@Autowired(required = false)
public void setBackupSender(MessageSender backupSender) {
this.backupSender = backupSender;
}

public void send(String message) {
try {
primarySender.send(message);
} catch (Exception e) {
if (backupSender != null) {
backupSender.send(message);
}
}
}
}

方式二:字段注入(最简洁但不推荐)

出现背景

字段注入随着 Spring 2.5 的 @Autowired 注解一起出现。它的写法极其简洁,因此迅速流行起来,尤其在快速开发和教程示例中被大量使用。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class OrderService {

@Autowired
private OrderRepository orderRepository;

@Autowired
private InventoryService inventoryService;

@Autowired
private NotificationService notificationService;

public void createOrder(OrderRequest request) {
// 直接使用,看起来很简洁
inventoryService.checkStock(request.getProductId());
Order order = new Order(request);
orderRepository.save(order);
notificationService.sendConfirmation(order);
}
}

为什么字段注入如此流行?

  1. 代码最少:不需要写构造器,不需要写 setter,一个注解搞定
  2. 添加依赖成本低:需要新依赖?加一行 @Autowired private XxxService xxx; 就行
  3. 教程友好:示例代码简短,便于展示核心逻辑

为什么 Spring 官方不推荐字段注入?

尽管字段注入写起来很爽,但它有几个严重的设计缺陷:

缺陷一:无法使用 final,破坏不可变性

1
2
3
4
5
6
7
@Service
public class OrderService {

// ❌ 编译错误!final 字段必须在构造器中初始化
@Autowired
private final OrderRepository orderRepository;
}

这涉及 Java 语言的基础知识:

  • final 字段必须在 对象构造完成之前 被赋值
  • 字段注入是在对象构造 完成之后,通过反射强行赋值的
  • 因此,字段注入与 final 在语义上是冲突的

缺陷二:依赖关系被隐藏,违反显式依赖原则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 从类的公开接口(构造器、方法签名)完全看不出它依赖什么
@Service
public class OrderService {

@Autowired
private OrderRepository orderRepository;

@Autowired
private InventoryService inventoryService;

@Autowired
private PriceCalculator priceCalculator;

@Autowired
private NotificationService notificationService;

@Autowired
private AuditService auditService;

// 这个类到底依赖多少东西?必须看源码才知道
}

缺陷三:单元测试困难

1
2
3
4
5
6
7
8
9
// 如何在不启动 Spring 容器的情况下测试 OrderService?
@Test
void testCreateOrder() {
// ❌ 问题:OrderService 没有公开的方式让我们传入 Mock 对象
OrderService service = new OrderService(); // 所有 @Autowired 字段都是 null!

// 必须使用反射或者 Spring 测试框架
// 这增加了测试的复杂度和运行时间
}

对比构造器注入的测试:

1
2
3
4
5
6
7
8
9
10
@Test
void testCreateOrder() {
// ✅ 简单直接:直接传入 Mock 对象
OrderRepository mockRepo = mock(OrderRepository.class);
InventoryService mockInventory = mock(InventoryService.class);

OrderService service = new OrderService(mockRepo, mockInventory);

// 测试逻辑...
}

缺陷四:容易导致类职责膨胀

字段注入的 “低成本” 反而成了一个陷阱:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 加一个依赖太容易了,不知不觉就变成了这样
@Service
public class OrderService {

@Autowired private OrderRepository orderRepository;
@Autowired private InventoryService inventoryService;
@Autowired private PriceCalculator priceCalculator;
@Autowired private DiscountService discountService;
@Autowired private TaxCalculator taxCalculator;
@Autowired private NotificationService notificationService;
@Autowired private AuditService auditService;
@Autowired private MetricsService metricsService;
@Autowired private CacheService cacheService;
@Autowired private EventPublisher eventPublisher;
// 10 个依赖!这个类是不是管太多了?
// 但因为每个依赖只是一行代码,问题被掩盖了
}

方式三:构造器注入(现代 Spring 的推荐方式)

历史演进

构造器注入并不是新概念,Spring 从 1.x 版本就支持。但在早期,由于 XML 配置构造器参数比较繁琐,它不如 Setter 注入流行。

1
2
3
4
5
<!-- 早期 XML 配置构造器注入,需要指定参数顺序或名称 -->
<bean id="orderService" class="com.example.service.OrderService">
<constructor-arg index="0" ref="orderRepository"/>
<constructor-arg index="1" ref="inventoryService"/>
</bean>

随着注解配置的普及,特别是 Spring 4.3 引入的 “单构造器自动注入” 特性,构造器注入变得非常简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class OrderService {

private final OrderRepository orderRepository;
private final InventoryService inventoryService;

// Spring 4.3+:只有一个构造器时,@Autowired 可以省略
public OrderService(OrderRepository orderRepository,
InventoryService inventoryService) {
this.orderRepository = orderRepository;
this.inventoryService = inventoryService;
}
}

配合 Lombok 进一步简化

在实际项目中,我们通常配合 Lombok 的 @RequiredArgsConstructor 注解,让代码更加简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
@RequiredArgsConstructor // Lombok 自动生成包含所有 final 字段的构造器
public class OrderService {

private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final NotificationService notificationService;

// 不需要手写构造器,Lombok 会自动生成:
// public OrderService(OrderRepository orderRepository,
// InventoryService inventoryService,
// NotificationService notificationService) {
// this.orderRepository = orderRepository;
// this.inventoryService = inventoryService;
// this.notificationService = notificationService;
// }

public void createOrder(OrderRequest request) {
// 业务逻辑
}
}

1.3.4. 三种注入方式的全面对比

现在我们已经详细了解了三种注入方式,让我们做一个全面的对比:

对比维度构造器注入Setter 注入字段注入
Spring 官方态度⭐⭐⭐⭐⭐ 强烈推荐⭐⭐⭐ 可选依赖时使用⭐⭐ 不推荐
不可变性(final)✅ 支持❌ 不支持❌ 不支持
依赖可见性✅ 构造器签名即依赖清单⚠️ 分散在多个方法❌ 隐藏在类内部
空指针安全✅ 创建时即完整❌ 可能部分初始化❌ 可能为 null
单元测试✅ 直接 new 传参⚠️ 需调用多个 setter❌ 需要反射或容器
循环依赖❌ 无法解决✅ 可解决✅ 可解决
代码量⚠️ 参数多时较长⚠️ 方法多时较长✅ 最少
IDE 支持✅ 重构友好✅ 重构友好⚠️ 反射注入,重构需谨慎

1.3.5. @Autowired 的依赖查找机制

当我们使用 @Autowired 注解时,Spring 容器需要找到正确的 Bean 来注入。这个查找过程遵循一套明确的规则。

第一步:按类型查找(byType)

Spring 首先会在容器中查找与目标类型匹配的 Bean:

1
2
3
4
5
6
7
@Service
public class OrderService {

@Autowired
private OrderRepository orderRepository;
// Spring 查找:容器中有没有类型为 OrderRepository 的 Bean?
}

第二步:处理多个候选者

如果容器中存在多个相同类型的 Bean,Spring 会进入更精细的筛选流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 假设容器中有两个 MessageSender 类型的 Bean
@Component("emailSender")
public class EmailSender implements MessageSender { }

@Component("smsSender")
public class SmsSender implements MessageSender { }

@Service
public class NotificationService {

@Autowired
private MessageSender messageSender;
// 问题:容器中有两个 MessageSender,注入哪个?
}

解决多候选者的三种方式

方式一:利用字段名匹配 Bean 名称

1
2
3
4
5
6
7
8
9
10
11
@Service
public class NotificationService {

@Autowired
private MessageSender emailSender; // 字段名是 emailSender
// Spring 会尝试匹配名为 "emailSender" 的 Bean ✅

@Autowired
private MessageSender smsSender; // 字段名是 smsSender
// Spring 会尝试匹配名为 "smsSender" 的 Bean ✅
}

方式二:使用 @Qualifier 显式指定

1
2
3
4
5
6
7
8
9
10
11
@Service
public class NotificationService {

@Autowired
@Qualifier("emailSender") // 明确指定要注入的 Bean 名称
private MessageSender primarySender;

@Autowired
@Qualifier("smsSender")
private MessageSender backupSender;
}

方式三:使用 @Primary 标记默认 Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@Primary // 标记为首选 Bean
public class EmailSender implements MessageSender { }

@Component
public class SmsSender implements MessageSender { }

@Service
public class NotificationService {

@Autowired
private MessageSender messageSender;
// 没有 @Qualifier 时,会注入 @Primary 标记的 EmailSender
}

完整的查找流程图

mermaid-diagram-2026-01-03-111121

构造器注入中的 @Qualifier 使用

在构造器注入中,@Qualifier 需要放在参数上:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class NotificationService {

private final MessageSender primarySender;
private final MessageSender backupSender;

public NotificationService(
@Qualifier("emailSender") MessageSender primarySender,
@Qualifier("smsSender") MessageSender backupSender) {
this.primarySender = primarySender;
this.backupSender = backupSender;
}
}

1.4. Spring 容器的两张面孔:BeanFactory 与 ApplicationContext

在前面的章节中,我们多次提到 “Spring 容器” 这个概念。现在是时候深入了解它的内部结构了。Spring 提供了两个核心的容器接口:BeanFactoryApplicationContext。理解它们的关系和区别,是深入理解 Spring 架构的关键。

1.4.1. 为什么需要 “容器”?从手工作坊到自动化工厂

在理解 Spring 容器之前,让我们先思考一个问题:如果没有容器,我们如何管理对象?

场景:一个中型电商系统的对象创建

假设我们有一个订单模块,涉及以下组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
OrderController
└── OrderService
├── OrderRepository
│ └── DataSource
├── InventoryService
│ └── InventoryRepository
│ └── DataSource
├── PriceCalculator
│ └── DiscountService
│ └── DiscountRepository
│ └── DataSource
└── NotificationService
├── EmailSender
│ └── MailConfig
└── SmsSender
└── SmsConfig

没有容器时的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Application {

public static void main(String[] args) {
// 1. 创建基础设施
DataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/shop");
dataSource.setUsername("root");
dataSource.setPassword("password");

MailConfig mailConfig = new MailConfig();
mailConfig.setHost("smtp.example.com");
mailConfig.setPort(587);

SmsConfig smsConfig = new SmsConfig();
smsConfig.setApiKey("xxx-xxx-xxx");

// 2. 创建 Repository 层
OrderRepository orderRepository = new OrderRepository(dataSource);
InventoryRepository inventoryRepository = new InventoryRepository(dataSource);
DiscountRepository discountRepository = new DiscountRepository(dataSource);

// 3. 创建 Service 层
DiscountService discountService = new DiscountService(discountRepository);
PriceCalculator priceCalculator = new PriceCalculator(discountService);
InventoryService inventoryService = new InventoryService(inventoryRepository);

EmailSender emailSender = new EmailSender(mailConfig);
SmsSender smsSender = new SmsSender(smsConfig);
NotificationService notificationService = new NotificationService(emailSender, smsSender);

OrderService orderService = new OrderService(
orderRepository, inventoryService, priceCalculator, notificationService);

// 4. 创建 Controller 层
OrderController orderController = new OrderController(orderService);

// 5. 启动应用...
// 6. 应用关闭时,还要记得关闭 DataSource...
}
}

这段代码的问题

  1. 创建顺序必须手动管理:必须先创建 DataSource,才能创建 Repository,才能创建 Service…
  2. 依赖关系硬编码:如果 OrderService 需要新增一个依赖,这里就要改
  3. 生命周期手动管理:DataSource 需要在应用关闭时释放连接,谁来负责?
  4. 无法灵活替换:想把 EmailSender 换成 MockEmailSender 做测试?改代码吧
  5. 代码膨胀:随着系统增长,这个 “组装代码” 会越来越长

容器的价值:自动化的对象管理

Spring 容器的核心价值就是 将上述所有工作自动化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 有了 Spring 容器,启动代码变成这样
@SpringBootApplication
public class Application {

public static void main(String[] args) {
// 一行代码,容器自动完成:
// 1. 扫描所有组件
// 2. 分析依赖关系
// 3. 按正确顺序创建对象
// 4. 自动装配依赖
// 5. 管理生命周期
SpringApplication.run(Application.class, args);
}
}

容器就像一个 自动化工厂,我们只需要告诉它 “有哪些零件”(通过注解或配置),它就能自动完成 “组装” 工作。

mermaid-diagram-2026-01-03-113828

1.4.2. BeanFactory:IoC 容器的最小契约

BeanFactory 是 Spring IoC 容器的 根接口,它定义了容器最基本、最核心的功能。可以把它理解为容器的 “最低标准”——任何 Spring 容器都必须实现这些基本能力。

BeanFactory 接口定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public interface BeanFactory {

// ==== ==== ==== ==== ==== 获取 Bean 的方法 ==== ==== ==== ==== ====

// 根据名称获取 Bean(返回 Object,需要强转)
Object getBean(String name) throws BeansException;

// 根据名称和类型获取 Bean(类型安全)
<T> T getBean(String name, Class<T> requiredType) throws BeansException;

// 根据类型获取 Bean(最常用)
<T> T getBean(Class<T> requiredType) throws BeansException;

// 根据名称获取 Bean,并传入构造参数(用于 prototype 作用域)
Object getBean(String name, Object... args) throws BeansException;


// ==== ==== ==== ==== ==== 查询 Bean 的方法 ==== ==== ==== ==== ====

// 判断容器中是否包含指定名称的 Bean
boolean containsBean(String name);

// 判断指定 Bean 是否为单例
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;

// 判断指定 Bean 是否为原型(每次获取都创建新实例)
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

// 获取指定 Bean 的类型
Class<?> getType(String name) throws NoSuchBeanDefinitionException;

// 获取指定 Bean 的所有别名
String[] getAliases(String name);
}

BeanFactory 的设计哲学

BeanFactory 的设计遵循 最小化原则——它只定义了 “获取 Bean” 和 “查询 Bean 信息” 这两类最基本的操作,不包含任何 “高级” 功能。

这种设计有几个好处:

  1. 职责单一:BeanFactory 只负责 “Bean 的获取”,不掺杂其他功能
  2. 易于实现:第三方可以轻松实现一个最小化的 IoC 容器
  3. 资源友好:在资源受限的环境(如早期的移动设备)中,可以使用轻量级实现

BeanFactory 的核心特点

特点说明
延迟加载(Lazy Loading)Bean 在第一次被请求时才创建,而不是容器启动时就创建
最小功能集只提供 Bean 的获取和查询,不支持 AOP、事件、国际化等
资源占用小适合资源受限的环境
需要手动注册后置处理器BeanPostProcessor 等扩展点需要手动注册

BeanFactory 的使用示例(了解即可)

在现代 Spring 开发中,我们几乎不会直接使用 BeanFactory。但了解它的用法有助于理解容器的本质:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 早期 Spring 使用 BeanFactory 的方式(现已很少使用)
public class BeanFactoryExample {

public static void main(String[] args) {
// 1. 创建 BeanFactory 实例
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

// 2. 创建 Bean 定义读取器
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);

// 3. 加载 Bean 定义
reader.loadBeanDefinitions(new ClassPathResource("beans.xml"));

// 4. 获取 Bean(此时才真正创建 Bean 实例)
OrderService orderService = beanFactory.getBean(OrderService.class);

// 注意:BeanPostProcessor 需要手动注册
// beanFactory.addBeanPostProcessor(new MyBeanPostProcessor());
}
}

1.4.3. ApplicationContext:企业级功能的全面扩展

ApplicationContextBeanFactory 的子接口,它在 BeanFactory 的基础上扩展了大量企业级应用所需的功能。在早期的spring框架实际开发中,我们使用的几乎都是 ApplicationContext

ApplicationContext 的接口继承体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public interface ApplicationContext extends
EnvironmentCapable, // 环境配置能力(profiles、properties)
ListableBeanFactory, // 批量获取 Bean 的能力
HierarchicalBeanFactory, // 父子容器层级能力
MessageSource, // 国际化消息能力
ApplicationEventPublisher, // 事件发布能力
ResourcePatternResolver { // 资源模式解析能力

// 获取容器的唯一标识
String getId();

// 获取应用名称
String getApplicationName();

// 获取容器的友好显示名称
String getDisplayName();

// 获取容器启动的时间戳
long getStartupDate();

// 获取父容器(支持容器层级)
ApplicationContext getParent();

// 获取内部的 AutowireCapableBeanFactory(高级用法)
AutowireCapableBeanFactory getAutowireCapableBeanFactory()
throws IllegalStateException;
}

ApplicationContext 相比 BeanFactory 的增强功能

功能领域BeanFactoryApplicationContext说明
Bean 获取基础能力,两者都支持
Bean 生命周期管理基础能力,两者都支持
单例 Bean 预加载❌ 懒加载✅ 启动时预加载ApplicationContext 在启动时就创建所有单例 Bean
BeanPostProcessor 自动注册❌ 需手动✅ 自动发现并注册简化了扩展点的使用
国际化(i18n)✅ MessageSource支持多语言消息
事件发布机制✅ ApplicationEventPublisher支持应用内事件的发布与监听
AOP 支持自动代理、切面织入
资源加载✅ ResourceLoader统一的资源访问抽象
环境抽象✅ EnvironmentProfile、Properties 管理
父子容器✅ HierarchicalBeanFactory支持容器层级结构

预加载 vs 懒加载的区别

这是 BeanFactory 和 ApplicationContext 最显著的行为差异:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// BeanFactory:懒加载
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// ... 加载 Bean 定义 ...
// 此时 Bean 还没有被创建!

OrderService service = beanFactory.getBean(OrderService.class);
// 调用 getBean 时才创建 OrderService 及其依赖


// ApplicationContext:预加载
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 容器启动完成时,所有单例 Bean 已经创建完毕!

OrderService service = context.getBean(OrderService.class);
// 直接从缓存中获取,不需要再创建

预加载的好处

  1. 快速失败:如果 Bean 配置有问题(如循环依赖、缺少依赖),在启动时就会报错,而不是运行时
  2. 启动后响应快:所有 Bean 都已就绪,第一次请求不会有创建延迟
  3. 资源预热:数据库连接池、缓存等可以在启动时就初始化好

常用的 ApplicationContext 实现类

Spring 提供了多种 ApplicationContext 实现,适用于不同的场景:

mermaid-diagram-2026-01-03-114332

各实现类的适用场景

实现类配置方式适用环境使用场景
AnnotationConfigApplicationContext注解/JavaConfig非 Web单元测试、独立应用、批处理任务
ClassPathXmlApplicationContextXML非 Web遗留项目、需要 XML 配置的场景
AnnotationConfigServletWebServerApplicationContext注解/JavaConfigServlet WebSpring Boot Web 应用(默认)
AnnotationConfigReactiveWebServerApplicationContext注解/JavaConfig响应式 WebSpring WebFlux 应用

Spring Boot 中的容器

在 Spring Boot 应用中,我们通常不需要手动创建 ApplicationContext。SpringApplication.run() 会根据类路径自动选择合适的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringBootApplication
public class MyApplication {

public static void main(String[] args) {
// SpringApplication.run() 内部会:
// 1. 检测应用类型(Servlet Web / Reactive Web / 非 Web)
// 2. 创建对应的 ApplicationContext 实现
// 3. 加载配置、创建 Bean、启动应用
ApplicationContext context = SpringApplication.run(MyApplication.class, args);

// 返回的 context 类型取决于应用类型:
// - Web 应用:AnnotationConfigServletWebServerApplicationContext
// - WebFlux 应用:AnnotationConfigReactiveWebServerApplicationContext
// - 非 Web 应用:AnnotationConfigApplicationContext
}
}

1.4.4. 容器的层级结构与父子容器

Spring 支持容器的层级结构——一个容器可以有一个父容器。这种设计在某些场景下非常有用。

父子容器的核心规则

  1. 子容器可以访问父容器中的 Bean
  2. 父容器无法访问子容器中的 Bean
  3. 子容器中的 Bean 可以覆盖父容器中同名的 Bean

mermaid-diagram-2026-01-03-114611

父子容器的典型应用:传统 Spring MVC

在传统的 Spring MVC 应用中(非 Spring Boot),通常会配置两个容器:

web.xml 配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<web-app>
<!-- 1. 父容器:由 ContextLoaderListener 创建 -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>

<!-- 2. 子容器:由 DispatcherServlet 创建 -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
</init-param>
</servlet>
</web-app>

两个容器的职责分工

容器创建者存放的 Bean配置文件
Root WebApplicationContext(父)ContextLoaderListenerService、Repository、DataSource 等业务组件applicationContext.xml
Servlet WebApplicationContext(子)DispatcherServletController、ViewResolver、HandlerMapping 等 Web 组件dispatcher-servlet.xml

这种设计的好处

  1. 职责分离:Web 层组件和业务层组件分开管理,边界清晰
  2. 复用性:一个父容器可以被多个子容器共享(多个 DispatcherServlet 场景)
  3. 隔离性:不同的 DispatcherServlet 可以有不同的 Web 配置,但共享业务组件

Bean 查找规则详解

当从子容器获取 Bean 时,查找顺序是:

mermaid-diagram-2026-01-03-114733

代码示例

1
2
3
4
5
6
7
8
// 假设 UserService 定义在父容器中
// UserController 定义在子容器中

// 从子容器获取 UserService —— 会向上查找父容器
UserService service = childContext.getBean(UserService.class); // ✅ 成功

// 从父容器获取 UserController —— 无法向下查找子容器
UserController controller = parentContext.getBean(UserController.class); // ❌ 失败

Spring Boot 的简化

在 Spring Boot 中,默认只有一个 ApplicationContext,不再区分父子容器:

1
2
3
4
5
6
7
8
9
@SpringBootApplication
public class MyApplication {

public static void main(String[] args) {
// Spring Boot 只创建一个容器
// 所有 Bean(Controller、Service、Repository)都在同一个容器中
ApplicationContext context = SpringApplication.run(MyApplication.class, args);
}
}

这种简化带来的好处:

  • 配置更简单:不需要维护多个配置文件
  • 理解成本低:不需要考虑 Bean 在哪个容器中
  • 调试更容易:所有 Bean 都在一个地方

但也有一些场景仍然需要父子容器的设计,比如:

  • 需要在一个应用中部署多个相互隔离的模块
  • 需要实现插件化架构,插件有自己独立的容器

1.5. 本章总结与核心概念速查

摘要回顾

本章我们从一个 “坏代码” 案例出发,揭示了直接 new 对象带来的三大困境:无法测试、难以扩展、职责混乱。为了解决这些问题,我们引入了 控制反转(IoC) 的设计思想——将对象创建的责任从业务类转移到外部容器。

依赖注入(DI) 是 IoC 最主流的实现方式。我们深入分析了三种注入方式的演进历史和设计权衡:

  • Setter 注入:Spring 早期的主流方式,适合可选依赖
  • 字段注入:最简洁但不推荐,无法使用 final,测试困难
  • 构造器注入:现代 Spring 的推荐方式,支持不可变性,依赖显式化

最后,我们剖析了 Spring 容器的两个核心接口:

  • BeanFactory:最小化的 IoC 容器契约,定义了获取 Bean 的基本能力
  • ApplicationContext:企业级功能的全面扩展,是我们日常使用的容器

核心概念速查表

概念定义关键要点
Bean由 Spring 容器创建、管理和装配的对象不是所有对象都应该成为 Bean,只有需要被容器管理的组件才应该
IoC(控制反转)将对象创建和依赖管理的控制权从业务代码转移到外部容器核心是 “责任转移”,业务类不再负责创建依赖
DI(依赖注入)IoC 的实现方式,由容器将依赖 “推送” 给对象对象被动接收依赖,无需主动获取
构造器注入通过构造函数参数注入依赖⭐ 官方推荐,支持 final,依赖显式,便于测试
Setter 注入通过 setter 方法注入依赖适合可选依赖,可解决循环依赖
字段注入通过反射直接注入字段❌ 不推荐,无法 final,测试困难,依赖隐藏
BeanFactorySpring IoC 容器的根接口最小化契约,懒加载,功能有限
ApplicationContextBeanFactory 的增强版企业级功能,预加载单例,支持 AOP/事件/国际化
@Autowired自动装配注解按类型查找 → 按名称匹配 → @Qualifier 指定
@Qualifier指定注入的 Bean 名称解决多个同类型 Bean 的歧义问题
@Primary标记首选 Bean多个同类型 Bean 时,优先注入被标记的

本章核心心智模型

从 “手工作坊” 到 “自动化工厂” 的转变

mermaid-diagram-2026-01-03-115029

核心认知升级

读完本章,你应该建立起以下认知:

  1. IoC 不是语法糖:它是一种架构级的责任重新分配,让业务类专注于业务逻辑
  2. Bean 不只是 “对象”:它是被容器管理的、有生命周期的、可被增强的特殊对象
  3. 构造器注入的 “冗长” 是特性:它让依赖关系显式化,让设计问题提前暴露
  4. ApplicationContext 是日常使用的容器:BeanFactory 是底层抽象,理解它有助于理解原理
  5. Spring Boot 简化了容器的使用:但底层原理没有变,理解原理才能解决复杂问题