This article was first published at blog.cc1234.cc

preface

This article is the most time consuming point is to think of a good title, both dazzling, but also guanghua introversion, it has proved that this is more difficult than cut demand!

Because dependencies between objects are often complex and can cause unexpected problems when used incorrectly, a typical problem is circular dependencies (also known as circular references).

Spring provides us with dependency injection and supports circular dependency injection under certain scenarios (injection of singleton beans)

The main purpose of this article is to examine how Spring handles circular dependencies in Bean creation.

I’ll look at what circular dependencies are and what’s bad about them, and finally look at how Spring handles them through the source code.

Circular dependencies are not only generated between Spring beans, but also between system modules and systems. This is a typical bad taste and should be avoided as far as possible.

What is a cyclic dependency

Circular dependencies refer to dependencies between multiple objects that form a closed loop.

The following figure shows A circular dependency formed by two objects, A and B

The following figure shows a circular dependency formed by multiple objects

In reality, due to the deep level of dependence, complex relationship and other factors, circular dependence may not be so clear.

Why avoid circular dependencies

Circular dependencies can cause many unexpected problems for a system, which we’ll discuss briefly below

First, circular dependence will have a domino effect

In other words, if a pebble falls on a calm lake, the ripples will instantly spread around.

Cyclic dependence forms a cyclic dependence, and an unstable change in one point of the ring will lead to an unstable change in the whole ring

The actual experience is

  • It is difficult to write tests for code, because volatility results in unstable tests being written
  • It’s hard to refactor because of interdependencies, where you change one object and it automatically affects other dependent objects
  • It’s hard to maintain, and you can’t imagine the consequences of your changes
  • .

2. Cyclic dependencies cause memory overflow

Refer to the code below

public class AService {
  private BService bService = new BService();
}

public class BService {
  private AService aService = new AService();
}
Copy the code

You get a stack overflow error when you create an object with new AService().

If you know the Java initialization order you should know why this happens.

When calling new AService(), the bService property will be initialized first, and the bService initialization will be initialized to the AService, which will form a circular call, and eventually cause the call stack memory overflow.

Spring’s example of circular dependencies

Let’s show circular dependency injection in Spring with a simple example. I show a constructor injection and a Field injection circular dependency example

  • Constructor injection

    @Service
    public class AService {
      
      private final BService bService;
      
      @Autowired
      public AService(BService bService) {
        this.BService = bService
      }
      
    }
    Copy the code
    @Service
    public class BService {
      
      private final AService aService;
      
      @Autowired
      public BService(AService aService) {
        this.aService = aService; }}Copy the code
  • The Field injection

    @Service
    public class AService {
      
      @Autowired
      private BService bService;
      
    }
    Copy the code
    @Service
    public class BService {
      
      @Autowired
      private AService aService;
      
    }
    Copy the code

    Setter injection is similar to Feild injection

If you start the Spring container, the way the constructor is injected will throw a BeanCreationException, indicating that you have a cyclic dependency.

However, the Field injection mode will start normally and the injection will be successful.

This shows that Spring can handle circular dependencies, but only if you do it the way it handles them.

It’s important to note that Prototype beans, for example, don’t handle the injection of circular dependencies.

A method to detect cyclic dependencies

When we look at how Spring’s Field injection solves circular dependencies, let’s look at how to detect circular dependencies

In a circular dependency scenario, we can identify the following constraints

  1. Dependencies are the structure of a graph
  2. Dependence is directed
  3. Circular dependencies indicate that dependencies produce cycles

Now, we know that detecting cyclic dependence is essentially detecting whether there is a loop in a graph, and that’s a very simple algorithmic problem.

A HashSet is used to keep track of the elements that appear in the direction of the dependency. When a repeating element occurs, the ring is created, and the repeating element is the starting point of the ring.

Refer to the figure below, the red nodes represent the points where the loops occur

Taking the first graph as an example, the direction of dependence is A->B->C->A, and it is easy to detect that A is the circular point.

How does Spring handle circular dependencies

Spring can handle cyclic dependencies (Field injection) for singleton beans. In this section, we’ll take a look at how it does it on paper

First, let’s simplify Spring’s Bean creation lifecycle into two steps: instantiation -> dependency injection, as shown in the figure below

Instantiation is equivalent to creating a concrete object with new, and dependency injection is equivalent to assigning values to properties of the object

Let’s extend this process to the creation of two interdependent beans, as shown in the figure below

A needs to instantiate B when performing dependency injection, and B will instantiate A when performing dependency injection, forming A very typical dependency ring.

The node that generates the ring is B during the dependency injection phase. If we “cut” it off, there will be no ring, as shown in the figure below

This does eliminate circular dependencies, but it brings up another problem. B is not dependency injected, which means B is incomplete. What can we do about that?

At this point, A has been created and maintained in the Spring container. A holds A reference to B, and Spring maintains A reference to B without dependency injection

When Spring actively creates B, it can get A reference to B directly (eliminating instantiation), and when it performs dependency injection, it can also get A reference to A directly from the container, thus creating B

B, which is held by A without dependency injection, is the same reference object as B, which is created separately in the following process. After B performs dependency injection, B held by A is A complete Bean.

Show me the code

Generalities without code are soulless

I’ve drawn a simplified flowchart to show the creation of a Bean (omits Spring’s BeanPostProcessor, Aware, etc.). I’d like you to go through it before we look at the source code.

The entry starts directly with the getBean(String) method and ends with the populateBean, and the processing for analyzing the cyclic dependencies is sufficient

GetBean (String) is a method of AbstractBeanFactory. It calls doGetBean (String) internally.

public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
 	@Override
	public Object getBean(String name) throws BeansException {
		return doGetBean(name, null.null.false);
	}
  
  protected <T> T doGetBean(final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly){.../ / # 1Object sharedInstance = getSingleton(beanName); .final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
    if (mbd.isSingleton()) {
      / / # 2
    	sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
						@Override
						public Object getObject(a) throws BeansException {
              	/ / # 3
								returncreateBean(beanName, mbd, args); }}); }...return(T)bean; }}Copy the code

I simplified the method body of doGetBean to correspond to the flow chart so that we can easily find the following invocation flow

doGetBean -> getSingleton(String) -> getSingleton(String, ObjectFactory)
Copy the code

GetSingleton is DefaultSingletonBeanRegistry overloaded methods

DefaultSingletonBeanRegistry maintained the three different states Map used to cache the Bean, later we analyze getSingleton when used

/** maintains all created beans */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/** Maintains the ObjectFactory */ for the Bean being created
private finalMap<String, ObjectFactory<? >> singletonFactories =newHashMap<String, ObjectFactory<? > > (16);

/** maintains all semi-finished beans */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
Copy the code

GetSingleton (String, Boolean); getSingleton(String, Boolean); getSingleton(String, Boolean);

From the diagram we can see the following query hierarchy

singletonObjects =>  earlySingletonObjects => singletonFactories
Copy the code

Combined with source code

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
  @Override
	public Object getSingleton(String beanName) {
		return getSingleton(beanName, true);
	}
  
  protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // Get the created Bean from singletonObjects
		Object singletonObject = this.singletonObjects.get(beanName);
    
    // If no Bean has been created, but the Bean is being created
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // Get the instantiated Bean from earlySingletonObjects
				singletonObject = this.earlySingletonObjects.get(beanName);
      
      	// If there is no instantiated Bean, but the parameter allowEarlyReference is true
				if (singletonObject == null && allowEarlyReference) {
          // Get the ObjectFactory from singletonFactoriesObjectFactory<? > singletonFactory =this.singletonFactories.get(beanName);
					if(singletonFactory ! =null) {
            // Use ObjectFactory to get the Bean instance
						singletonObject = singletonFactory.getObject();
            
            // Save the instance and clean up the ObjectFactory
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName); }}}return(singletonObject ! = NULL_OBJECT ? singletonObject :null); }}Copy the code

GetSingleton (String, ObjectFactory) ¶ getSingleton(String, ObjectFactory) ¶

public Object getSingleton(String beanName, ObjectFactory
        singletonFactory) {...// Get the cached Bean
    Object singletonObject = this.singletonObjects.get(beanName);
			if (singletonObject == null) {...// The tag Bean is being created
        beforeSingletonCreation(beanName);
				boolean newSingleton = false; .// Create a new Bean, which actually calls the createBean method
        singletonObject = singletonFactory.getObject();
        newSingleton = true; .if (newSingleton) {
          / / cache the beanaddSingleton(beanName, singletonObject); }}return(singletonObject ! = NULL_OBJECT ? singletonObject :null);
	}
Copy the code

The process is so clear that there is no need to draw it. Simply create a Bean using the ObjectFactory passed in if the beanName cannot find the Bean.

From the first snippet we can see that the ObjectFactory’s getObject method actually calls the createBean method

sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
						@Override
						public Object getObject(a) throws BeansException {
              	/ / # 3
								returncreateBean(beanName, mbd, args); }});Copy the code

CreateBean are implemented AbstractAutowireCapableBeanFactory, internal call doCreateBean methods

DoCreateBean takes care of bean instantiation, dependency injection, and so on.

Refer to the below

CreateBeanInstance is responsible for instantiating a Bean object.

AddSingletonFactory saves the reference to the singleton object through the ObjectFactory, which is then cached in the Map (executed before dependency injection).

The populateBean mainly performs dependency injection.

The following is the source code, the basic flow chart with the above to keep consistent, the details of the place I also marked the annotation

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
      implements AutowireCapableBeanFactory {
	@Override
	protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {...return doCreateBean(beanName, mbdToUse, args);
	}
	
	protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) {... BeanWrapper instanceWrapper =null;
		if (instanceWrapper == null) {
			// Instantiate the Bean
			instanceWrapper = createBeanInstance(beanName, mbd, args);
		}
		finalObject bean = (instanceWrapper ! =null ? instanceWrapper.getWrappedInstance() : null);
		// Allow preexposure of singleton beans
		boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
		if (earlySingletonExposure) {
			// Create and cache the ObjectFactory
			addSingletonFactory(beanName, new ObjectFactory<Object>() {
				@Override
				public Object getObject(a) throws BeansException {
					// If you ignore the BeanPostProcessor logic, this method actually returns the bean object directly
					// The bean object is the one instantiated earlier
					returngetEarlyBeanReference(beanName, mbd, bean); }}); }...// Dependency injectionpopulateBean(beanName, mbd, instanceWrapper); . }}Copy the code

If you look closely at the code snippet above, you’ve figured out the key to Spring’s handling of circular dependencies

Taking A and B cyclic dependency injection as an example, we draw A complete injection flow chart

Notice the yellow nodes in the figure above. Let’s go through the process again

  1. When I create A, I’m going to putInstantiated AthroughaddSingleFactory(yellow node) method cache, and then perform dependency injection B.
  2. The injection will go through the creation process, and finally B will perform dependency injection A.
  3. Since the reference to A has been cached in the first step, it can pass when creating A againgetSingletonMethod gets this advance reference to A (get the original cached objectFactory and get the object reference from it), and then dependency injection for B is complete.
  4. After B is created, the dependency injection for A is completed, so A is created successfully.

This completes the dependency injection process

conclusion

It’s time to wrap up, but Spring’s handling of cyclic dependencies on singleton beans is not that complicated, and with a little extension, we can use this approach to handle similar problems.

The inevitable article still leaves a lot of holes, such as

  • I haven’t explained in detail why constructor injection can’t handle cyclic dependencies
  • Did I go into detail about how Spring detects circular dependencies
  • Nor did I explain why Prototype’s beans can’t handle circular dependencies
  • .

Of course, all of this can be found in the Spring Bean creation process (getBean(String) method), the details are left to the reader to find in the source code oh

reference

  1. Circular_dependency