Spring’s cyclic dependencies
What are circular dependencies
What are circular dependencies? It can be divided into two parts: loop and dependency. Loop refers to the loop in the computer field, where the execution process forms a closed loop. Dependence is the prerequisite to complete this action, and we usually say that the meaning of dependence is roughly the same. In Spring, there are direct or indirect dependencies between one or more Bean instances, which constitute circular calls. Circular dependencies can be divided into direct circular dependencies and indirect circular dependencies. Simple dependency scenarios of direct circular dependencies are as follows: Bean A depends on Bean B, which in turn depends on Bean A (Bean A -> Bean B -> Bean A), A dependency scenario for indirect circular dependencies: Bean A depends on Bean B, Bean B depends on Bean C, and Bean C depends on Bean A, with an extra layer in the middle, but ultimately A loop (Bean A -> Bean B -> Bean C -> Bean A).
Type of loop dependency
The first type is self-dependence, where you rely on yourself to form a circular dependency, which usually doesn’t happen, because it’s easy for us to see.
The second type is direct dependency, which occurs between two objects. For example, Bean A depends on Bean B, which in turn depends on Bean A, which is not hard to spot if you are careful.
The third type of dependency is indirect dependency. This type of dependency occurs when three or more objects depend on each other. Indirect dependency is the simplest: Bean A depends on Bean B, Bean B depends on Bean C, and Bean C depends on Bean A. As you can imagine, when there are many intermediate dependencies, it is difficult to find such cyclic dependencies.
Spring supports several cyclic dependency scenarios
Before looking at Spring’s handling of several cyclic dependency scenarios, let’s take a look at what cyclic dependency scenarios are available in Spring. Most of the common scenarios are summarized as follows:
As the saying goes, there are no secrets under the source code, the following is the source code to explore whether Spring supports these scenarios, and why it supports or does not support the reason, without saying too much, let’s get to the point.
The first scenario — setter injection of singleton beans
This is one of the most common ways to use it. Suppose you have two services named OrderService (order-related business logic) and TradeService (transaction-related business logic) with the following code:
/** * @author mghio * @since 2021-07-17 */ @Service public class OrderService { @Autowired private TradeService tradeService; public void testCreateOrder() { // omit business logic ... }}Copy the code
/** * @author mghio * @since 2021-07-17 */ @Service public class TradeService { @Autowired private OrderService orderService; public void testCreateTrade() { // omit business logic ... }}Copy the code
Spring supports cyclic dependencies. The reason we don’t notice cyclic dependencies is that Spring has quietly solved them.
Assuming that no processing is done and the normal creation logic is followed, the process looks like this: The container creates OrderService, finds it dependent on TradeService, creates OrderService, finds it dependent on TradeService… , an infinite loop occurs, and finally a stack overflow error occurs, and the program stops. To support this common cyclic dependency scenario, Spring breaks object creation into the following steps:
- Instantiate a new object (in the heap), but the object properties have not been assigned at this point
- Assign a value to an object
- Call the methods of some of the implementation classes of BeanPostProcessor, at which point the Bean has been created and assigned properties. All classes in the container that implement the BeanPostProcessor interface will be called (e.g. AOP)
- InitializingBean (if InitializingBean is implemented, methods of this class are called to complete the initialization of the class)
- Returns the created instance
Therefore, Spring l3 cache is introduced to deal with this problem (l3 cache definition in the org. Springframework. Beans. Factory. Support. DefaultSingletonBeanRegistry), The first level cache, singletonObjects, is used to hold fully initialized beans. Beans taken from this cache can be used directly. The second level cache, earlySingletonObjects, is used to hold a cache of pre-exposed singletonObjects. The third level of the singletonFactories cache is used to hold a cache of singleton object factories that hold Bean factory objects that are used to resolve circular dependencies. The processing flow of the three-level cache in the above example is as follows:
If you look at the definition source of the third level cache, you may also have a question: why is the third level cache defined as Map<String, ObjectFactory<? >>, can’t you cache objects directly? You can’t save the object instance directly because you can’t enhance it. Details visible class org. Springframework. Beans. Factory. Support. AbstractAutowireCapableBeanFactory# doCreateBean methods part of the source code is as follows:
Scenario 2 — Setter injection of multi-instance beans
The only difference is that both services are now declared to be multiple. The code for this example 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 ... }}Copy the code
/** * @author mghio * @since 2021-07-17 */ @Service @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class TradeService { @Autowired private OrderService orderService; public void testCreateTrade() { // omit business logic ... }}Copy the code
If you run the above code in Spring, it should start normally. Reason is that the class org. Springframework. Beans. Factory. Support. The DefaultListableBeanFactory preInstantiateSingletons () method of the pre instantiation processing, Filter out the Bean with multiple example types, and the code part of the method is as follows:
However, if another singleton type of Bean depends on the multi-singleton type of Bean, the loop dependency error shown below will be reported.
Scenario 3 — Setter injection of proxy objects
This scenario is also frequently encountered. In order to implement asynchronous calls, the @async annotation is sometimes added to the method of the XXXXService class to make the method an asynchronous call to the outside (provided that the enabled annotation is added to the enabled class). 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); }}Copy the code
/** * @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 ... }}Copy the code
/** * @author mghio * @since 2021-07-17 */ @Service @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class TradeService { @Autowired private OrderService orderService; public void testCreateTrade() { // omit business logic ... }}Copy the code
In a scenario marked with the @Async annotation, the proxy object is automatically generated via AOP after the @enableAsync annotation is added. The above code to run throws BeanCurrentlyInCreationException anomalies. The general operation process is shown in the figure below:
The source code in the org. Springframework. Beans. Factory. Support. DoCreateBean method AbstractAutowireCapableBeanFactory class, Will determine whether the object in the second level cache earlySingletonObjects is equal to the original object, method determination part of the source code is as follows:
The level 2 cache holds proxy objects generated by AOP that are not equal to the original object, so a loop-dependency error is thrown. If you look at the source code, you will find that if the second level cache is empty, it will return directly (because there is no comparison object, there is no verification), so you will not report the loop-dependent error. By default, Spring does a recursive search based on the full path of the file, which is sorted by path + file name. So we just adjust the two class names so that the class whose methods are annotated with @async comes next.
The fourth scenario — constructor injection
Constructor injection scenarios are rare, and none of the corporate or open source projects I’ve worked with so far have used constructor injection. Although it is rare, it is important to know why Spring does not support this scenario’s cyclic dependency. Example constructor injection code 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 ... }}Copy the code
/** * @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 ... }}Copy the code
Constructor injection cannot be added to the third-level cache. The third-level cache in the Spring framework is useless in this scenario, so an exception is thrown, and the overall flow is as follows (dashed lines indicate that it cannot be executed, but the next step is drawn for intuition) :
The fifth scenario — DependsOn cycle
The @dependson annotation is used to specify the order in which an instance is instantiated. The following code is used:
/** * @author mghio * @since 2021-07-17 */ @Service @DependsOn("tradeService") public class OrderService { @Autowired private TradeService tradeService; public void testCreateOrder() { // omit business logic ... }}Copy the code
/** * @author mghio * @since 2021-07-17 */ @Service @DependsOn("orderService") public class TradeService { @Autowired private OrderService orderService; public void testCreateTrade() { // omit business logic ... }}Copy the code
Spring supports singleton setter injection, but the @dependson annotation of the sample code will result in a circular dependency error. Reason is that the class org. Springframework. Beans. Factory. Support. DoGetBean AbstractBeanFactory method () check whether there is the instance of dependsOn circular dependencies, If there is a loop dependency, throw a loop dependency exception, the method determines part of the code as follows:
conclusion
This article mainly introduces what circular dependencies are and Spring’s handling of various circular dependency scenarios. Only part of the source code involved is listed in the article, and the location of the source code is marked. Interested friends can go to see the full source code. Finally, Spring support for various loop dependency scenarios is shown below (P.S. Spring version: 5.1.9.release) :