Why does spring use level 3 caching to solve circular dependency?

Time:2020-12-1

Focus on “Su San Shuo technology”, reply: there are surprises in development manual and time management.

Maybe some friends don’t know about spring’s circular dependency. Let’s take a look at this example first.

@Service
public class AService {

    private BService bService;

    public AService(BService bService) {
        this.bService = bService;
    }

    public void doA() {
        System.out.println("call doA");
    }
}
@Service
public class BService {

    private AService aService;

    public BService(AService aService) {
        this.aService = aService;
    }

    public void doB() {
        System.out.println("call doB");
    }
}
@RequestMapping("/test")
@RestController
public class TestController {

    @Autowired
    private AService aService;

    @RequestMapping("/doSameThing")
    public String doSameThing() {
        aService.doA();
        return "success";
    }
}
@SpringBootApplication

When we run the main method of the application class to start the service, we report the following exception:

Requested bean is currently in creation: Is there an unresolvable circular reference?

Why does spring use level 3 caching to solve circular dependency?

It is obvious here that there is a cyclic dependence.

What is circular dependence?

Circular dependency is that instance a depends on instance B, and instance B depends on instance a.

Or instance a depends on instance B, instance B depends on instance C, and instance C depends on instance a.

The interdependence between multiple instances, like this, forms a loop, which is called circular dependency.

Why is circular dependence formed?

In the above example, when aservice is instantiated, the constructor public aservice (bservice bservice) will be called, which depends on the instance of bservice. At this time, bservice has not been instantiated yet. The constructor public bservice (aservice aservice) needs to be called to complete the instantiation. The constructor also needs the instance of aservice as a parameter. Because aservice and bservice are not instantiated in advance and depend on each other’s instances as parameters during the instantiation process, which constitutes a dead loop, so they can’t be instantiated in the end.

How does spring solve circular dependency?

You just need to adjust the above example a little, instead of constructor injection, use Autowired injection directly.

@Service
public class AService {

    @Autowired
    private BService bService;

    public AService() {
    }

    public void doA() {
        System.out.println("call doA");
    }
}
@Service
public class BService {

    @Autowired
    private AService aService;

    public BService() {
    }

    public void doB() {
        System.out.println("call doB");
    }
}

We can see that it can start normally, indicating that the circular dependency has been solved by itself

Why does spring use level 3 caching to solve circular dependency?

Why can spring rely on cycles?

call applicationContext.getBean (XX) method, which will eventually be called to dogetbean method of abstractbeanfactory class. Because this method is very long, I omitted some irrelevant code.

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,

We can see that as soon as the method comes in, it will call the getsingleton method to get the instance from the cache. If it can’t get it. It will judge whether the scope is: single instance, multi column or none. Different scopes have different rules for creating instances. Next, let’s focus on the getsingleton method.

public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);
  }
protected 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;
  }

We find that there are three map sets:

/** Cache of singleton objects: bean name --> bean instance */
  private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

  /** Cache of singleton factories: bean name --> ObjectFactory */
  private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

  /** Cache of early singleton objects: bean name --> bean instance */
  private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

Singletonobjects corresponds to level 1 cache, earlysingletonobjects corresponds to level 2 cache, and singleton factors corresponds to level 3 cache.

The logic of the getsingleton method above is as follows:

  1. First get the instance from singletonobjects (first level cache). If it can be obtained, the singletonobject instance will be returned directly.
  2. If the wrong instance is obtained from singletonobjects (first level cache), and then from earlysingletonobjects (second level cache), if it can be obtained, the singletonobject instance will be returned directly.
  3. If the wrong instance is obtained from earlysingletonobjects (Level 2 cache), the singletonfactory is obtained from singletonfactories (Level 3 cache). If it is obtained, the GetObject method is called to create the instance, and the created instance is put into the earlysingsingletonobjects (Level 2 cache), and the singletonfactory instance is deleted from singletonfactories (Level 3 cache) The singletonobject instance is returned.
  4. If the instance cannot be obtained from singletonobjects, earlysingletonobjects, and singletonfactories, the singletonobject object is empty.

To get an instance, you need to call applicationContext.getBean (“XXX”) method. The getBean method is called for the first time. When the code goes to the getsingleton method, the singletonobject object returned is empty. Then proceed to the next step. By default, the scope of the bean is singleton. Next, we will focus on this Code:

The createbean method calls the docreatebean method, which is also relatively long. We omit irrelevant code.

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)

The main process of this method is as follows:

  1. Create bean instance
  2. Determine whether the scope is a singleton, allow circular dependency, and the current bean is being created, not yet created. If all the conditions are met, addsingletonfactory is called to put the bean instance into the cache.
  3. Call the populatebean method for dependency injection
  4. Call the initializebean method to complete the object initialization and AOP enhancement

We can focus on the addsingletonfactory method first.

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
      if (!this.singletonObjects.containsKey(beanName)) {
        this.singletonFactories.put(beanName, singletonFactory);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
      }
    }
  }

The logic of this method is to determine that if no instance is found in singletonobjects, the singleton factory instance will be put into singleton factories and the instance in earlysingletonobjects will be removed.

After the createbean method is executed, the outer getsingleton method is called

Why does spring use level 3 caching to solve circular dependency?

Let’s focus on the getsingleton method

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized (this.singletonObjects) {
      Object singletonObject = this.singletonObjects.get(beanName);
      if (singletonObject == null) {
        if (this.singletonsCurrentlyInDestruction) {
          throw new BeanCreationNotAllowedException(beanName,
              "Singleton bean creation not allowed while singletons of this factory are in destruction " +
              "(Do not request a bean from a BeanFactory in a destroy method implementation!)");
        }
        beforeSingletonCreation(beanName);
        boolean newSingleton = false;
        boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
        if (recordSuppressedExceptions) {
          this.suppressedExceptions = new LinkedHashSet<>();
        }
        try {
          singletonObject = singletonFactory.getObject();
          newSingleton = true;
        }
        catch (IllegalStateException ex) {

          singletonObject = this.singletonObjects.get(beanName);
          if (singletonObject == null) {
            throw ex;
          }
        }
        catch (BeanCreationException ex) {
          if (recordSuppressedExceptions) {
            for (Exception suppressedException : this.suppressedExceptions) {
              ex.addRelatedCause(suppressedException);
            }
          }
          throw ex;
        }
        finally {
          if (recordSuppressedExceptions) {
            this.suppressedExceptions = null;
          }
          afterSingletonCreation(beanName);
        }
        if (newSingleton) {
          addSingleton(beanName, singletonObject);
        }
      }
      return singletonObject;
    }
  }

The logic of this method is very simple, that is, first get the instance from singletonobjects (first level cache), if not, call singletonFactory.getObject () method creates an instance and then calls the addSingleton method into the singletonObjects cache.

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
      this.singletonObjects.put(beanName, singletonObject);
      this.singletonFactories.remove(beanName);
      this.earlySingletonObjects.remove(beanName);
      this.registeredSingletons.add(beanName);
    }
  }

This method will put the instance into singletonobjects (first level cache) and delete singletonfactors (second level cache), so that when getBean is called again, the instance can be obtained from singletonobjects (Level 1 cache).

With all that said, let’s go back to the scenario in the example.

Why does spring use level 3 caching to solve circular dependency?

Why does spring use L3 cache instead of L2 cache?

In this case, as in the example, there is no problem with only level 2 caching.

But if there is such a case: instance a depends on instance B and instance C, instance B depends on instance a, and instance C depends on instance a.

When instantiating a, first expose objectfactorya to level 3 cache, and call getBean (b) dependency injection B instance. After instantiation of B, object factoryb is exposed to the third level cache in advance, and getBean (a) dependency injection is called to inject instance A. since objectfactorya is exposed in advance, instance a can be obtained from level 3 cache. Instance B completes dependency injection and is upgraded to level 1 cache. A instantiation and getBean (c) dependency injection C instance. After C instantiation, objectfactoryc is exposed to the third level cache in advance, and getBean (a) dependency injection a instance is called. Since objectfactorya is exposed in advance, an instance a can be obtained from the three-level cache. Note that we need to get an instance of a from the level 3 cache. We know that the instance in the level 3 cache is accessed by calling the singletonFactory.getObject () method. The returned result may be different each time. If the secondary cache is not used, there will be a problem here. The a instance obtained twice is not the same.

Summary:

The circular dependency problem can only be solved in the case of singleton, and allowcircularreferences should be set to true.

Circular dependency still occurs when:

  1. Constructor Injection
  2. If the scope is not a singleton, of course, in the custom scope, you can implement the logic to avoid circular dependency
  3. The allowcircularreferences parameter is set to false

If you like this article, please pay attention to it: Su San talks about technology

Why does spring use level 3 caching to solve circular dependency?