1. 依赖注入方式
依赖注入(DI): 是指通过外部配置,将依赖关系注入到对象中。依赖注入有四种主要方式:构造器注入、setter方法注入、接口注入以及注解注入。
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 容器;
- 设计原则问题:更容易违背单一设计原则。
总结
优点 |
缺点 |
应用 |
|
构造方法注入 |
|
代码臃肿,可读性差 |
依赖项是必需的,且不需要在对象生命周期内发生变化(官方推荐) |
Setter 注入 |
|
无法注入不可变对象 |
依赖项可能发生变化,或者是可选的 |
接口注入 |
|
|
注入依赖项需要规范化或者实现快速切换时 |
注解注入 |
实现使用简单,方便维护 |
|
高频使用 |
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。