MiniSpring框架学习笔记-解决循环依赖的简化IoC容器
- [03. 解决循环依赖的简化 IoC 容器](#03. 解决循环依赖的简化 IoC 容器)
-
- 学完这一节要明白什么
- [先看没有 IoC 的问题](#先看没有 IoC 的问题)
- [这节只讨论 IoC,不讨论所有 Spring 问题](#这节只讨论 IoC,不讨论所有 Spring 问题)
- [IoC 容器是什么](#IoC 容器是什么)
- [Demo 要解决的循环依赖](#Demo 要解决的循环依赖)
- 入口代码
- [创建 ApplicationContext 时做了什么](#创建 ApplicationContext 时做了什么)
- [BeanDefinition 是什么](#BeanDefinition 是什么)
- [getBean 的主流程](#getBean 的主流程)
- [createBean 的关键点](#createBean 的关键点)
- 构造方法注入
- [setter 属性注入](#setter 属性注入)
- 循环依赖是怎么被拆开的
- 为什么构造器循环依赖解决不了
- 两个缓存分别负责什么
- [XML 示例](#XML 示例)
- [这份 Demo 里容易误解的地方](#这份 Demo 里容易误解的地方)
- 最后总结一下
教程: https://github.com/YaleGuo/minis
极客时间: 手把手带你写一个 MiniSpring
03. 解决循环依赖的简化 IoC 容器
学MIniSpring不是为了把 Spring 源码完整复刻一遍,也不是为了写一个能上线的容器。
我们真正要学的是:平时写 @Service、@Autowired 的时候,对象到底是从哪里来的?依赖关系到底是谁装进去的?为什么有些循环依赖 Spring 能处理,有些又不能?
如果这些东西一直停留在"框架帮我做了"的层面,业务代码能跑当然没问题。一旦项目启动失败、Bean 注入失败、循环依赖报错,就很容易只看到异常,看不到异常背后的流程。
所以这一节的重点是把 IoC 容器拆成一个很小的 Demo。它足够小,方便看清主线;它也足够像 Spring,能帮助我们理解 Spring 里那些更复杂的设计。
学完这一节要明白什么
先把目标说清楚。学完之后,你至少要能说清楚这几件事:
- IoC 到底反转了什么控制权。
BeanDefinition、BeanFactory、ApplicationContext分别负责什么。getBean("aservice")背后大概走了哪些步骤。- setter 循环依赖为什么可以靠"提前暴露半成品对象"解决。
- 这个 Demo 能解决什么,不能解决什么。
本节的主线可以压缩成一句话:
text
XML 配置 -> BeanDefinition -> BeanFactory -> 创建对象 -> 提前放入 earlySingletonObjects -> setter 注入 -> 放入 singletons -> getBean 返回
抓住这条线,后面的代码就不会显得零散。
先看没有 IoC 的问题
传统写法里,一个对象经常会自己创建它依赖的对象:
java
public class OrderService {
private UserService userService = new UserServiceImpl();
}
这段代码能跑,但它有一个明显问题:OrderService 直接写死依赖了 UserServiceImpl。
如果哪天想换成 UserServiceImpl2,就必须改 OrderService 的代码。对象创建、对象选择、业务逻辑,全都混在一起了。
这就是"控制权在业务代码手里"。
IoC 做的事情,就是把这部分控制权拿出来,交给容器:
java
public class OrderService {
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
}
业务类只声明"我需要一个 UserService",至于具体用哪个实现类、什么时候创建、怎么注入,由容器负责。
所以 IoC 的核心不是"用了 XML"或者"用了注解",而是:
text
对象的创建权、依赖的装配权,从业务代码转移到了容器。
依赖注入,也就是 DI,是实现 IoC 的一种方式。
这节只讨论 IoC,不讨论所有 Spring 问题
开头很容易把 Spring 的所有能力都混在一起,比如:
@Service为什么能创建对象?@Autowired为什么能注入对象?- HTTP 请求为什么能进入 Controller?
- MyBatis 的 SQL 为什么能和 Spring 集成?
这些问题都和 Spring 生态有关,但不都属于 IoC 容器本身。
本节只聚焦一个更小的问题:
text
容器如何根据配置创建 Bean,并把 Bean 之间的依赖关系装配起来?
只要这个问题搞懂了,再看注解扫描、MVC、AOP、事务、MyBatis 集成,就有了地基。
IoC 容器是什么
IoC 容器可以先理解成一个对象工厂,不过它比普通工厂多做了几件事:
- 保存 Bean 的定义信息。
- 按定义创建对象。
- 处理对象之间的依赖关系。
- 缓存单例对象。
- 在合适的时机返回已经准备好的对象。
在这个 Demo 里,几个概念对应关系是这样的:
| 概念 | 可以怎么理解 | 在 Demo 里的角色 |
|---|---|---|
BeanDefinition |
Bean 的说明书 | 记录 id、class、构造参数、属性注入信息 |
BeanFactory |
真正干活的工厂 | 创建 Bean、查找 Bean、缓存 Bean |
ApplicationContext |
更上层的容器入口 | 读取配置,启动 BeanFactory,对外提供 getBean |
singletons |
成品仓库 | 保存已经创建完成的单例 Bean |
earlySingletonObjects |
半成品仓库 | 保存刚实例化、还没完成 setter 注入的 Bean |
注意:真实 Spring 的 ApplicationContext 比这里强很多,它还负责事件、资源、环境变量、国际化、AOP 等等。Demo 里只保留最关键的 IoC 主线。
Demo 要解决的循环依赖
本节讨论的是 setter 循环依赖。举个例子:
text
aservice -> baseservice -> basebaseservice -> aservice
也就是说:
- 创建
aservice时,需要注入baseservice。 - 创建
baseservice时,需要注入basebaseservice。 - 创建
basebaseservice时,又需要注入aservice。
如果容器只会"缺什么就立刻完整创建什么",流程就会变成:
text
创建 aservice
需要 baseservice
创建 baseservice
需要 basebaseservice
创建 basebaseservice
又需要 aservice
再创建 aservice
...
这样会一直递归下去。
解决思路是:创建对象分两步走。
text
第一步:先调用构造方法,拿到一个对象引用。
第二步:再做 setter 属性注入。
只要第一步拿到了对象引用,就可以先把它放到 earlySingletonObjects 里。后面如果别的 Bean 又依赖它,就先把这个"半成品对象"拿出来用,等依赖都串起来之后,再逐步完成属性注入。
这也是本节最重要的概念。
入口代码
测试代码很简单:
java
public class Test1 {
public static void main(String[] args) throws BeansException {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
AService aService = (AService) ctx.getBean("aservice");
aService.sayHello();
}
}
这里有两个动作:
new ClassPathXmlApplicationContext("beans.xml"):读取配置,启动容器。ctx.getBean("aservice"):从容器里拿一个已经装配好的对象。
ApplicationContext 这个名字可以先简单理解成"应用上下文"。它是应用启动后的一整个运行环境,里面有 Bean 定义、Bean 实例、配置关系等信息。
在这个 Demo 里,ClassPathXmlApplicationContext 主要就是一个入口类:它负责读取 XML,然后把具体创建 Bean 的工作交给 SimpleBeanFactory。
创建 ApplicationContext 时做了什么
核心流程是:
- 根据文件名创建
Resource。 - 创建
SimpleBeanFactory。 - 用
XmlBeanDefinitionReader读取 XML。 - 把 XML 里的
<bean>解析成BeanDefinition。 - 调用
refresh()预创建单例 Bean。
代码可以这样看:
java
public class ClassPathXmlApplicationContext implements BeanFactory {
private final SimpleBeanFactory beanFactory;
public ClassPathXmlApplicationContext(String fileName) throws BeansException {
this(fileName, true);
}
public ClassPathXmlApplicationContext(String fileName, boolean isRefresh) throws BeansException {
Resource resource = new ClassPathXmlResource(fileName);
SimpleBeanFactory simpleBeanFactory = new SimpleBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(simpleBeanFactory);
// 读取 XML,把 bean 配置解析成 BeanDefinition。
reader.loadBeanDefinitions(resource);
this.beanFactory = simpleBeanFactory;
// 预创建单例 Bean。传 false 时,可以等第一次 getBean 再创建。
if (isRefresh) {
this.beanFactory.refresh();
}
}
public Object getBean(String beanName) throws BeansException {
return this.beanFactory.getBean(beanName);
}
// containsBean、isSingleton、isPrototype、getType 等方法,
// Demo 中也是继续转发给 this.beanFactory,这里不展开。
}
这里有一个容易说错的点:不是 refresh() 本身解决了循环依赖。
真正解决循环依赖的是 createBean() 里的"先实例化、提前暴露、再注入属性"这套流程。refresh() 只是触发所有单例 Bean 提前走一遍 getBean()。
BeanDefinition 是什么
XML 里的 Bean 配置长这样:
xml
<bean id="aservice" class="com.chenhai.test.AServiceImpl">
<constructor-arg type="String" name="name" value="abc"/>
<constructor-arg type="int" name="level" value="3"/>
<property type="String" name="property1" value="Someone says"/>
<property type="String" name="property2" value="Hello World!"/>
<property type="com.chenhai.test.BaseService" name="ref1" ref="baseservice"/>
</bean>
容器不能直接拿 XML 字符串创建对象,它需要先把 XML 转成程序里的结构。
这个结构就是 BeanDefinition。
可以把它理解成 Bean 的说明书:
text
beanName: aservice
className: com.chenhai.test.AServiceImpl
constructorArgs: name=abc, level=3
properties: property1, property2, ref1
后面 BeanFactory 创建对象时,不再关心 XML 原文,只关心 BeanDefinition。
这一步很关键。真实 Spring 也是先把各种配置来源变成 BeanDefinition,比如 XML、注解扫描、Java Config,最后都会进入比较统一的 Bean 创建流程。
getBean 的主流程
getBean(beanName) 是整个容器最重要的方法。
它的逻辑不要背代码,记住顺序就行:
- 先从
singletons里找完整 Bean。 - 找不到,再从
earlySingletonObjects里找半成品 Bean。 - 还找不到,就根据
BeanDefinition创建 Bean。 - 创建完成后放进
singletons。 - 从
earlySingletonObjects移除半成品引用。
关键代码如下:
java
@Override
public Object getBean(String beanName) throws BeansException {
Object singleton = this.getSingleton(beanName);
if (singleton != null) {
return singleton;
}
singleton = this.earlySingletonObjects.get(beanName);
if (singleton != null) {
return singleton;
}
BeanDefinition beanDefinition = this.beanDefinitionMap.get(beanName);
if (beanDefinition == null) {
throw new BeansException("No bean named " + beanName);
}
Object bean = createBean(beanDefinition);
this.registerSingleton(beanName, bean);
this.earlySingletonObjects.remove(beanName);
return bean;
}
这里的 earlySingletonObjects 不是"复制了一份对象",它保存的是同一个对象引用。
所以后面 setter 注入继续给这个对象补属性时,已经拿到早期引用的地方,也会看到同一个对象被补完整。
createBean 的关键点
创建 Bean 的关键不是一行 newInstance(),而是分成两个阶段:
java
private Object createBean(BeanDefinition beanDefinition) throws BeansException {
try {
Class<?> clz = Class.forName(beanDefinition.getClassName());
// 第一阶段:先调用构造方法,得到一个对象引用。
Object obj = doCreateBean(clz, beanDefinition.getConstructorArgumentValues());
// 先把半成品放出来,后面处理循环依赖时可能会用到。
this.earlySingletonObjects.put(beanDefinition.getId(), obj);
// 第二阶段:再做 setter 注入。
handleProperties(beanDefinition, clz, obj);
return obj;
} catch (Exception e) {
throw new BeansException("Create bean failed: " + beanDefinition.getId());
}
}
这段代码就是本节的核心。
如果你把 earlySingletonObjects.put(...) 放到 handleProperties(...) 后面,循环依赖就解决不了了。因为属性注入的时候已经开始递归找依赖了,必须在递归发生前,把当前对象的引用先暴露出去。
构造方法注入
构造方法注入主要是演示反射创建对象。
java
private Object doCreateBean(Class<?> clz, ArgumentValues argumentValues) throws Exception {
if (argumentValues == null || argumentValues.isEmpty()) {
return clz.getDeclaredConstructor().newInstance();
}
Class<?>[] paramTypes = new Class<?>[argumentValues.getArgumentCount()];
Object[] paramValues = new Object[argumentValues.getArgumentCount()];
for (int i = 0; i < argumentValues.getArgumentCount(); i++) {
ArgumentValue argumentValue = argumentValues.getIndexedArgumentValue(i);
paramTypes[i] = getClassType(argumentValue.getType());
paramValues[i] = getValue(argumentValue.getType(), argumentValue.getValue());
}
Constructor<?> constructor = clz.getConstructor(paramTypes);
return constructor.newInstance(paramValues);
}
这里需要注意两个小点:
paramTypes和paramValues必须一一对应,否则反射找不到正确构造方法。- XML 里写的
"int"、"String"是字符串,需要转换成真正的int.class、String.class。
这个 Demo 里构造方法参数只处理了少数类型,够演示就行。真实框架会做更完整的类型转换。
setter 属性注入
setter 注入分两种情况:
- 普通值,比如
String、int。 - Bean 引用,比如
ref="baseservice"。
代码重点在这里:
java
private void handleProperties(BeanDefinition beanDefinition, Class<?> clz, Object obj) throws Exception {
PropertyValues propertyValues = beanDefinition.getPropertyValues();
if (propertyValues == null || propertyValues.isEmpty()) {
return;
}
for (PropertyValue propertyValue : propertyValues.getPropertyValueList()) {
String propertyName = propertyValue.getName();
Class<?> paramType = getClassType(propertyValue.getType());
Object paramValue = getValue(propertyValue.getType(), propertyValue.getValue());
if (propertyValue.isRef()) {
paramValue = getBean((String) propertyValue.getValue());
paramType = Class.forName(propertyValue.getType());
}
String methodName = "set"
+ propertyName.substring(0, 1).toUpperCase()
+ propertyName.substring(1);
Method method = clz.getMethod(methodName, paramType);
method.invoke(obj, paramValue);
}
}
普通值比较简单,直接做类型转换后调用 setter。
ref 更关键。遇到 ref="baseservice" 时,容器不会把字符串 "baseservice" 注入进去,而是会递归调用:
java
getBean("baseservice")
这就是依赖关系被串起来的地方,也是循环依赖可能发生的地方。
循环依赖是怎么被拆开的
假设依赖关系是:
text
aservice -> baseservice -> basebaseservice -> aservice
容器启动后,执行 getBean("aservice"),过程大概是这样:
text
1. 创建 aservice
2. 调用 aservice 构造方法,得到 aservice 对象
3. 把 aservice 放入 earlySingletonObjects
4. 给 aservice 注入 ref1,需要 baseservice
5. 创建 baseservice
6. 调用 baseservice 构造方法,得到 baseservice 对象
7. 把 baseservice 放入 earlySingletonObjects
8. 给 baseservice 注入依赖,需要 basebaseservice
9. 创建 basebaseservice
10. 调用 basebaseservice 构造方法,得到 basebaseservice 对象
11. 把 basebaseservice 放入 earlySingletonObjects
12. 给 basebaseservice 注入依赖,需要 aservice
13. 再次 getBean("aservice")
14. singletons 里还没有完整 aservice
15. earlySingletonObjects 里有半成品 aservice
16. 直接返回这个 aservice 引用
到这里,递归就断开了。
后面 basebaseservice 完成注入,放入 singletons;baseservice 继续完成注入,放入 singletons;最后 aservice 也完成注入,放入 singletons。
这就是"提前暴露半成品对象"的价值。
为什么构造器循环依赖解决不了
这点一定要分清楚。
这个 Demo 解决的是 setter 循环依赖,不是构造器循环依赖。
setter 注入可以这样拆:
text
先 new A()
把 A 暴露出去
再 setB(...)
但构造器注入是这样:
java
public A(B b) {
this.b = b;
}
如果 A 的构造方法需要 B,B 的构造方法又需要 A,那 A 连构造方法都走不完,对象引用还没有产生,自然也就没法提前放进 earlySingletonObjects。
所以这类依赖在这个 Demo 里解决不了。真实 Spring 对构造器循环依赖也通常会报错。
两个缓存分别负责什么
这个 Demo 里有两个缓存:
java
protected Map<String, Object> singletons = new ConcurrentHashMap<>(256);
singletons 保存完整 Bean。一个 Bean 已经完成构造、属性注入,后面再来拿,直接从这里返回。
java
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(256);
earlySingletonObjects 保存半成品 Bean。它只在 Bean 创建过程中临时使用,用来打断 setter 循环依赖。
完整流程是:
text
创建对象后:放入 earlySingletonObjects
属性注入后:放入 singletons
创建完成后:从 earlySingletonObjects 移除
真实 Spring 里还有更复杂的三级缓存,主要是为了处理 AOP 代理等场景。本节 Demo 没有 AOP,所以用二级缓存就能讲清楚主线。
XML 示例
下面这个 XML 演示了三种配置:
constructor-arg:构造器参数。property value:普通属性值。property ref:引用另一个 Bean。
xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
<bean id="aservice" class="com.chenhai.test.AServiceImpl">
<constructor-arg type="String" name="name" value="abc"/>
<constructor-arg type="int" name="level" value="3"/>
<property type="String" name="property1" value="Someone says"/>
<property type="String" name="property2" value="Hello World!"/>
<property type="com.chenhai.test.BaseService" name="ref1" ref="baseservice"/>
</bean>
</beans>
这里要注意:property 的 type 要和 setter 方法的参数类型对得上。
比如 XML 写的是:
xml
<property type="com.chenhai.test.BaseService" name="ref1" ref="baseservice"/>
那目标类里应该有类似这样的 setter:
java
public void setRef1(BaseService ref1) {
this.ref1 = ref1;
}
如果 setter 参数类型写成了别的类型,clz.getMethod(methodName, paramType) 就可能找不到方法。
这份 Demo 里容易误解的地方
第一,refresh() 不是解决循环依赖的核心。
它只是触发 Bean 创建。真正关键的是 createBean() 里先把对象实例放进 earlySingletonObjects,再进行 setter 注入。
第二,earlySingletonObjects 里不是完整对象。
它里面的对象已经调用过构造方法,但还没完成所有属性注入。所以它只能在创建过程中临时救场,不能当作最终成品长期使用。
第三,这个 Demo 只讨论单例 Bean。
如果是 prototype Bean,每次 getBean() 都要创建新对象,缓存策略就不同了。setter 循环依赖也不能简单套用这个方案。
第四,这个 Demo 没有处理 AOP 代理。
真实 Spring 有时候返回的不是原始对象,而是代理对象。为了保证循环依赖场景里拿到的也是正确代理对象,Spring 需要更复杂的缓存和生命周期处理。
第五,异常处理可以更友好。
示例代码里:
java
throw new BeansException("Create bean failed: " + beanDefinition.getId());
为了 Demo 简洁可以这样写,但真实项目最好把原始异常带上,否则排查反射失败、类型不匹配、setter 不存在时会比较难。
最后总结一下
这一节最重要的不是记住每一行代码,而是记住 IoC 容器创建 Bean 的节奏:
text
先读配置,得到 BeanDefinition。
再按 BeanDefinition 创建对象。
创建对象时先实例化,提前暴露引用。
然后做 setter 注入。
最后把完整 Bean 放入单例缓存。
循环依赖能被解决,是因为 setter 注入把"创建对象"和"注入依赖"拆成了两个阶段。容器可以在对象刚创建出来的时候,先把引用放出来,等依赖链走完之后再把对象补完整。
理解了这条主线,再回头看 Spring 的 @Service、@Autowired、ApplicationContext,就不会只是"框架自动帮我做了",而是能大概知道它是在什么阶段、用什么思路做的。