Interview question: how to solve the circular dependency problem of Spring

Posted by maskme on Tue, 07 Dec 2021 17:42:36 +0100

Circular dependency in Spring

What is circular dependency

What is circular dependency? It can be divided into two parts: loop and dependency. Loop refers to the loop in the computer field, and the execution process forms a closed loop; Dependency is the prerequisite for completing this action, which is generally consistent with the meaning of dependency we usually say. In Spring, there are direct or indirect dependencies between one or more Bean instances, forming a circular call. Circular dependency can be divided into direct circular dependency and indirect circular dependency. The simple dependency scenario of direct circular dependency: Bean a depends on Bean B, and then Bean B depends on Bean a in turn (Bean a - > Bean B - > Bean a), A dependency scenario of indirect circular dependency: Bean a is dependent on Bean B, Bean B is dependent on Bean C, and Bean C is dependent on Bean A. there is an additional layer in the middle, but finally a loop is formed (Bean a - > Bean B - > Bean C - > Bean a).

Type of circular dependency

The first is self dependence, which forms circular dependence. Generally, this circular dependence does not occur because it is easy for us to find.

The second is direct dependency, which occurs between two objects. For example, Bean A depends on Bean B, and then Bean B in turn depends on Bean A. if you are careful, it is not difficult to find it with the naked eye.

The third is indirect dependency. This dependency type occurs in the scenario of three or more object dependencies. The simplest scenario of indirect dependency is: Bean A depends on Bean B, Bean B depends on Bean C, and Bean C depends on Bean A. It can be imagined that when there are many intermediate dependent objects, it is difficult to find this circular dependency. Generally, some tools are used to check it.

Spring's support for several cyclic dependency scenarios

Before introducing Spring's handling of several circular dependency scenarios, let's take a look at the scenarios of circular dependency in Spring. Most common scenarios are summarized as follows:

As a saying goes, there is no secret under the source code. Let's explore whether Spring supports these scenarios and the reasons for support or not through the source code. Without more words, let's get to the point.

Scenario ① - setter injection of a single Bean

This is also one of the most commonly used methods. It is assumed that there are two services: OrderService (order related business logic) and TradeService (transaction related business logic). The code is as follows:

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
public class OrderService {

  @Autowired
  private TradeService tradeService;

  public void testCreateOrder() {
    // omit business logic ...
  }

}

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
public class TradeService {

  @Autowired
  private OrderService orderService;

  public void testCreateTrade() { 
    // omit business logic ...
   }

}

In this circular dependency scenario, the program can run normally. From the code point of view, there is indeed circular dependency, that is to say, Spring supports this circular dependency scenario. The reason why we don't notice circular dependency here is that Spring has solved it silently.

Assuming that no processing is done and executed according to the normal creation logic, the process is as follows: the container first creates an OrderService and finds that it depends on TradeService, then creates an OrderService, and then finds that it depends on TradeService... An infinite loop occurs. Finally, a stack overflow error occurs and the program stops. In order to support this common circular dependency scenario, Spring divides the creation of objects into the following steps:

  1. Instantiate a new object (in the heap), but the object property is not assigned at this time
  2. Assign a value to an object
  3. Call the methods of some implementation classes of BeanPostProcessor. At this stage, the Bean has created and assigned properties. At this time, all classes in the container that implement the BeanPostProcessor interface will be called (e.g. AOP)
  4. Initialization (if InitializingBean is implemented, the method of this class will be called to complete the initialization of the class)
  5. Returns the created instance

Therefore, Spring introduces a three-level cache to deal with this problem (the three-level cache is defined in org.springframework.beans.factory.support.DefaultSingletonBeanRegistry). The first level cache singletonObjects is used to store fully initialized beans, and the beans taken from the cache can be used directly, The second level cache earlySingletonObjects is used to store the cache of singleton objects exposed in advance, the original Bean object (the attribute has not been assigned) to solve the circular dependency, and the third level cache singletonFactories is used to store the cache of singleton object factory and Bean factory object to solve the circular dependency. The processing flow of the above example using L3 cache is as follows:

If you have seen the definition source code of level 3 cache, you may also have such a question: why should the definition of level 3 cache be defined as map < string, objectfactory < >, Can't I cache objects directly? The object instance cannot be saved directly here, because it cannot be enhanced. See org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean method for details. The source code is as follows:

The second scenario -- setter injection of multiple beans

This method is usually used relatively rarely, or the two services mentioned above are used as examples. The only difference is that they are declared as multiple examples. The example code is as follows:

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class OrderService {

  @Autowired
  private TradeService tradeService;

  public void testCreateOrder() {
    // omit business logic ...
  }

}

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class TradeService {

  @Autowired
  private OrderService orderService;

  public void testCreateTrade() { 
    // omit business logic ...
   }

}

If you run the above code in Spring, you can start normally and successfully. The reason is that during pre instantiation processing of preInstantiateSingletons() method of org.springframework.beans.factory.support.DefaultListableBeanFactory, multiple types of beans are filtered out. The code of the method is as follows:

However, if other singleton type beans depend on these multi instance type beans at this time, the following circular dependency error will be reported.

Scenario 3 - setter injection of proxy object

This kind of scenario is often encountered. Sometimes, in order to realize asynchronous call, it will be added to the method of XXXXService class @Async Annotation to make the method call asynchronously to the outside (if you want to add the enabling annotation @ EnableAsync on the enabling class), the example code is as follows:

/**
 * @author mghio
 * @since 2021-07-17
 */
@EnableAsync
@SpringBootApplication
public class BlogMghioCodeApplication {

  public static void main(String[] args) {
    SpringApplication.run(BlogMghioCodeApplication.class, args);
  }

}

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class OrderService {

  @Autowired
  private TradeService tradeService;

  @Async
  public void testCreateOrder() {
    // omit business logic ...
  }

}

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class TradeService {

  @Autowired
  private OrderService orderService;

  public void testCreateTrade() { 
    // omit business logic ...
   }

}

Marked in @Async In the annotation scenario, after adding the enable asynchronous annotation (@ EnableAsync), the proxy object will be automatically generated through AOP. The above code will throw a BeanCurrentlyInCreationException. The general process of operation is shown in the figure below:

The source code is in the method doCreateBean of org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory class. It will judge whether the object in the second level cache earlySingletonObjects is equal to the original object. The source code of the method judgment part is as follows:

The object stored in the L2 cache is the proxy object generated by AOP, which is not equal to the original object, so a circular dependency error is thrown. If you take a closer look at the source code, you will find that if the L2 cache is empty, it will be returned directly (because there are no objects to compare, and it is impossible to verify at all), and the circular dependency error will not be reported. By default, Spring searches recursively according to the full path of the file, sorts according to the path + file name, and loads it first, so we just need to adjust the names of the two classes, Let the method be marked @Async The annotated classes can be sorted later.

Scenario ④ - constructor injection

There are few scenarios for constructor injection. So far, I have not encountered using constructor injection in the company projects and open source projects I have contacted. Although it is not used much, I need to know why Spring does not support circular dependency in this scenario. The example code of constructor injection is as follows:

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
public class OrderService {

  private TradeService tradeService;

  public OrderService(TradeService tradeService) {
    this.tradeService = tradeService;
  }

  public void testCreateOrder() {
    // omit business logic ...
  }

}

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
public class TradeService {

  private OrderService orderService;

  public TradeService(OrderService orderService) {
    this.orderService = orderService;
  }

  public void testCreateTrade() {
    // omit business logic ...
  }

}

Constructor injection cannot be added to the level 3 cache. The level 3 cache in the Spring framework is useless in this scenario, so only exceptions can be thrown. The overall process is as follows (the dotted line indicates that it cannot be executed, and the next step is also drawn for visualization):

Scenario ⑤ - DependsOn circular dependency

This kind of DependsOn circular dependency scenario is rarely used in general. Just learn about the problems that will lead to circular dependency. The @ DependsOn annotation is mainly used to specify the instantiation order. The example code is as follows:

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
@DependsOn("tradeService")
public class OrderService {

  @Autowired
  private TradeService tradeService;

  public void testCreateOrder() {
    // omit business logic ...
  }

}

/**
 * @author mghio
 * @since 2021-07-17
 */
@Service
@DependsOn("orderService")
public class TradeService {

  @Autowired
  private OrderService orderService;

  public void testCreateTrade() {
    // omit business logic ...
  }

}

From the above, we know that if the class here is not annotated with @ dependsOn annotation, it can run normally, because Spring supports singleton setter injection, but a circular dependency error will be reported after adding the @ dependsOn annotation of the sample code, because it is in the method doGetBean() of the class org.springframework.beans.factory.support.AbstractBeanFactory Check whether the instance of dependsOn has a circular dependency. If there is a circular dependency, throw a circular dependency exception. The method judgment code is as follows:

summary

This paper mainly introduces what circular dependency is and Spring's handling of various circular dependency scenarios. Only some of the source codes involved are listed, and their positions are marked in the source code. Interested friends can see the complete source code. Finally, Spring's support for various circular dependency scenarios is shown in the figure below (P.S. Spring version: 5.1.9.RELEASE):

Topics: Spring Interview prototype