1. 依赖注入方式
1.1. 构造器注入
1.1.1. 概述
构造器注入是指通过构造方法将依赖项注入到对象中。在构造方法中,将依赖项作为参数传入,然后在对象被创建时将其保存在成员变量中。
构造器注入是一种简单有效的依赖注入方式,可以保证依赖项的不可变性。在实际开发中,如果依赖项是必需的,且不需要在对象生命周期内发生变化,可以考虑使用构造器注入。
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(int id) {
return userRepository.getUserById(id);
}
}
1.1.2. 特点
构造方法注入是 Spring 官方从 4.x 之后推荐的注入方式。
在Spring 4.3 以后,如果我们的类中只有单个构造函数 ,不写 @Autowired注解也可实现依赖注入。这种注入称为隐式注入。
优点
- 可注入不可变对象;注入时对象可用final修饰。在 Java 中 final 对象(不可变)要么直接赋值,要么在构造方法中赋值,所以当使用set方法注入或注解注入 final 对象时,它不符合 Java 中 final 的使用规范,所以就不能注入成功了。
- 注入对象不会被修改;构造方法在对象创建时只会执行一次,因此它不存在注入对象被随时(调用)修改的情况。
- 注入对象会被完全初始化;构造方法是在对象创建之初执行的,因此被注入的对象在使用之前,会被完全初始化 。
- 通用性更好,适用于 IoC 框架还是非 IoC 框架 。
- 固定依赖注入的顺序,避免循环依赖的问题。
缺点
- 代码臃肿,可读性差,不便维护。
1.2. setter方法注入
1.2.1. 概述
Setter方法注入是指通过setter方法将依赖项注入到对象中。在setter方法中,将依赖项作为参数传入,然后将其保存在成员变量中。
Setter方法注入是一种常用的依赖注入方式,可以保证依赖项的可变性。在实际开发中,如果依赖项可能发生变化,或者是可选的,可以考虑使用Setter方法注入。
public class UserService {
private UserRepository userRepository;
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(int id) {
return userRepository.getUserById(id);
}
}
1.2.2. 特点
在Spring 3.x 中,Spring建议使用setter来注入
优点
- 完全符合单一职责的设计原则
- 只有对象是需要时才会注入依赖,而不是在初始化的时候就注入。
- 依赖的可变性。
缺点
- 无法注入一个不可变的对象;
1.3. 接口注入
1.3.1. 概述
接口注入是指通过实现接口将依赖项注入到对象中。在接口中定义依赖项的setter方法,然后在实现类中实现该方法,将依赖项注入到对象中。
接口注入相对于构造方法注入和Setter方法注入,需要定义额外的接口,增加了代码复杂度,但可以保证依赖项的可变性。
public interface UserRepositorySetter {
void setUserRepository(UserRepository userRepository);
}
public class UserService implements UserRepositorySetter {
private UserRepository userRepository;
@Override
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(int id) {
return userRepository.getUserById(id);
}
}
1.4. 注解注入
1.4.1. 概述
注解注入是指通过注解将依赖项注入到对象中。在依赖项上添加注解,然后在对象中使用@Autowired注解将依赖项注入到对象中。
注解注入是一种简单便捷的依赖注入方式,可以保证依赖项的可变性。在实际开发中,如果使用Spring等框架,可以考虑使用注解注入。
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(int id) {
return userRepository.getUserById(id);
}
}
1.4.2. 特点
优点
- 实现简单,使用简单,方便维护
缺点
- 功能性问题:无法注入一个不可变的对象;
- 通用性问题:只能适应于 IoC 容器;
- 设计原则问题:更容易违背单一设计原则。
2. 循环依赖
2.1. 什么是循环依赖?
Spring 循环依赖是指:两个或多个不同的 Bean 对象,相互成为各自的字段,当这两个 Bean 中的其中一个 Bean 进行依赖注入时,会陷入死循环,即循环依赖现象。
@Component
public class UserServiceA {
@Autowire
private UserServiceB userServiceB;
}
@Component
public class UserServiceB {
@Autowire
private UserServiceA userServiceA;
}
2.2. 循环依赖会出现什么问题?
在没有考虑Spring框架的情况下,循环依赖并不会带来问题,因为对象之间相互依赖是非常普遍且正常的现象。但使用Spring框架,我们将创建Bean对象的控制权交给容器,当出现循环依赖时,容器会不知道先创建哪个Bean,会爆异常 BeanCurrentlyInCreationException 。
在Spring框架中,一个对象的实例化并非简单地通过new关键字完成,而是经历了一系列Bean生命周期的阶段。正是由于这种Bean的生命周期机制,才导致了循环依赖问题的出现。要深入理解Spring中的循环依赖,首先需要对Spring中Bean的完整生命周期有所了解。
2.3. Bean生命周期
Spring 管理的对象称为 Bean,通过Spring的扫描机制获取到类的BeanDefinition后,接下来的流程是:
- 解析BeanDefinition以实例化Bean:
- 推断类的构造方法。
- 利用反射机制实例化对象(称为原始对象)。
- 填充原始对象的属性,实现依赖注入。
- 如果原始对象中的方法被AOP增强,CGLIB动态代理继承原始对象生成代理对象。
- 将生成的代理对象存放到单例池(在源码中称为singletonObjects)中,以便下次直接获取。
这个过程简要描述了Spring容器在实例化Bean并处理AOP时的流程。
在Spring中,Bean的生成过程涉及多个复杂步骤,远不止上述简要提及的4个步骤。除了所列步骤外,还包括诸如Aware回调、初始化等繁琐流程。
2.4. 代码层面实现
2.4.1. 初始定义
定义一个学生类以及教师类
/**
* 定义一个Teacher对象,并交给IOC管理
*/
@Component
@Data
public class Teacher {
//老师姓名
private String name;
//注入关联Student对象@Autowired
private Student student;
}
/**
* 定义一个学生对象,并交给IOC管理
*/
@Component
@Data
public class Student {
//学生姓名
private String name;
//注入关联Teacher对象@Autowired
private Teacher teacher;
}
2.4.2. 配置解决
从SpringBoot2.6.0以后的版本开始,SpringBoot默认不会自动解决set方式循环依赖问
题,如果要解决我们需要在application.yml中添加配置解决循环依赖
spring:
main:
#允许spring中利用set方式解决自动循环依赖问题
allow-circular-references: true
2.4.3. 在构造方法上添加@Lazy
思路:打破循环依赖只需让一个对象实例先初始化完成
/**
* 定义一个Teacher对象,并交给IOC管理
*/
@Component
@Data
public class Teacher {
//老师姓名
private String name;
//注入关联Student对象@Autowired
private Student student;
public Teacher(Student student){
this.student = student;
}
}
/**
* 定义一个学生对象,并交给IOC管理
*/
@Component
@Data
public class Student {
//学生姓名
private String name;
//注入关联Teacher对象@Autowired
private Teacher teacher;
public Student(@Lazy Teacher teacher){
this.teacher = teacher;
}
}
3. 三级缓存
3.1. 概述
而针对循环依赖,Spring通过一些机制来协助开发者解决部分循环依赖问题,这便是三级缓存。
| SingletonObjects | 一级缓存 | 存储完整的 Bean; |
|---|---|---|
| EarlySingletonObjects | 二级缓存 | 存储从第三级缓存中创建出代理对象的 Bean,即半成品的 Bean; |
| SingletonFactory | 三级缓存 | 存储实例化完后,包装在 FactoryBean 中的工厂 Bean; |
public class DefaultSingletonBeanRegistry implements SingletonBeanRegistry {
/**
* 一级缓存
*/
private Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
/**
* 二级缓存
*/
private Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>();
/**
* 三级缓存
*/
private Map<String, ObjectFactory<?>> singletonFactory = new HashMap<>();
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// Consistent creation of early reference within full singleton lock
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
}
在上面的 getSingleton 方法中,先从 SingletonObjects 中获取完整的 Bean,如果获取失败,就从 EarlySingletonObjects 中获取半成品的 Bean,如果 EarlySingletonObjects 中也没有获取到,那么就从 SingletonFactory 中,通过 FactoryBean 的 getBean 方法,获取提前创建 Bean。如果 SingletonFactory 中也没有获取到,就去执行创建 Bean 的方法。
3.2. 解决循环依赖
Spring 产生一个完整的 Bean 可以看作三个阶段:
- createBean:实例化 Bean;
- populateBean:对 Bean 进行依赖注入;
- initializeBean:执行 Bean 的初始化方法;
产生循环依赖的根本原因是:对于一个实例化后的 Bean,当它进行依赖注入时,会去创建它所依赖的 Bean,但此时它本身没有缓存起来,如果其他的 Bean 也依赖于它自己,那么就会创建新的 Bean,陷入了循环依赖的问题。
所以,三级缓存解决循环依赖的根本途径是:当 Bean 实例化后,先将自己存起来,如果其他 Bean 用到自己,就先从缓存中拿,不用去创建新的 Bean 了,也就不会产生循环依赖的问题了。过程如下图所示:
在 Spring 源码中,调用完 createInstance 方法后,然后就把当前 Bean 加入到 SingletonFactory 中,也就是在实例化完毕后,就加入到三级缓存中;
Spring通过三级缓存对Bean延迟初始化解决循环依赖。
具体如下:
- singletonObjects缓存:这是 Spring 容器用来缓存完全初始化好的单例 bean 实例的缓存。
- earlySingletonObjects缓存:这个缓存是用来保存被实例化但还未完全初始化的 bean (半成品)的引用。
- singletonFactories缓存:这个缓存保存的是用于创建 bean 实例的 ObjectFactory,用于支持循环依赖的延迟初始化。
Spring 通过这三级缓存的组合,来确保在循环依赖情况下,能够正常初始化 bean。当一个 bean 在初始化过程中需要依赖另一个还未初始化的 bean 时,Spring 会调用相应的 对象工厂来获取对应的 bean 半成品实例,这样就实现了循环依赖的延迟初始化。一旦 bean 初始化完成,它就会被移动到正式的单例缓存中。
3.3. 一层和两层缓存可以吗?
只使用一级缓存的情况,是不能够解决循环依赖的,有下面两个原因:
- 当我们仅使用一级缓存时,Bean 在初始化完成后被放入缓存中。但这依然会导致循环依赖问题。因为依赖注入发生在初始化之前,所以在依赖注入时,无法从缓存中获取到相应的 Bean,从而再次引发循环依赖。
- 如果我们在 Bean 实例化后立即将其放入缓存呢?这也不可行。因为我们忽略了代理对象(Spring AOP)的存在。如果创建的 Bean 是代理对象,则必须在实例化后立即创建。然而,这会带来新的问题:JDK Proxy 代理对象仅实现了目标类的接口,这会导致依赖注入时无法找到相应的属性和方法,从而导致错误。 换句话说,提前创建的代理对象缺乏原始对象的属性和方法。
只使用二级缓存,是可以解决的,但是为什么不用呢?
- 对于普通对象,使用二级缓存可以解决循环依赖问题。对象实例化后,放入第一级缓存。如果其他对象需要依赖注入该对象,可以直接从第一级缓存中获取。待对象初始化完成后,再写入第二级缓存。
- 然而,对于代理对象而言,情况就复杂了许多。如果循环依赖注入的对象是代理对象,我们就需要在对象实例化后提前创建代理对象,也就是提前创建所有代理对象。但目前的 Spring AOP 设计中,代理对象的创建是在初始化方法中的 AnnotationAwareAspectJAutoProxyCreator 后置处理器创建的。这与 Spring AOP 的代理设计原则相悖。故Spring增加了SingletonFactory,存储着 FactoryBean。