文章目录
Spring学习目录
环境
spring6里程碑版本的仓库
依赖:spring context依赖、junit依赖、log4j2依赖
log4j2.xml文件放到类路径下。
什么是Bean的循环依赖
A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。
比如:丈夫类Husband,妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。
singleton下的set注入产生的循环依赖
我们来编写程序,测试一下在singleton+setter的模式下产生的循环依赖,Spring是否能够解决?
创建Bean:丈夫类Husband
/**
* 丈夫类
*/
public class Husband {
private String name;
private Wife wife;
@Override
public String toString() {
return "Husband{" +
"name='" + name + '\'' +
", wife=" + wife.getName() +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setWife(Wife wife) {
this.wife = wife;
}
}
创建Bean:妻子类Wife
/**
* 妻子类
*/
public class Wife {
private String name;
private Husband husband;
public void setName(String name) {
this.name = name;
}
public void setHusband(Husband husband) {
this.husband = husband;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Wife{" +
"name='" + name + '\'' +
", husband=" + husband.getName() +
'}';
}
}
创建spring.xml配置:
<bean id="husbandBean" class="com.circlar.dependency.bean.Husband" scope="singleton">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.circlar.dependency.bean.Wife" scope="singleton">
<property name="name" value="李四"/>
<property name="husband" ref="husbandBean"/>
</bean>
测试程序:
@Test
public void testSingletonSet(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);
System.out.println(husbandBean);
System.out.println(wifeBean);
}
在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。、
怎么解决问题呢,先这样解释:(后面有底层解释,先这样理解)
Spring在解析xml文件的时候,遇见beanA,会先把beanA创建出来,然后发现beanA的属性需要beanB
然后Spring会去找beanB,发现beanB也是单例的,就会把对象先创建出来。
主要原因是,在这种模式下Spring对Bean的管理主要分为清晰的两个阶段:
- 第一阶段:在Spring容器加载的时候,实例化Bean,只要其中任意一个Bean实例化后,就马上进行 ”曝光“ 【不等待属性赋值就曝光】
- 第二阶段:Bean ”曝光“ 之后,再进行属性赋值。
核心的原因就是:实例化对象和对象的属性赋值分为两个阶段完成。
在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。
prototype下的set注入产生的循环依赖
我们再来测试一下:prototype+set注入的方式下,循环依赖会不会出现问题?
还是那两个夫妻Bean
然后创建一个新的spring配置文件:spring1.xml
<bean id="husbandBean" class="com.circlar.dependency.bean.Husband" scope="prototype">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.circlar.dependency.bean.Wife" scope="prototype">
<property name="name" value="李四"/>
<property name="husband" ref="husbandBean"/>
</bean>
测试程序:
@Test
public void testPrototypeSet(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring1.xml");
Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);
System.out.println(husbandBean);
System.out.println(wifeBean);
}
翻译为:创建名为“husbandBean”的bean时出错:请求的bean当前正在创建中:是否存在无法解析的循环引用?
会出现异常BeanCreationException,当前bean正处于创建中异常,
也就是说bean处于第二步(populateBean:填充属性)就卡住了
如果我们把其中一个Bean改为单例的试试
<bean id="husbandBean" class="com.circlar.dependency.bean.Husband" scope="prototype">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.circlar.dependency.bean.Wife" scope="singleton">
<property name="name" value="李四"/>
<property name="husband" ref="husbandBean"/>
</bean>
再次运行,发现运行正常。
构造注入产生的循环依赖
我们再来测试一下构造注入的方式下,spring是否能够解决这种循环依赖。
修改丈夫类:
/**
* 丈夫类
*/
public class Husband {
private String name;
private Wife wife;
@Override
public String toString() {
return "Husband{" +
"name='" + name + '\'' +
", wife=" + wife.getName() +
'}';
}
public String getName() {
return name;
}
public Husband(String name, Wife wife) {
this.name = name;
this.wife = wife;
}
}
修改妻子类:
/**
* 妻子类
*/
public class Wife {
private String name;
private Husband husband;
public Wife(String name, Husband husband) {
this.name = name;
this.husband = husband;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Wife{" +
"name='" + name + '\'' +
", husband=" + husband.getName() +
'}';
}
}
singleton下的构造注入
创建spring2.xml配置:
<bean id="husband" class="com.circlar.dependency.bean2.Husband" scope="singleton">
<constructor-arg index="0" value="张三"/>
<constructor-arg index="1" ref="wife"/>
</bean>
<bean id="wife" class="com.circlar.dependency.bean2.Wife" scope="singleton">
<constructor-arg index="0" value="李四"/>
<constructor-arg index="1" ref="husband"/>
</bean>
测试程序:
@Test
public void testSingletonConstructor(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring2.xml");
Husband husband = applicationContext.getBean("husband", Husband.class);
Wife wife = applicationContext.getBean("wife", Wife.class);
System.out.println(husband);
System.out.println(wife);
}
出现异常
prototype下的构造注入
修改spring2.xml:
<bean id="husband" class="com.circlar.dependency.bean2.Husband" scope="prototype">
<constructor-arg index="0" value="张三"/>
<constructor-arg index="1" ref="wife"/>
</bean>
<bean id="wife" class="com.circlar.dependency.bean2.Wife" scope="prototype">
<constructor-arg index="0" value="李四"/>
<constructor-arg index="1" ref="husband"/>
</bean>
再次运行测试程序,发现出现异常:
主要原因是因为通过构造方法注入导致的:因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成导致的。
Spring解决循环依赖的机理(底层实现)
Spring的循环依赖的理论依据其实是基于Java的引用传递,当我们获取到对象的引用时,实例化Bean的时候:调用无参数构造方法来完成。对象的属性是可以延后设置的。
通过Bean的生命周期我们知道,Spring的单例对象完成初始化主要分为三步:
- 第一步:实例化Bean,其实也就是调用对象的构造方法实例化对象
- 第二步:Bean属性赋值 ,这一步主要是多bean的依赖属性进行填充
- 第三步:初始化Bean ,调用spring xml中的init 方法。
循环依赖主要发生在第一、第二步。
对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为缓存)。
Spring为了解决单例的循环依赖问题,使用了三级缓存。
Spring在DefaultSingletonBeanRegistry这个类设置了三级缓存:
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
这三个Map的key都是存储Bean的id,
这三级缓存分别指:
- 三级缓存:singletonFactories :存储单例对象工厂,每一个单例Bean对象都会对应一个单例工厂对象。存储创建该单例对象时对应的那个单例工厂对象。
- 二级缓存:earlySingletonObjects :存储提前曝光的单例Bean对象,这个Bean只是完成了初始化第一步。
- 一级缓存:singletonObjects:存储完整的单例Bean对象,这个Bean完成了初始化1、2、3步了。
首先在创建BeanA对象的时候,会把BeanA对象的工厂存入(曝光)在三级缓存里面。
解决循环依赖底层的一个代码是DefaultSingletonBeanRegistry的getSingleton方法:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
Spring使用三个判断,分别从缓存的一级读取到三级:
- 首先一级缓存singletonObjects中获取。如果获取不到,并且对象正在创建中。
- 就再从二级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories获取
- 就从三级缓存singletonFactory(三级缓存)获取
如果获取到了则:从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。
那么BeanA和BeanB的循环依赖完整的解决是:
beanA首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象beanB
此时就尝试去获取beanB,发现beanB还没有被创建,所以走beanB创建流程,beanB在初始化第一步的时候发现自己依赖了对象beanA,
于是尝试获取beanA,尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全)尝试二级缓存earlySingletonObjects(也没有),
尝试三级缓存singletonFactories,由于beanA通过ObjectFactory将自己提前曝光了,所以beanB能够通过ObjectFactory拿到beanA对象
beanB拿到beanA对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。
此时返回beanA中,beanA此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终beanA也完成了初始化,进去了一级缓存singletonObjects中,
于是beanB拿到了beanA的对象引用,所以beanB的beanA对象完成了初始化。