循环依赖是指多个Bean之间互相依赖,形成一个闭环的情况。这种情况可能会导致系统无法正常初始化或运行,因此需要一种机制来解决循环依赖问题。
Spring解决循环依赖问题的核心思想是延迟注入和三级缓存机制。下面详细介绍这些概念:
1. 延迟注入(Lazy Initialization):
Spring默认情况下是延迟注入Bean的属性的,这意味着Spring在实例化Bean时不会立即注入它们的属性引用。相反,它会先创建一个尚未完成初始化的Bean实例,然后将这个实例放入一个"早期单例"缓存中。这个Bean实例已经包含了对其他Bean的引用,但是属性还没有被注入。这样,Spring就避免了循环依赖的直接发生。
2. 三级缓存机制:
Spring引入了三级缓存机制,用于处理Bean的实例化和属性注入过程中的循环依赖。
- singletonObjects:这是Spring容器的一级缓存,用于存储已经完成初始化和属性注入的单例Bean实例。这些Bean可以被其他Bean引用。
- earlySingletonObjects:这是Spring容器的二级缓存,用于存储已经实例化但属性注入尚未完成的Bean实例。这个缓存是为了解决循环依赖问题而存在的。在这个缓存中,Bean实例包含了对其他Bean的引用,但属性注入还没有完成。
- singletonFactories:这是Spring容器的三级缓存,用于存储Bean的工厂方法。当Bean需要被初始化时,Spring会使用工厂方法来创建Bean,并将Bean实例放入这个缓存中。这个缓存也是为了解决循环依赖问题而存在的。
整个循环依赖解决过程如下:
- 首先,Spring创建Bean的实例,但不进行属性注入,将Bean放入earlySingletonObjects缓存中。
- 接着,Spring开始属性注入过程,如果发现需要注入的属性是其他Bean的引用,它会尝试从singletonObjects缓存中获取已经初始化的Bean实例,如果没有找到,就会继续递归初始化该Bean,此时将使用singletonFactories缓存来创建Bean的实例,同时将Bean实例放入earlySingletonObjects缓存中。
- 当Bean的属性注入完成后,Bean实例将从earlySingletonObjects缓存中移除,然后放入singletonObjects缓存中,表示该Bean已经完成初始化,可以被其他Bean引用。
- 如果发现循环依赖,Spring会抛出异常,因为无法解决的循环依赖问题会导致无限递归。
总之,Spring的延迟注入和三级缓存机制有效地解决了循环依赖问题,允许程序员在配置依赖关系时更自由地引用其他Bean,同时保证了Bean的初始化顺序和完整性。但要注意,合理的Bean设计和依赖管理仍然是避免循环依赖问题的最佳实践。
示例:
我们将使用以下三个类:Customer
、Order
和 Product
。
public class Customer {private String name;private Order order;public Customer(String name) {this.name = name;}public void setOrder(Order order) {this.order = order;}public void printDetails() {System.out.println("Customer Name: " + name);System.out.println("Order Details:");order.printOrder();}
}
public class Order {private Product product;public void setProduct(Product product) {this.product = product;}public void printOrder() {System.out.println("Product Details:");product.printProduct();}
}
public class Product {private String name;public Product(String name) {this.name = name;}public void printProduct() {System.out.println("Product Name: " + name);}
}
现在,让我们看看如何在 Spring 中配置这些 Bean,以及如何解决潜在的循环依赖问题。
spring-config.xml
<bean id="customer" class="com.example.Customer"><constructor-arg value="John" />
</bean><bean id="order" class="com.example.Order"><property name="product" ref="product" />
</bean><bean id="product" class="com.example.Product"><constructor-arg value="Laptop" />
</bean>
在这个示例中,Customer
依赖于 Order
,Order
依赖于 Product
,同时 Product
依赖于 Customer
,形成了循环依赖。
Spring 会采用类似前面描述的方式来解决循环依赖:
- 首先,Spring 实例化
Customer
,但不注入Order
。 - 然后,Spring 实例化
Order
,但不注入Product
。 - 接下来,Spring 实例化
Product
。 - 现在,Spring 开始属性注入过程。它尝试为
Customer
注入Order
,从缓存中找不到,因此继续实例化Order
。这时,Order
已经实例化,但还未注入Product
。 - Spring 为
Order
注入Product
,从缓存中找不到,继续实例化Product
。 - 此时,Spring 为
Product
注入Customer
,从缓存中找到已经实例化的Customer
。 - 属性注入完成后,所有 Bean 都会从早期缓存中移除,放入单例缓存中。
最终,Spring 成功解决了这个复杂的循环依赖问题,并且可以正常打印 Customer
、Order
和 Product
的详细信息。
需要注意的是,虽然 Spring 可以解决循环依赖,但在设计应用程序时,仍然建议尽量避免复杂的循环依赖关系,以保持代码的清晰性和可维护性。
三级缓存,提前暴露对象,aop
总:什么是循环依赖问题,A依赖B,B依赖C,C依赖A
分:先说明bean得创建过程:实例化,初始化(填充属性)
1.先创建A对象,实例化A对象,此时A对象中的b属性为空
2.从容器中查找B对象,如果找到了,直接赋值不存在循环依赖问题(不通),找不到直接创建B对象
3.实例化B对象,此时B对象中的a属性为空,填充属性a
4.从容器中查找A对象,找不到,直接创建
此时,如果仔细琢磨的话,会发现A对象,是存在的,只不过此时的A对象不是一个完整的状态,只完成了实例化但是未完成初始化,如果在程序调用过程中,拥有了某个对象的引用,能否在后期给他完成赋值操作,可以优先把非完整状态的对象优先赋值,等待后续操作来完成赋值,相当于提前暴露了某个不完整对象的引用,所以解决问题的核心在于实例化和初始化分开操作,这也是解决循环依赖问题的关键,
当所有的对象都完成实例化和初始化操作之后,还要把完整对象放到容器中,此时在容器中存在对象的几种状态,完成实例化=但未完成初始化,完整状态,因为都在容器中,所以要使用不同的map结构来进行存储,此时就有了一级缓存和二级缓存,如果一级缓存中有了,那么二级缓存中就不会存在同名的对象,因为他们的查找顺序是1,2,3这样的方式来查找的。一级缓存中放的是完整对象,二级缓存中放的是非完整对象,
为什么需要三级缓存?
三级缓存的value类型是ObjectFactory,是一个函数式接口,存在的意义是保证在整个容器的运行过程中同名的bean对象只能有一个。
如果一个对象需要被代理,或者说需要生成代理对象,那么要不要优先生成一个普通对象?要
普通对象和代理对象是不能同时出现在容器中的,因此当一个对象需要被代理的时候,就要使用代理对象覆盖掉之前的普通对象,在实际的调用过程中,是没有办法确定什么时候对象被使用,所以就要求某个对象被调用的时候,优先判断此对象是否需要被代理,类似于一种回调机制的实现,因此传入lambda表达式的时候,可以通过lambda表达式来执行对象的覆盖过程,getEarlyBeanReference()
因此,所有的bean对象在创建的时候要优先放到三级缓存中,在后续的使用过程中,如果需要被代理则返回代理对象,如果不需要被代理,则直接返回普通对象
以下是一个简单的Java代码示例,演示了Spring中的循环依赖以及Spring是如何解决它的。这个示例包含两个类:Person
和 Address
,Person
类依赖于 Address
,而 Address
类也依赖于 Person
。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class Person {private String name;private Address address;@Autowiredpublic Person(Address address) {this.address = address;}public void setName(String name) {this.name = name;}public String getName() {return name;}public Address getAddress() {return address;}
}@Component
public class Address {private String city;private Person person;@Autowiredpublic Address(Person person) {this.person = person;}public void setCity(String city) {this.city = city;}public String getCity() {return city;}public Person getPerson() {return person;}
}
在这个示例中,Person
和 Address
类都使用了构造函数注入,它们互相依赖对方。现在,让我们分析一下Spring是如何解决这个循环依赖的:
- 当Spring容器启动时,它首先创建一个
Person
对象并放入一级缓存(singletonObjects缓存),但尚未完成初始化。 - 接下来,Spring创建一个
Address
对象,并将Person
对象注入到Address
对象中。在注入时,它发现Person
对象已经在一级缓存中,但尚未完成初始化。因此,它创建一个Person
的代理对象并将它注入到Address
对象中,然后将Address
对象放入二级缓存(earlySingletonObjects缓存)。 Person
对象的初始化过程继续,它的构造函数完成并将Address
对象注入。在注入时,Spring发现Address
对象已经在二级缓存中,但尚未完成初始化。因此,它创建一个Address
的代理对象并将它注入到Person
对象中。Address
对象的初始化过程继续,它的构造函数完成。此时,Address
对象和Person
对象都已完成初始化。- 最终,Spring将
Person
和Address
对象放回一级缓存,以供其他Bean使用。代理对象被替换为真正的对象。
这样,Spring成功解决了 Person
和 Address
之间的循环依赖,通过创建代理对象,使得初始化过程可以顺利完成。