java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring 依赖注入和循环依赖

Spring 依赖注入和循环依赖的实例解析

作者:纯洁的小魔鬼

依赖注入的主要目的是降低类之间的耦合度,使得代码更加灵活、可维护和可测试,这篇文章主要介绍了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 的实例
    @Bean
    public MyDependency myDependency() {
        return new MyDependency();
    }
    // 用于创建 MyService 的实例,并通过构造函数将 MyDependency 的实例传递进去
    @Bean
    public 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;
    @Autowired
    public 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, 改变该依赖对象的message
		MyDependency newDependency = context.getBean(MyDependency.class);
		newDependency.setMessage("修改之后的输出");
		myService.doSomething();
	}
}

输出:

默认输出
修改之后的输出

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

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

@Component
public class MyService {
    public MyDependency myDependency;
    @Autowired
    public 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() {
    }
    @Autowired
    public 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
    @Bean
    public MyDependency myDependency() {
        return new MyDependency();
    }
    // 定义 MyService Bean
    @Bean
    public 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 Bean
		MyService 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 注解
    @Autowired
    public 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 方法注入
    @Autowired
    public 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 {
    @Autowired
    private 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;
    @Autowired
    public ClassA(ClassB classB) {
        this.classB = classB;
    }
    public void print() {
        System.out.println("ClassA");
    }
}
@Component
public class ClassB {
    private ClassA classA;
    @Autowired
    public 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;
    @Autowired
    public void setClassB(ClassB classB) {
        this.classB = classB;
    }
    public void print() {
        System.out.println("ClassA");
    }
}
@Component
public class ClassB {
    private ClassA classA;
    @Autowired
    public 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 {
    @Autowired
    private ClassB classB;
    public void print() {
        System.out.println("ClassA");
    }
}
@Component
public class ClassB {
    @Autowired
    private 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
	 */
	@Nullable
	protected 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
    @Autowired
    public ClassA(ClassB classB) {
        this.classB = classB;
    }
    public void print() {
        System.out.println("ClassA");
    }
}
@Component
public class ClassB {
    private ClassA classA;
    @Lazy
    @Autowired
    public 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;
    @Autowired
    public void setClassB(ClassB classB) {
        this.classB = classB;
    }
    public void print() {
        System.out.println("ClassA");
    }
}
@Scope("prototype")
@Component
public class ClassB {
    private ClassA classA;
    @Autowired
    public 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 内部,可以使用工厂方法或者手动创建实例,然后在需要的时候将依赖注入。

到此这篇关于Spring 依赖注入和循环依赖的文章就介绍到这了,更多相关Spring 依赖注入和循环依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文