Spring 依赖注入和循环依赖

一.依赖注入的方式

       依赖注入(Dependency Injection,简称DI)是一种软件设计模式和编程技术,用于实现类之间的解耦和依赖关系的管理。它的核心思想是:在对象创建时,由外部容器负责将该对象所依赖的其他对象(依赖)传递进来,而不是由对象自己去创建或查找这些依赖对象

        依赖注入的主要目的是降低类之间的耦合度,使得代码更加灵活、可维护和可测试。通过依赖注入,一个类不需要关心它所依赖的对象是如何创建和获取的,而只需定义需要哪些依赖,并由外部容器负责提供这些依赖。这样可以减少代码中的硬编码,使得代码更加模块化和可复用。

        不同的注入方式各有优劣,选择合适的方式取决于具体的业务场景和设计要求。构造函数注入通常用于强制的、不可变的依赖关系,而 setter 方法注入和成员变量注入则更适合可选的、可变的依赖关系。使用 Spring 时,可以根据具体情况选择最合适的依赖注入方式。

依赖注入通常有以下几种方式:

1. 构造函数注入(Constructor Injection)

        通过构造函数来实现对依赖对象的注入。使用构造函数注入时,Spring 容器会在创建对象时自动解析构造函数的参数,并将相应的依赖对象传递给构造函数,从而完成对象的创建和注入

        构造函数注入是 Spring 中常用的一种依赖注入方式,它使得对象的创建和依赖注入更加清晰和类型安全。相比于其他注入方式(如属性注入或方法注入),构造函数注入更具有优势,因为它确保了在对象创建时所有必需的依赖都已经得到了满足。

1. 构造函数注入是通过在类的构造函数中传入依赖对象来实现的。

2. 构造函数注入强制依赖对象在创建实例时必须提供,是类的必要组成部分, 确保了类在实例化时的完整性。

3. 构造函数注入通常用于表示强制的、必须的依赖关系,提供了更好的不可变性和线程安全性

4. 使用构造函数注入时,类的依赖关系在实例化时就被解决,对象的状态一经创建就不能更改(如果既有构造方法注入, 又提供了 setter 方法, 这时依赖对象是可以改变的, 如果不想被改变, 需要把注入的对象设置 final, 这样就无法设置 setter 方法了, 此时对象不可变, 指的是不能变成两个不同的对象, 但是就算加了final, 依赖对象内的一些属性还是可以改变的, 而 setter 注入和成员变量注入则无法设置为final)。

1.1 @Configuration 方式实现

a. 类 MyService 需要注入 MyDependency 类, 在构造方法中声明需要依赖的参数

public class MyService {private final MyDependency myDependency;public MyService(MyDependency myDependency) {this.myDependency = myDependency;}public MyDependency getMyDependency() {return myDependency;}

b. MyService 类中需要注入的依赖类

public class MyDependency {public void test(){System.out.println("测试");}}

c. 定义配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class AppConfig {// 用于创建并配置 MyDependency 的实例@Beanpublic MyDependency myDependency() {return new MyDependency();}// 用于创建 MyService 的实例,并通过构造函数将 MyDependency 的实例传递进去@Beanpublic MyService myService(MyDependency myDependency) {return new MyService(myDependency);}
}

d. 使用 MyService 的 bean

	public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);MyService myService = context.getBean(MyService.class);myService.getMyDependency().test();}

执行后会打印 "测试", 说明 MyService 实例创建成功, 并且成功注入了 MyDependency

1.2 @Autowired 方式实现

a. 构造函数用 @Autowired 修饰, 且 MyService 使用 @Component 标注为 Bean

@Component
public class MyService {public final MyDependency myDependency;@Autowiredpublic MyService(MyDependency myDependency) {this.myDependency = myDependency;}public void doSomething() {System.out.println(myDependency.getMessage());}}

b. 所依赖的 MyDependency 类同样标注为 Bean

@Component
public class MyDependency {private String message = "默认输出";public void setMessage(String message) {this.message = message;}public String getMessage() {return message;}}

c. 使用 MyService 的 bean

@SpringBootApplication
public class Application {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(Application.class, args);MyService myService = context.getBean(MyService.class);myService.doSomething();// 从容器中取出 MyDependency, 改变该依赖对象的messageMyDependency newDependency = context.getBean(MyDependency.class);newDependency.setMessage("修改之后的输出");myService.doSomething();}}

输出:

默认输出
修改之后的输出

说明 MyService 实例创建成功, 并且成功注入了 MyDependency, 并且虽然加了 final, 但是 message 的属性还是可以发生变化

d. 如果不将依赖设为 final, 然后再为 依赖设置一个 setter 方法的话

@Component
public class MyService {public MyDependency myDependency;@Autowiredpublic MyService(MyDependency myDependency) {this.myDependency = myDependency;}public void setMyDependency(MyDependency myDependency) {this.myDependency = myDependency;}public void doSomething() {System.out.println(myDependency.getMessage());}}

e.进行一下测试

@SpringBootApplication
public class Application {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(Application.class, args);MyService myService = context.getBean(MyService.class);myService.doSomething();// 改变所依赖的对象MyDependency newDependency = new MyDependency();newDependency.setMessage("修改之后的输出");// 设置为新的依赖对象myService.setMyDependency(newDependency);myService.doSomething();// 容器中的依赖对象的输出MyDependency oldDependency = context.getBean(MyDependency.class);System.out.println(oldDependency.getMessage());}}

输出:

默认输出
修改之后的输出 
默认输出

可见 MyDependency 对象被改变了, 但是 MyDependency 的bean还是不变的, setter 注入和成员变量注入更是如此

1.3 需要注意的地方

        如果该类的构造方法只有一个, 可以省略 @Autowired 注解。如果该类中存在多个构造函数, 则需要加上 @Autowired 注解,以明确指明这个构造函数负责依赖的的注入。

        如果需要注入的是一个接口, 并且有多个实现类, 则我们需要 @Qualifier() 注解标注我们需要注入的类

a. 需要注入的接口和实现类

public interface IMyDependency {void test();}
@Component("myDependency")
public class MyDependency implements IMyDependency{public void test(){System.out.println("测试");}}
@Component("myDependency2")
public class MyDependency2  implements IMyDependency{public void test(){System.out.println("测试2");}}

b. 注入 IMyDependency 接口, 并且存在多个构造函数

@Component
public class MyService {private IMyDependency myDependency;public MyService() {}@Autowiredpublic MyService(@Qualifier("myDependency2")IMyDependency myDependency) {this.myDependency = myDependency;}public IMyDependency getMyDependency() {return myDependency;}}

c.使用 MyService 的 bean

@SpringBootApplication
public class Application {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(Application.class, args);MyService myService = context.getBean(MyService.class);myService.getMyDependency().test();}

执行后会打印 "测试2", 说明 MyService 实例创建成功, 并且成功注入了 MyDependency2

2. 属性注入(Setter Injection)

        通过类的属性(setter 方法)来传递依赖对象。在类的属性上使用注解或配置来标识依赖关系,并由外部容器在创建类的实例后,通过 setter 方法将依赖对象注入。

1. Setter 方法注入是一种非强制性的注入方式,与构造函数注入相对。

2. 在 Setter 方法注入中,Spring 容器会在实例化 Bean 后,通过调用对象的相应 setter 方法,将依赖的对象或值注入到对象中

3. 通过 Setter 方法注入,可以在对象实例化后动态地设置其依赖属性。这种方式相对灵活,可以允许对象在运行时更改其依赖,但同时也可能导致一些问题,例如在没有正确设置依赖的情况下调用对象的方法可能会引发空指针异常。因此,在使用 Setter 方法注入时,需要确保对象的依赖在调用其方法前已经正确设置。

2.1 @Configuration 方式实现

a. 类 MyService 需要注入 MyDependency 类, 在 setter 方法中声明需要依赖的参数

public class MyService {private MyDependency myDependency;public void setMyDependency(MyDependency myDependency) {this.myDependency = myDependency;}public void test() {myDependency.test();}}

b. MyService 类中需要注入的依赖类

public class MyDependency {public void test(){System.out.println("测试");}}

c. 定义配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class AppConfig {// 定义 MyDependency Bean@Beanpublic MyDependency myDependency() {return new MyDependency();}// 定义 MyService Bean@Beanpublic MyService myService(MyDependency dependency) {MyService service = new MyService();service.setMyDependency(dependency);return service;}
}

d. 使用 MyService 的 bean

public class Application {public static void main(String[] args) {// 创建 Spring 容器并加载配置类AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);// 获取 MyService BeanMyService myService = context.getBean(MyService.class);// 调用 MyService 的方法myService.test();// 关闭容器context.close();}}

执行后会打印 "测试", 说明 MyService 实例创建成功, 并且成功注入了 MyDependency

2.2 @Autowired 方式实现

a. setter方法用 @Autowired 修饰, 且 MyService 使用 @Component 标注为 Bean

@Component
public class MyService {private MyDependency myDependency;// setter 方法上加 @Autowired 注解@Autowiredpublic void setMyDependency(MyDependency myDependency) {this.myDependency = myDependency;}public void test() {myDependency.test();}}

b. 所依赖的 MyDependency 类同样标注为 Bean

@Component
public class MyDependency {public void test(){System.out.println("测试");}}

c. 使用 MyService 的 bean

@SpringBootApplication
public class Application {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(Application.class, args);MyService myService = context.getBean(MyService.class);myService.test();}}

执行后会打印 "测试", 说明 MyService 实例创建成功, 并且成功注入了 MyDependency

2.3 运行时更改其依赖对象

a. setter方法用 @Autowired 修饰, 且 MyService 使用 @Component 标注为 Bean

@Component
public class MyService {private MyDependency myDependency;// Setter 方法注入@Autowiredpublic void setMyDependency(MyDependency myDependency) {this.myDependency = myDependency;}public void doSomething() {System.out.println(myDependency.getMessage());}}

b. 所依赖的 MyDependency 类同样标注为 Bean, 并设置 message 属性, 来观察依赖对象是否发生了变化

@Component
public class MyDependency {private String message = "默认输出";public void setMessage(String message) {this.message = message;}public String getMessage() {return message;}
}

c.进行更改测试

@SpringBootApplication
public class Application {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(Application.class, args);MyService myService = context.getBean(MyService.class);// 调用 MyService 的方法,显示初始注入的消息myService.doSomething();// 在运行时进行更改MyDependency newDependency = new MyDependency();newDependency.setMessage("修改之后的输出");// 使用 Setter 方法注入新的 MyDependency 实例myService.setMyDependency(newDependency);// 调用 MyService 的方法,显示修改后的消息myService.doSomething();}
}

输出:

默认输出
修改之后的输出

输出发生了变化, 说明依赖对象发生了改变

3. 成员变量注入

Spring 的成员变量注入是一种通过直接将依赖注入到类的成员变量上来实现依赖注入的方式。通过在成员变量上使用 @Autowired 注解,Spring 容器会自动将匹配的依赖注入到该成员变量中。

1. 成员变量注入是通过直接在类的成员变量上添加注解(如 @Autowired)来实现的, 更加简单。

2. 依赖对象在对象创建后直接通过反射注入到成员变量上。

3. 成员变量注入类似于 setter 方法注入,也允许依赖对象在对象创建后进行变更,对象的依赖可以在对象创建后动态更改。

a. 成员变量用 @Autowired 修饰, 且 MyService 使用 @Component 标注为 Bean

@Component
public class MyService {@Autowiredprivate MyDependency myDependency;public void doSomething() {System.out.println(myDependency.getMessage());}
}

b. 所依赖的 MyDependency 类同样标注为 Bean, 并设置 message 属性, 来观察依赖对象是否发生了变化

@Component
public class MyDependency {private String message = "默认输出";public void setMessage(String message) {this.message = message;}public String getMessage() {return message;}
}

c.测试

@SpringBootApplication
public class Application {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(Application.class, args);MyService myService = context.getBean(MyService.class);// 调用 MyService 的方法,显示初始注入的消息myService.doSomething();// 取出 MyDependency 的bean, 在这基础之上进行修改MyDependency newDependency = context.getBean(MyDependency.class);newDependency.setMessage("修改之后的输出");myService.doSomething();}
}

输出:

默认输出
修改之后的输出

输出发生了变化, 说明依赖对象发生了改变

二.循环依赖注入

       在 Spring 框架中,循环依赖是指两个或多个 Bean 之间互相依赖,形成了一个循环链,导致 Spring 容器无法正确地完成 Bean 的初始化

         为了解决循环依赖问题,Spring 使用了"提前暴露"和"后期填充"的策略。简单来说,就是在创建 Bean 的过程中,如果发现循环依赖,Spring 会尽可能提前暴露一个未完全初始化的 Bean 实例,以便在后续的创建过程中使用,从而打破循环依赖。这需要 Spring 容器在创建和管理 Bean 实例时进行一些复杂的操作,以确保依赖关系正确解析。

        Spring 在默认的情况下主要解决了单例模式下的某些循环依赖的问题,因为 Spring 的默认作用域是单例(Singleton)。在单例作用域下,Spring 可以在容器启动时创建所有的 Bean,并在创建 Bean 实例的同时处理循环依赖。以下主要在单例模式下进行演示。

1.构造方法注入

如果两个类的构造方法参数中存在循环依赖,Spring 无法解决这个问题, 因为构造方法是用来创建对象的初始步骤,执行构造函数是实例化阶段, 其它注入方式是在初始化阶段(构造函数执行完成, 将对象创建出来后,给对象的属性赋值), 如果参数相互引用,那么就会形成一个循环依赖。

@Component
public class ClassA {private ClassB classB;@Autowiredpublic ClassA(ClassB classB) {this.classB = classB;}public void print() {System.out.println("ClassA");}
}@Component
public class ClassB {private ClassA classA;@Autowiredpublic ClassB(ClassA classA) {this.classA = classA;}public void print() {System.out.println("ClassB");}
}@SpringBootApplication
public class Application {public static void main(String[] args) {ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);// 尝试获取 ClassA、ClassB 和 ClassC 的实例ClassA classA = context.getBean(ClassA.class);ClassB classB = context.getBean(ClassB.class);// 调用 print 方法,查看输出classA.print();classB.print();}}

此示例会发生循环依赖

2. setter 方法注入

@Component
public class ClassA {private ClassB classB;@Autowiredpublic void setClassB(ClassB classB) {this.classB = classB;}public void print() {System.out.println("ClassA");}
}@Component
public class ClassB {private ClassA classA;@Autowiredpublic void setClassC(ClassA classA) {this.classA = classA;}public void print() {System.out.println("ClassB");}
}@SpringBootApplication
public class Application {public static void main(String[] args) {ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);// 尝试获取 ClassA、ClassB 和 ClassC 的实例ClassA classA = context.getBean(ClassA.class);ClassB classB = context.getBean(ClassB.class);// 调用 print 方法,查看输出classA.print();classB.print();}}

此示例不会发生循环依赖, 因为 Spring 的 Bean 默认为单例模式, Spring解决了单例模式下 setter 注入方式的循环依赖问题, 具体怎么解决的, 下文细说。

3.成员变量注入

@Component
public class ClassA {@Autowiredprivate ClassB classB;public void print() {System.out.println("ClassA");}
}@Component
public class ClassB {@Autowiredprivate ClassA classA;public void print() {System.out.println("classA");}
}@SpringBootApplication
public class Application {public static void main(String[] args) {ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);// 尝试获取 ClassA、ClassB 和 ClassC 的实例ClassA classA = context.getBean(ClassA.class);ClassB classB = context.getBean(ClassB.class);// 调用 print 方法,查看输出classA.print();classB.print();}}

此示例不会发生循环依赖, 与 setter 方法注入一样

三. Spring 三级缓存

Spring框架中的三级缓存是用于处理单例 Bean 的循环依赖问题的一种机制。这三级缓存包括:

1. singletonObjects这是Spring容器中单例 Bean 的缓存,它保存了已经完全初始化的单例 Bean 的实例。在这个缓存中,Bean 的名字作为键,Bean 的实例作为值。

2. earlySingletonObjects这个缓存包含了尚未完成构造的单例 Bean 的半成品(即尚未执行完构造函数的 Bean 实例)。这些 Bean 是不完整的,但已经创建,用于解决循环依赖问题。

3. singletonFactories这个缓存用于存储用于创建单例 Bean 的工厂对象。这些工厂对象是 Bean 实例化的原始来源。

1. 三级缓存的工作原理

	// 一个 ConcurrentHashMap,用于存储已经完全初始化的单例 Bean 的实例。// 在这个缓存中,Bean 的名字作为键,Bean 的实例作为值。private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);// 另一个 ConcurrentHashMap,用于存储尚未完成构造的单例 Bean 的半成品(即尚未执行完构造函数的 Bean 实例)。// 这些 Bean 是不完整的,但已经创建,用于解决循环依赖问题。private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);// 存储用于创建单例 Bean 的工厂对象。这些工厂对象是 Bean 实例化的原始来源。private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);/*** 返回指定名称的单例bean* 获取指定名字的单例 Bean 的实例,同时处理循环依赖的情况* 检查已经实例化的单例对象,并允许提前引用当前正在创建的单例对象(解决循环引用问题)。* @param beanName bean名称* @param allowEarlyReference 是否允许提前引用* @return*/@Nullableprotected Object getSingleton(String beanName, boolean allowEarlyReference) {// 尝试从 singletonObjects 缓存中获取指定名字的 Bean 实例,如果找到了,就直接返回Object singletonObject = this.singletonObjects.get(beanName);// 如果在 singletonObjects 缓存中找不到 Bean 实例,并且判断 Bean 当前是否正在创建,如果是,// 那么它会尝试从 earlySingletonObjects 缓存中获取 Bean 的半成品。if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {// 如果找到了半成品 Bean, 就返回这个半成品 Bean。singletonObject = this.earlySingletonObjects.get(beanName);// 如果半成品 Bean 为null, 并且允许提前引用的情况下执行以下代码// 这是一个双检锁(或者多检锁模式)if (singletonObject == null && allowEarlyReference) {synchronized (this.singletonObjects) {// 再次尝试从 singletonObjects 缓存中获取指定名字的 Bean 实例singletonObject = this.singletonObjects.get(beanName);// 如果仍为空if (singletonObject == null) {// 再次尝试从 earlySingletonObjects 缓存中获取 Bean 的半成品。singletonObject = this.earlySingletonObjects.get(beanName);// 如果还为空if (singletonObject == null) {// 那么就查看 singletonFactories 缓存中是否有一个工厂对象(ObjectFactory),// 可以用来创建指定名字的单例对象。ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);// 如果工程对象不为空if (singletonFactory != null) {// 使用工厂对象的 getObject() 方法来创建单例对象,并,表示这个对象正在创建中。singletonObject = singletonFactory.getObject();// 将这个对象放入 earlySingletonObjects 缓存中this.earlySingletonObjects.put(beanName, singletonObject);// 然后从 singletonFactories 缓存中移除对应的工厂对象,以避免重复创建。this.singletonFactories.remove(beanName);}}}}}}return singletonObject;}// 当前正在创建中的 Bean 的名字集合private final Set<String> singletonsCurrentlyInCreation =Collections.newSetFromMap(new ConcurrentHashMap<>(16));/*** 用于存储正在创建中的 Bean 的名字。每当 Spring 容器开始创建一个 Bean 时,* 它会将该 Bean 的名字添加到这个集合中,表示这个 Bean 正在创建过程中。* @param beanName* @return*/public boolean isSingletonCurrentlyInCreation(String beanName) {return this.singletonsCurrentlyInCreation.contains(beanName);}

1. 当容器需要获取某个Bean时,它会首先查找singletonObjects缓存,如果找到就直接返回Bean的实例。

2. 如果没有找到,容器会检查earlySingletonObjects缓存,如果存在半成品Bean,就将其完成初始化并返回。

3. 如果仍然没有, 则使用工厂对象ObjectFactory的 getObject() 方法来创建单例对象。放到 earlySingletonObjects 中返回。

三级缓存的主要作用是解决单例 Bean 的循环依赖问题。通过这种机制,Spring 容器能够在 Bean 的创建和初始化过程中提前暴露和处理依赖关系,确保循环依赖能够正确解决。

注意:  这个三级缓存机制仅适用于单例 Bean,对于其他作用域(如原型、会话、请求等),Spring 采用不同的机制来处理依赖。

2. 为什么需要三级缓存

由上我们可以思考一下, 如果只要一级缓存和三级缓存, 而去掉二级缓存, 是不是也可以实现解决循环依赖的需求, 比如在一级缓存 singletonObjects 中如果找不到, 则直接调用三级缓存singletonFactories, 这样的话是不是也可以?

答案是不可以的:

        在 singletonFactories 三级缓存中, ObjectFactory.getObject() 方法所实现的功能是, 如果 bean 被 AOP 切面代理则返回的是 beanProxy 对象,如果未被代理则返回的是原 bean 实例,这样我们就能拿到属性未填充的 Bean 实例,然后从三级缓存移除,放到二级缓存earlySingletonObjects中。

       而 Bean 如果被 AOP 代理了, ObjectFactory.getObject() 每次都会返回一个新的代理对象, 这跟我们的单例模式是相悖的, 所以把生成的代理对象, 放入二级缓存中, 则可以保证生成的代理对象唯一。并且二级缓存 earlySingletonObjects 可以帮助我们更准确的跟踪正在创建中的 Bean。

四.解决循环依赖注入问题

       由上测试可知, Spring 自己只能解决单例模式下, setter方式注入和成员变量注入时的循环依赖问题, 但是没有解决构造方法注入时的循环依赖问题, 而且, 在某些过于复杂的情况下, 即使是 setter 注入或者成员变量注入的情况下, 比如有些依赖是构造方法注入, 有些是成员变量注入, 相互引用时可能也会发生循环依赖的报错, 我们可以采用一些办法解决这些问题。

@Lazy 注解

       Spring 默认情况下,Bean 是在容器启动时加载的。这意味着在 Spring 容器启动时,它会实例化和初始化所有单例(Singleton)作用域的 Bean。这样,在应用程序运行过程中,这些 Bean 已经准备好使用。

        当在一个 Bean 上使用 @Lazy 注解时,这个 Bean 将在首次被访问时才会被实际创建和初始化,而不是在 Spring 容器启动时立即创建。因此可以解决循环依赖的问题

工作原理:

1. Lazy 注解的工作原理是通过创建一个代理对象来实现延迟初始化。当一个Bean被标记为 @Lazy 时,Spring会创建一个代理对象来代替原始的Bean对象。当其他Bean依赖该Bean时,实际上是依赖了代理对象。代理对象会在第一次被访问时,才真正初始化原始的 Bean 对象。

2. 初始化时注入代理对象,真实调用时使用 Spring AOP 动态代理去关联真实对象,然后通过反射完成调用。

3. 加在构造器上,作用域为构造器所有参数,加在构造器某个参数上,作用域为该参数。

4. 作用在接口上,使用 JDK动态代理,作用在类上,使用 CGLib动态代理。

@Component
public class ClassA {private ClassB classB;@Lazy@Autowiredpublic ClassA(ClassB classB) {this.classB = classB;}public void print() {System.out.println("ClassA");}
}@Component
public class ClassB {private ClassA classA;@Lazy@Autowiredpublic ClassB(ClassA classA) {this.classA = classA;}public void print() {System.out.println("ClassB");}
}

五.多例(prototype)模式下的循环依赖

       原型(Prototype)作用域的 Bean, Spring 不会解决循环依赖问题, 这是因为在单例模式下,Spring 会在容器启动时创建所有的 Bean 实例,并且可以提前暴露未完全初始化的实例来解决循环依赖。而多例模式的 Bean 是每次请求时都会创建一个新的实例,而不是在容器启动时就创建。因此,在原型作用域下,如果存在循环依赖,Spring 将无法提前暴露一个未完全初始化的 Bean 实例。这意味着在多例模式下,Spring 不会保留 Bean 实例之间的状态或引用关系,因为它们是独立的。

现在有两个多例 Bean,A 和 B,它们相互依赖,即 A 需要引用 B,同时 B 也需要引用 A。会发生如下情况:

当你请求 A 时,Spring 创建了一个新的 A 实例,并尝试注入 B。

当你请求 B 时,Spring 创建了一个新的 B 实例,并尝试注入 A。

此时,A 和 B 都是全新的实例,它们互相不知道对方的存在,因此无法建立循环依赖关系。

@Scope("prototype")
@Component
public class ClassA {private ClassB classB;@Autowiredpublic void setClassB(ClassB classB) {this.classB = classB;}public void print() {System.out.println("ClassA");}
}@Scope("prototype")
@Component
public class ClassB {private ClassA classA;@Autowiredpublic void setClassC(ClassA classA) {this.classA = classA;}public void print() {System.out.println("ClassB");}
}@SpringBootApplication
public class Application {public static void main(String[] args) {ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);// 尝试获取 ClassA、ClassB 和 ClassC 的实例ClassA classA = context.getBean(ClassA.class);ClassB classB = context.getBean(ClassB.class);// 调用 print 方法,查看输出classA.print();classB.print();}}

程序发生了循环引用报错

解决方法:

1. 重新设计架构避免多例 Bean 之间的循环依赖。可能需要重新组织类的职责,将功能划分为更小的单元,以减少依赖关系的复杂性

2. 引入中介对象引入一个中介对象,将循环依赖关系切断。中介对象可以是单例模式的,用于协调多例 Bean 之间的交互

3. 手动管理依赖解析不依赖 Spring 的自动注入。在每个多例 Bean 内部,可以使用工厂方法或者手动创建实例,然后在需要的时候将依赖注入

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/82633.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【react】慎用useLayoutEffect转而使用useEffect

由于useLayoutEffect钩子是在dom获得后、渲染组件前。因此&#xff0c;如果在useLayoutEffect中设置一些长耗时的&#xff0c;或者死循环之类的任务&#xff0c;会导致内存堆栈溢出。这时候需要转用useEffect。 // 适配全局宽度拉动变化时&#xff0c;legend显示数量React.use…

OpenAI 模型列表模型

〖ChatGPT实践指南 - 零基础扫盲篇⑧〗- OpenAI 的 模型(Model) 介绍 最新模型描述最大 TOKENS训练日期 gpt-4比任何 GPT-3.5 模型都更强大&#xff0c;能够执行更复杂的任务&#xff0c; 并针对聊天进行了优化。将使用我们最新的模型迭代进行更新。 8,192 tokens Up to Sep 20…

[git] 撤销已经push的提交

1.首先先撤销在本地的commit&#xff1a; git reset --soft HEAD~1这段的意思是撤销最近的一次commit&#xff0c;并且保留工作区的修改。 2.撤销了commit之后&#xff0c;使用git push提交变更到远程 git push origin <本地分支名>:<远程分支名> -f注意&#…

【Linux】shell 提示符

​ Shell俗称壳程序&#xff0c;是一种由C语言编写的用于和操作系统交互的命令解析器软件。它用来接收用户输入命令&#xff0c;然后调用相应的应用程序。 Shell同时又是一种程序设计语言。作为命令语言&#xff0c;它交互式解释和执行用户输入的命令或者自动地解释和执行预先…

PyG-GAT-Cora(在Cora数据集上应用GAT做节点分类)

文章目录 model.pymain.py参数设置运行图 model.py import torch.nn as nn from torch_geometric.nn import GATConv import torch.nn.functional as F class gat_cls(nn.Module):def __init__(self,in_dim,hid_dim,out_dim,dropout_size0.5):super(gat_cls,self).__init__()s…

java学习--day6(数组)

文章目录 day5作业今天的内容1.数组1.1开发中为啥要有数组1.2在Java中如何定义数组1.3对第二种声明方式进行赋值1.4对数组进行取值1.5二维数组【了解】1.6数组可以当成一个方法的参数【重点】1.7数组可以当成一个方法的返回值1.8数组在内存中如何分配的【了解】 2.数组方法循环…

PFEA111–20 PFEA111–20 人工智能如何颠覆石油和天然气行业

PFEA111–20 PFEA111–20 人工智能如何颠覆石油和天然气行业 人工智能(AI)和机器学习(ML)等新技术的到来正在改变几十年来行业的运营方式。这些技术正在带来革命性的变革&#xff0c;影响着整个行业。石油和天然气行业在其运营过程中面临着许多挑战&#xff0c;如未连接的环境…

在SpringBoot项目中整合SpringSession,基于Redis实现对Session的管理和事件监听

1、SpringSession简介 SpringSession是基于Spring框架的Session管理解决方案。它基于标准的Servlet容器API&#xff0c;提供了Session的分布式管理解决方案&#xff0c;支持把Session存储在多种场景下&#xff0c;比如内存、MongoDB、Redis等&#xff0c;并且能够快速集成到Spr…

AI 标注终结人工标注,效率高 100 倍,成本为 14%

AI 标注终结人工标注,效率高 100 倍,成本为 14% 稀缺性如何解决数据标注廉价的新方法数据标注自动化AnthropicAutolabel 安装 (Python)AI 标注终结人工标注,效率高 100 倍,成本为 14% 稀缺性 大模型满天飞的时代,AI行业最缺的是什么 算力(显卡)高质量的数据OpenAI 正…

Java21 LTS版本

一、前言 除了众所周知的 JEP 之外&#xff0c;Java 21 还有更多内容。首先请确认 java 版本&#xff1a; $ java -version openjdk version "21" 2023-09-19 OpenJDK Runtime Environment (build 2135-2513) OpenJDK 64-Bit Server VM (build 2135-2513, mixed mo…

activiti7的数据表和字段的解释

activiti7的数据表和字段的解释 activiti7版本有25张表&#xff0c;而activiti6有28张表&#xff0c;activiti5有27张表&#xff0c;绝大部分的表和字段的含义都是一样的&#xff0c;所以本次整理的activiti7数据表和字段的解释&#xff0c;也同样适用于activiti6和5。 1、总览…

pcl--第五节 点云表面法线估算

估算点云表面法线 * 表面法线是几何表面的重要属性&#xff0c;在许多领域&#xff08;例如计算机图形应用程序&#xff09;中大量使用&#xff0c;以应用正确的光源以产生阴影和其他视觉效果。 给定一个几何表面&#xff0c;通常很难将表面某个点的法线方向推断为垂直于该点…

python execute() 使用%s 拼接sql 避免sql注入攻击 好于.format

1 execute(参数一:sql 语句) # 锁定当前查询结果行 cursor.execute("SELECT high, low, vol FROM table_name WHERE symbol %s FOR UPDATE;"% (symbol,)) 2 .format() cursor.execute("SELECT high, low, vol FROM table_name WHERE symbol {} FOR UPDATE;…

Pytorch实现LSTM预测模型并使用C++相应的ONNX模型推理

Pytorch实现RNN模型 代码 import torch import torch.nn as nnclass LSTM(nn.Module):def __init__(self, input_size, output_size, out_channels, num_layers, device):super(LSTM, self).__init__()self.device deviceself.input_size input_sizeself.hidden_size inpu…

CockroachDB集群部署

CockroachDB集群部署 1、CockroachDB简介 CockroachDB(有时简称为CRDB)是一个免费的、开源的分布式 SQL 数据库&#xff0c;它建立在一个事务性和强一致性的键 值存储之上。它由 PebbleDB(一个受 RocksDB/leveldb 启发的 K/B 存储库)支持&#xff0c;并使用 Raft 分布式共识…

TypeScript入门

目录 一&#xff1a;语言特性 二&#xff1a;TypeScript安装 NPM 安装 TypeScript 三&#xff1a;TypeScript基础语法 第一个 TypeScript 程序 四&#xff1a;TypeScript 保留关键字 空白和换行 TypeScript 区分大小写 TypeScript 注释 TypeScript 支持两种类型的注释 …

初识C语言——详细入门一(系统性学习day4)

目录 前言 一、C语言简单介绍、特点、基本构成 简单介绍&#xff1a; 特点&#xff1a; 基本构成&#xff1a; 二、认识C语言程序 标准格式&#xff1a; 简单C程序&#xff1a; 三、基本构成分类详细介绍 &#xff08;1&#xff09;关键字 &#xff08;2&#xf…

fork函数

二.fork函数 2.1函数原型 fork()函数在 C 语言中的原型如下&#xff1a; #include <unistd.h>pid_t fork(void);其中pid_t是一个整型数据类型&#xff0c;用于表示进程ID。fork()函数返回值是一个pid_t类型的值&#xff0c;具体含义如下&#xff1a; 如果调用fork()的…

MyBatis中当实体类中的属性名和表中的字段名不一样,怎么办

方法1&#xff1a; 在mybatis核心配置文件中指定&#xff0c;springboot加载mybatis核心配置文件 springboot项目的一个特点就是0配置&#xff0c;本来就省掉了mybatis的核心配置文件&#xff0c;现在又加回去算什么事&#xff0c;总之这种方式可行但没人这样用 具体操作&…

MFC C++ 数据结构及相互转化 CString char * char[] byte PCSTR DWORE unsigned

CString&#xff1a; char * char [] BYTE BYTE [] unsigned char DWORD CHAR&#xff1a;单字节字符8bit WCHAR为Unicode字符:typedef unsigned short wchar_t TCHAR : 如果当前编译方式为ANSI(默认)方式&#xff0c;TCHAR等价于CHAR&#xff0c;如果为Unicode方式&#xff0c…