How does Spring solve circular dependencies?

Keywords: Spring

introduce

Let's talk about circular dependency first. Spring needs to inject B when initializing A, and A when initializing B. after spring starts, these two beans must be initialized

There are two scenarios for Spring's circular dependency

  1. Loop dependency of constructor
  2. Circular dependency of attributes

For the loop dependency of the constructor, you can use the @ Lazy annotation in the constructor to delay loading. When injecting dependencies, the proxy object is injected first, and then the object is created to complete the injection when it is used for the first time

The circular dependency of attributes is mainly solved through three map s

Loop dependency of constructor

@Component
public class ConstructorA {

	private ConstructorB constructorB;

	@Autowired
	public ConstructorA(ConstructorB constructorB) {
		this.constructorB = constructorB;
	}
}

@Component
public class ConstructorB {

	private ConstructorA constructorA;

	@Autowired
	public ConstructorB(ConstructorA constructorA) {
		this.constructorA = constructorA;
	}
}

@Configuration
@ComponentScan("com.javashitang.dependency.constructor")
public class ConstructorConfig {
}
1234
public class ConstructorMain {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext context =
				new AnnotationConfigApplicationContext(ConstructorConfig.class);
		System.out.println(context.getBean(ConstructorA.class));
		System.out.println(context.getBean(ConstructorB.class));
	}
}

When running the main method of ConstructorMain, an exception will be reported in the first line, indicating that Spring cannot initialize all beans, that is, Spring cannot solve the above circular dependency.

We can solve this problem by adding @ Lazy annotation to the parameters of constructor a or constructor B

@Autowired
public ConstructorB(@Lazy ConstructorA constructorA) {
	this.constructorA = constructorA;
}

Because we mainly focus on the cyclic dependency of attributes, the cyclic dependency of constructors will not be analyzed too much

Circular dependency of attributes

Let's first demonstrate what is a circular dependency of attributes

@Component
public class FieldA {

	@Autowired
	private FieldB fieldB;
}

@Component
public class FieldB {

	@Autowired
	private FieldA fieldA;
}

@Configuration
@ComponentScan("com.javashitang.dependency.field")
public class FieldConfig {
}
1234
public class FieldMain {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext context =
				new AnnotationConfigApplicationContext(FieldConfig.class);
		// com.javashitang.dependency.field.FieldA@3aa9e816
		System.out.println(context.getBean(FieldA.class));
		// com.javashitang.dependency.field.FieldB@17d99928
		System.out.println(context.getBean(FieldB.class));
	}
}

The Spring container starts normally and can obtain two beans, FieldA and FieldB

The circular dependence of attributes is often asked in interviews. Generally speaking, it's not complicated, but it involves the initialization process of Spring Bean, so it feels complicated. I'll write a demo to demonstrate the whole process

The initialization process of Spring beans is actually quite complex. In order to facilitate understanding the Demo, I divide the initialization process of Spring beans into two parts

  • bean instantiation process, that is, call the constructor to create the object

  • The initialization process of a bean, that is, filling in various properties of the bean

After the bean initialization process is completed, the bean can be created normally

Let's start writing the Demo. The ObjectFactory interface is used to produce beans, which is the same as the interface defined in Spring

public interface ObjectFactory<T> {
	T getObject();
}

public class DependencyDemo {

	// Initialized Bean
	private final Map<String, Object> singletonObjects =
			new ConcurrentHashMap<>(256);

	// The factory corresponding to the Bean being initialized. At this time, the object has been instantiated
	private final Map<String, ObjectFactory<?>> singletonFactories =
			new HashMap<>(16);

	// Store the Bean being initialized. The object is put in before it is instantiated
	private final Set<String> singletonsCurrentlyInCreation =
			Collections.newSetFromMap(new ConcurrentHashMap<>(16));

	public  <T> T getBean(Class<T> beanClass) throws Exception {
		// The class name is the name of the Bean
		String beanName = beanClass.getSimpleName();
		// Already initialized, or initializing
		Object initObj = getSingleton(beanName, true);
		if (initObj != null) {
			return (T) initObj;
		}
		// The bean is being initialized
		singletonsCurrentlyInCreation.add(beanName);
		// Instantiate bean
		Object object = beanClass.getDeclaredConstructor().newInstance();
		singletonFactories.put(beanName, () -> {
			return object;
		});
		// Start initializing the bean, that is, populating the properties
		Field[] fields = object.getClass().getDeclaredFields();
		for (Field field : fields) {
			field.setAccessible(true);
			// Get the class of the field to be injected
			Class<?> fieldClass = field.getType();
			field.set(object, getBean(fieldClass));
		}
		// Initialization complete
		singletonObjects.put(beanName, object);
		singletonsCurrentlyInCreation.remove(beanName);
		return (T) object;
	}

	/**
	 * allowEarlyReference The meaning of the parameter is whether Spring allows circular dependency. The default value is true
	 * Therefore, when allowrearlyreference is set to false, the project will fail to start when there is a circular dependency
	 */
	public Object getSingleton(String beanName, boolean allowEarlyReference) {
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null 
				&& isSingletonCurrentlyInCreation(beanName)) {
			synchronized (this.singletonObjects) {
				if (singletonObject == null && allowEarlyReference) {
					ObjectFactory<?> singletonFactory =
							this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
					}
				}
			}
		}
		return singletonObject;
	}

	/**
	 * Determine whether the bean is being initialized
	 */
	public boolean isSingletonCurrentlyInCreation(String beanName) {
		return this.singletonsCurrentlyInCreation.contains(beanName);
	}

}

Test a wave

public static void main(String[] args) throws Exception {
	DependencyDemo dependencyDemo = new DependencyDemo();
	// Pretend to scan out the object
	Class[] classes = {A.class, B.class};
	// Pretend that the project initializes all bean s
	for (Class aClass : classes) {
		dependencyDemo.getBean(aClass);
	}
	// true
	System.out.println(
			dependencyDemo.getBean(B.class).getA() == dependencyDemo.getBean(A.class));
	// true
	System.out.println(
			dependencyDemo.getBean(A.class).getB() == dependencyDemo.getBean(B.class));
}

Isn't it simple? We only used two map s to solve the Spring circular dependency

Two maps can handle circular dependency, so why does Spring use three maps?

The reason is also very simple. When we get the corresponding ObjectFactory from singletonFactories according to BeanName, then we call getObject() to return the corresponding Bean. In our example, the implementation of ObjectFactory is simple, that is, directly returning objects that are instantiated. But in Spring, it is not so simple. The execution process is rather complicated. In order to avoid getting ObjectFactory and calling getObject() each time, we simply cache the objects created by ObjectFactory, so that we can improve efficiency.

For example, a depends on B and C, and B and C depend on A. if caching is not done, initializing B and C will call the getObject() method of ObjectFactory corresponding to a. If you do caching, you only need to call B or C once.

Knowing the idea, we changed the above code and added a cache.

public class DependencyDemo {

	// Initialized Bean
	private final Map<String, Object> singletonObjects =
			new ConcurrentHashMap<>(256);

	// The factory corresponding to the Bean being initialized. At this time, the object has been instantiated
	private final Map<String, ObjectFactory<?>> singletonFactories =
			new HashMap<>(16);

	// Beans produced by the factory corresponding to the cache Bean
	private final Map<String, Object> earlySingletonObjects =
			new HashMap<>(16);

	// Store the Bean being initialized. The object is put in before it is instantiated
	private final Set<String> singletonsCurrentlyInCreation =
			Collections.newSetFromMap(new ConcurrentHashMap<>(16));

	public  <T> T getBean(Class<T> beanClass) throws Exception {
		// The class name is the name of the Bean
		String beanName = beanClass.getSimpleName();
		// Already initialized, or initializing
		Object initObj = getSingleton(beanName, true);
		if (initObj != null) {
			return (T) initObj;
		}
		// The bean is being initialized
		singletonsCurrentlyInCreation.add(beanName);
		// Instantiate bean
		Object object = beanClass.getDeclaredConstructor().newInstance();
		singletonFactories.put(beanName, () -> {
			return object;
		});
		// Start initializing the bean, that is, populating the properties
		Field[] fields = object.getClass().getDeclaredFields();
		for (Field field : fields) {
			field.setAccessible(true);
			// Get the class of the field to be injected
			Class<?> fieldClass = field.getType();
			field.set(object, getBean(fieldClass));
		}
		singletonObjects.put(beanName, object);
		singletonsCurrentlyInCreation.remove(beanName);
		earlySingletonObjects.remove(beanName);
		return (T) object;
	}

	/**
	 * allowEarlyReference The meaning of the parameter is whether Spring allows circular dependency. The default value is true
	 */
	public Object getSingleton(String beanName, boolean allowEarlyReference) {
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null
				&& isSingletonCurrentlyInCreation(beanName)) {
			synchronized (this.singletonObjects) {
				singletonObject = this.earlySingletonObjects.get(beanName);
				if (singletonObject == null && allowEarlyReference) {
					ObjectFactory<?> singletonFactory =
							this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return singletonObject;
	}
}

What we as like as two peas in getSingleton principle is exactly the same as that in org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean).

Sum up a wave

  1. When taking bean s, first get them from singletonObjects (first level cache)
  2. If it cannot be obtained and the object is being created, it is obtained from the earlySingletonObjects (L2 cache)
  3. If you still can't get it, you can get it from singletonFactories (Level 3 cache), then put the obtained object into earlySingletonObjects (Level 2 cache), and clear the singletonFactories (Level 3 cache) corresponding to the bean
  4. After the bean is initialized, put it into singletonObjects (first level cache) and clear the earlySingletonObjects (second level cache) corresponding to the bean

Posted by liamthebof on Thu, 25 Nov 2021 15:17:15 -0800