6. [原理剖析] 深入 IoC 容器

6. [原理剖析] 深入 IoC 容器

摘要: 本章我们将探讨 Spring IoC 容器中两个非常重要的底层话题。首先是经典的面试难题——循环依赖,我们将从原理上剖析 Spring 是如何通过三级缓存解决它的。其次是作为 Spring 设计思想基石的工厂模式。

6.1. Bean 的循环依赖问题 (面试高频)

首先,我们需要理解什么是循环依赖,以及为什么它在 IoC 容器中是一个值得被探讨的问题。

6.1.1. "痛点"场景: 什么是循环依赖?

循环依赖(Circular Dependency),也称为循环引用,指的是两个或多个 Bean 之间相互持有对方的引用,形成一个闭环。最简单的场景就是 A 依赖 B,同时 B 又依赖 A。

A -> B -> A

一个生动的比喻:
想象一下“丈夫”(Husband)和“妻子”(Wife)两个类。Husband 对象有一个 Wife 类型的属性,Wife 对象也有一个 Husband 类型的属性。当 Spring 容器试图创建它们时,就会遇到一个难题:

  • 要创建 Husband 实例,必须先注入一个 Wife 实例。
  • 但要创建 Wife 实例,又必须先注入一个 Husband 实例。这就形成了一个看似无解的“死循环”。

准备代码:
我们将使用这个“夫妻”模型来贯穿本节的实验。

文件路径: src/main/java/com/example/spring6/bean/Husband.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
package com.example.spring6.bean;

import lombok.Data;
import lombok.ToString;

@Data
@ToString(exclude = "wife") // 注意,如果这里不排除wife的话会栈溢出的,这是独属于lombok的依赖循环问题
public class Husband {
private String name;
private Wife wife;
}

文件路径: src/main/java/com/example/spring6/bean/Wife.java (新增文件)

1
2
3
4
5
6
7
8
9
10
11
package com.example.spring6.bean;

import lombok.Data;
import lombok.ToString;

@Data
@ToString(exclude = "husband")
public class Wife {
private String name;
private Husband husband;
}

6.1.2. 实践验证:Spring 能否解决循环依赖?

Spring 能否解决循环依赖,取决于 Bean 的作用域和注入方式。让我们通过三个核心场景来一探究竟。

准备配置文件:
文件路径: src/main/resources/spring-dependency.xml (新增配置文件)

场景一: singleton + Setter 注入 (成功)

这是最常见的场景:两个 Bean 都是单例,并且通过 Setter 方法相互注入。

XML 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="husband" class="com.example.spring6.bean.Husband">
<property name="name" value="张三"/>
<property name="wife" ref="wife"/>
</bean>

<bean id="wife" class="com.example.spring6.bean.Wife">
<property name="name" value="小红"/>
<property name="husband" ref="husband"/>
</bean>
</beans>

测试代码:

1
2
3
4
5
6
7
8
@Test
public void testSingletonSetterDI() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring-dependency.xml");
Husband husband = context.getBean("husband", Husband.class);
Wife wife = context.getBean("wife", Wife.class);
System.out.println(husband);
System.out.println(wife);
}
1
2
Husband{name='张三', wifeName=小红}
Wife{name='小红', husbandName=张三}

结论: Spring 完美地解决了单例 Bean 通过 Setter 方式注入的循环依赖问题。

场景二: prototype + Setter 注入 (失败)

如果我们将两个 Bean 的作用域都改为 prototype,情况会如何?

XML 配置 (修改 scope):

1
2
3
4
5
6
7
8
9
<bean id="husband" class="com.example.spring6.bean.Husband" scope="prototype">
<property name="name" value="张三"/>
<property name="wife" ref="wife"/>
</bean>

<bean id="wife" class="com.example.spring6.bean.Wife" scope="prototype">
<property name="name" value="小红"/>
<property name="husband" ref="husband"/>
</bean>

结论: Spring 无法解决 prototype 作用域 Bean 的循环依赖问题,并会抛出 BeanCurrentlyInCreationException 异常。

场景三: singleton + 构造器注入 (失败)

我们回到 singleton 作用域,但改用构造器进行注入。

准备构造器注入的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// HusbandWithConstructor.java
public class HusbandWithConstructor {
private final String name;
private final WifeWithConstructor wife;
public HusbandWithConstructor(String name, WifeWithConstructor wife) {
this.name = name; this.wife = wife;
}
// ...
}
// WifeWithConstructor.java
public class WifeWithConstructor {
private final String name;
private final HusbandWithConstructor husband;
public WifeWithConstructor(String name, HusbandWithConstructor husband) {
this.name = name; this.husband = husband;
}
// ...
}

XML 配置:

1
2
3
4
5
6
7
8
9
<bean id="husband" class="com.example.spring6.bean.HusbandWithConstructor">
<constructor-arg name="name" value="张三"/>
<constructor-arg name="wife" ref="wife"/>
</bean>

<bean id="wife" class="com.example.spring6.bean.WifeWithConstructor">
<constructor-arg name="name" value="小红"/>
<constructor-arg name="husband" ref="husband"/>
</bean>

结论: Spring 同样无法解决构造器注入方式的循环依赖问题,即使 Bean 是单例的。


6.1.3. 原理浅析:Spring 如何通过“三级缓存”解开死结?

为什么只有“singleton + Setter 注入”的组合能成功?这背后的功臣,就是 Spring IoC 容器内部精巧的“三级缓存”机制。

面试官深度剖析
今天 下午 3:15

刚才的实验结果很有趣。你能量化一下,为什么 Spring 能解决 singleton 和 Setter 的循环依赖吗?

求职者

核心原因在于 Spring 将 Bean 的创建过程分为了两步:1. 实例化2. 属性填充。对于单例 Bean,Spring 可以在完成第一步实例化之后,不等第二步属性填充完成,就将这个半成品的对象提前暴露出去。

很好。那 “提前曝光” 是通过什么机制实现的呢?能具体谈谈吗?

求职者

它是通过一个位于 DefaultSingletonBeanRegistry 类中的三级缓存来实现的。

求职者

一级缓存 : 存放已完成初始化的单例 Bean,是最终的成品区。

求职者

二级缓存 : 存放提前曝光的、未完成属性填充的半成品单例 Bean。

求职者

三级缓存 : 存放能生产半成品 Bean 的工厂 (ObjectFactory)。

能描述一下创建 husband 和 wife 的完整流程吗?

求职者

当然。

求职者
  1. getBean(“husband”),三级缓存中都没有,开始创建。
求职者
  1. new Husband() 实例化一个半成品 husband 对象。
求职者
  1. Spring 并不立即填充其 wife 属性,而是将一个能获取这个半成品 husband 的工厂放入三级缓存。
求职者
  1. 开始填充 husband 的属性,发现它需要 wife。
求职者
  1. getBean(“wife”),三级缓存中都没有,开始创建。
求职者
  1. new Wife() 实例化一个半成品 wife 对象,并同样将其工厂放入三级缓存。
求职者
  1. 开始填充 wife 的属性,发现它需要 husband。
求 postoji
  1. 再次 getBean(“husband”),此时 Spring 从三级缓存中找到了 husband 的工厂,通过工厂拿到半成品 husband 对象,并将其放入二级缓存。
求职者
  1. wife 成功获取到半成品 husband 的引用,完成属性填充和初始化,成为一个成品。随后,wife 被放入一级缓存。
求职者
  1. husband 也成功获取到成品 wife 的引用,完成自己的属性填充和初始化,也成为成品,并被放入一级缓存。循环依赖解决。

非常清晰!那最后请解释一下,为什么构造器注入和 prototype 作用域就不行呢?

求职者

构造器注入 的问题在于,它的实例化和属性填充是在同一步 (new Husband(wife)) 中完成的。在 new 的那一刻就必须拿到 wife 的实例,无法提前曝光一个半成品,所以陷入了死循环。

求职者

prototype 作用域 的问题在于,Spring 容器不对 prototype Bean 进行缓存。每次请求都是全新的,也就没有了可以存放半成品的缓存机制,自然也无法解决循环依赖。


6.2. [背景知识] 工厂设计模式

6.2.1. "痛点"场景: 为什么我们需要工厂?

在面向对象编程的初期,我们创建对象的方式通常非常直接:

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public void fight() {
// 客户端代码直接依赖具体的 Tank 类
Tank tank = new Tank();
tank.attack();

// 如果要换成战斗机,就需要修改客户端代码
Fighter fighter = new Fighter();
fighter.attack();
}
}

这段代码存在一个严重的问题:客户端 (Client) 与具体的产品 (Tank, Fighter) 强耦合

  • 缺乏弹性: 如果将来我们新增一种武器 Dagger,就必须修改 Client 类的代码。
  • 违反开闭原则 (OCP): 我们的系统对“扩展”(增加新武器)是关闭的,因为扩展需要“修改”现有代码。

解决方案:
引入一个“工厂”角色,专门负责创建对象。客户端不再关心对象是如何被创建的,只管向工厂索要即可。这就实现了创建过程与使用过程的分离


6.2.2. 模式概览: 简单工厂 vs 工厂方法

简单工厂模式

这是最基础的工厂模式,它将所有产品的创建逻辑集中在一个工厂类中。

1. 准备代码
文件路径: src/main/java/com/example/spring6/factory/Weapon.java (及实现类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.spring6.factory;

// 抽象产品角色
public abstract class Weapon {
public abstract void attack();
}

// 具体产品角色
class Tank extends Weapon {
public void attack() { System.out.println("坦克开炮!"); }
}
class Fighter extends Weapon {
public void attack() { System.out.println("战斗机投弹!"); }
}
class Dagger extends Weapon {
public void attack() { System.out.println("匕首攻击!"); }
}

文件路径: src/main/java/com/example/spring6/factory/WeaponFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.spring6.factory;

// 工厂类角色
public class WeaponFactory {
/**
* 根据武器类型,通过一个静态方法生产武器
* @param weaponType 武器类型
* @return 具体的武器实例
*/
public static Weapon get(String weaponType) {
if ("TANK".equals(weaponType)) {
return new Tank();
} else if ("FIGHTER".equals(weaponType)) {
return new Fighter();
} else if ("DAGGER".equals(weaponType)) {
return new Dagger();
} else {
throw new RuntimeException("不支持的武器类型");
}
}
}

2. 客户端调用
文件路径: src/main/java/com/example/spring6/factory/Client.java

1
2
3
4
5
6
7
8
9
10
11
12
package com.example.spring6.factory;

public class Client {
public static void main(String[] args) {
// 客户端不再关心 Tank 和 Fighter 是如何 new 出来的
Weapon tank = WeaponFactory.get("TANK");
tank.attack();

Weapon fighter = WeaponFactory.get("FIGHTER");
fighter.attack();
}
}

优点: 实现了创建和使用的分离。
缺点: 违反了开闭原则。每当新增一种武器,我们都必须修改 WeaponFactory 类的 get 方法


工厂方法模式

为了解决简单工厂的 OCP 问题,工厂方法模式将工厂也进行了抽象。

核心思想: 不再由一个全能工厂来创建所有产品,而是为每一种产品都提供一个专门的工厂。

1. 准备代码
我们继续使用上面的 Weapon 产品类。

文件路径: src/main/java/com/example/spring6/factory/WeaponFactory.java (修改为接口)

1
2
3
4
5
6
package com.example.spring6.factory;

// 抽象工厂角色
public interface WeaponFactory {
Weapon get();
}

文件路径: src/main/java/com/example/spring6/factory/TankFactory.java (等具体工厂)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.spring6.factory;

// 具体工厂角色,专门生产 Tank
class TankFactory implements WeaponFactory {
@Override
public Weapon get() {
return new Tank();
}
}

// 具体工厂角色,专门生产 Fighter
class FighterFactory implements WeaponFactory {
@Override
public Weapon get() {
return new Fighter();
}
}

2. 客户端调用

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
// 想生产坦克,就使用坦克的工厂
WeaponFactory tankFactory = new TankFactory();
Weapon tank = tankFactory.get();
tank.attack();

// 想生产战斗机,就使用战斗机的工厂
WeaponFactory fighterFactory = new FighterFactory();
Weapon fighter = fighterFactory.get();
fighter.attack();
}
}

优点: 完美遵循了开闭原则。如果现在需要新增 Dagger 武器,我们只需新增一个 DaggerFactory 即可,完全不需要修改任何现有代码。
缺点: 当产品种类非常多时,会导致工厂类的数量急剧增加。


6.2.3. Spring 中的体现: BeanFactoryFactoryBean 的区别

理解了工厂模式后,我们就可以来辨析 Spring 中两个最容易混淆的核心概念了。

面试官深度辨析
今天 下午 4:30

我们经常听说 Spring 是一个大工厂,也常提到 BeanFactoryFactoryBean。它们之间有什么关系和区别?

求职者

这是一个经典问题。虽然名字很像,但它们是两个完全不同维度的概念。

求职者

BeanFactory 是工厂。它是 Spring IoC 容器的顶级接口,是 Spring 框架的“心脏”,是管理所有 Bean 的“总工厂”。它的职责是定义如何获取和管理 Bean,是 Spring IoC 功能的基石。我们通常使用的 ApplicationContext 就是它的一个功能更强大的子接口。

求职者

FactoryBean 是一个 Bean。它是一个可以被总工厂 (BeanFactory) 管理的、比较特殊的 Bean。特殊之处在于,它本身的作用不是给自己用,而是作为一个“小型、可插拔的零件工厂”,去生产另一个 Bean。

那么从容器中获取 Bean 时,这两者有什么不同?

求职者

这是最关键的区别。假设我们有一个 idmyCarEngineFactoryBean

求职者

当我们调用 context.getBean("myCarEngine") 时,我们得到的不是这个 FactoryBean 本身,而是它内部 getObject() 方法返回的那个“发动机”对象。

求- 职者

如果我们确实需要 FactoryBean 这个“机床”本身,需要使用一个 & 符号,像这样调用:context.getBean("&myCarEngine")

求职者

总结来说,BeanFactory 是管理者,FactoryBean 是被管理者中的一个特殊工匠。


重要信息: 后续内容还有重要的注解开发和面向切面AOP的核心,并不是笔记这里不讲了,是我认为他更适合在Spring-boot章节来讲,我们对比了这么多SpringBoot相关的知识点,所以我们与其学习Spring的基础用法,不如直接过渡到Spring-boot中,相信我,在学习了以上内容之后,过度到Spring-boot是最好契机!