1. Introduction

In the past two days, I encountered a very interesting Spring cycle dependency problem, but this is not quite the same as the previous cycle dependency problem, hidden quite hidden, the network rarely see other people encounter similar problems. I’ll just call it an atypical Spring cycle dependency problem. But I’m sure I’m not the first to step in this hole, and I’m sure I’m not the last, probably just because there are so few people who have done it and there are so few records. So here just record this pit, convenient posterity to view.

As Lu Xun (I) once said, “There is no pit in this world. When people step on more people, they become pits.”

2. Typical scenario

I’ve heard a lot of comments from people reviewing other people’s code: “How can you design with cyclic dependencies between these classes? You’ll make a mistake!” .

In theory, cyclic dependencies should be layered, extracted from the common part, and then dependent on the common part by each functional class.

However, in complex code, manager classes call each other too much, and there is always an unintended problem of cyclic dependencies between classes. However, sometimes we find that when we use Spring for dependency injection, although there are cyclic dependencies between beans, the code itself is most likely to work normally, and there seems to be no bugs.

Many sensitive students must be wondering, how can cyclic dependence violate the law of causality? Yes, none of this is taken for granted.

3. What is dependency

In fact, it is not accurate, or at least not detailed, to say that A depends on B in any context. We can simply define what a dependency is.

A depends on B, which can be understood as the realization of some functions in A needs to call other functions in B. This can also be broken down into two meanings:

  1. A is strongly dependent on B. Creating an instance of A itself requires B to participate. Contrast in real life like
    My mother gave birth to youThe same.
  2. A is weakly dependent on B. Creating an instance of A does not require B’s participation, but A implements the function by calling B’s methods. Contrast in real life like
    Male plow female weaveThe same.

So cyclic dependency actually has two meanings:

  1. Circular dependencies between strong dependencies.
  2. Cyclic dependencies between weak dependencies.

At this point, I think you know what I’m talking about.

4. What is dependency mediation

For strong dependencies, A and B cannot be mutually dependent, or the universe would explode. Such dependence is therefore currently irreconcilable.

For weak dependence, the existence of A and B does not have A prerequisite relationship, A and B just cooperate with each other. So normally you don’t have causality violations.

So what is cyclic dependency mediation? My understanding is:

The process of mistaking weak dependencies for strong ones and changing them back to weak ones.

Based on the above analysis, we basically know how Spring performs cyclic dependency mediation (only weak dependencies, strong cyclic dependencies can only be automatically mediated by God).

5. Why dependency injection

Most people just implement the IOC container as a “map of storing beans” and the DI implementation as “assign beans to fields in a class by annotation + reflection”. In fact, many people overlook DI’s dependency mediation function. Helping us with dependency mediation is itself an important reason to use IOC+DI.

In the days before dependency injection, many people passed dependencies between classes through constructors (effectively creating strong dependencies). As projects get bigger and bigger, it’s easy to get irreconcilable circular dependencies. Developers are forced to reabstract, which can be troublesome. In fact, we made weak dependencies strong by coupling class construction, class configuration, and class initialization logic into the constructor.

DI helps us decouple the function of the constructor.

So how does Spring decouple?

6. Spring’s dependency injection model

There is a lot of information about this part on the Internet. My understanding is about the three steps mentioned above:

  1. The structure of the class, calls constructors, resolves strong dependencies (typically no-parameter constructs), and creates class instances.
  2. Configuration of a classAccording to the dependency injection annotations in the Field/GetterSetter, resolve weak dependencies, and populate any classes that need to be injected.
  3. Class initialization logicCall the initialization method in the lifecycle (for example
    @PostConstructComments or
    InitializingBeanthe
    afterPropertiesSetMethod) to perform the actual initialization business logic.

Thus, the function of the constructor is reduced from three to one, which is responsible for the construction of the class. It leaves the configuration of the class to DI and the initialization logic of the class to the lifecycle.

Thinking of this layer, I suddenly solved the problem I had been blocking in my mind for a long time. At the beginning of learning Spring, I could not figure out:

  • Why does Spring have an extra initialization method in the Bean life cycle in addition to the constructor?
  • What’s the difference between this initializer and the constructor?
  • Why does Spring recommend writing initialization logic in initialization methods in the lifecycle?

Now, taken together, the explanation is clear:

  1. For dependency mediation, Spring does not inject dependencies when it calls the constructor. That is, beans injected through DI cannot be used in constructors (perhaps, but Spring does not guarantee this).
  2. If you don’t use the injection-injected bean in the constructor and just use the arguments in the constructor, that’s fine, but it causes the bean to be heavily dependent on its input bean. Mediation cannot be performed when subsequent cyclic dependencies occur.

7. Atypical problems

Conclusion?

Based on the above analysis, we should reach the following consensus:

  • The way to pass dependencies through constructors is
    There may beCausing cyclic dependencies that cannot be automatically mediated.
  • Circular dependencies created purely by dependency injection through Field/ gettersetters are perfectly capable of being mediated automatically.

So I’ve come to a conclusion that I think is correct. It worked, until I discovered the scene:

When you do dependency injection on beans in Spring, you will never have unmediated cyclic dependencies as long as you do not use constructor injection in the case of purely circular dependencies.

Of course, I don’t mean to say that constructor injection is not recommended. Instead, I think it is more demanding and elegant to be able to “gracefully use constructor injection without introducing loop dependencies.” Implementing this approach requires a higher level of abstraction and naturally decouples functionality.

The problem

A simplified version of the actual problem might look something like this (the following classes are in the same package) :

@SpringBootApplication @Import({ServiceA.class, ConfigurationA.class, BeanB.class}) public class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); }}Copy the code
public class ServiceA {
    @Autowired
    private BeanA beanA;

    @Autowired
    private BeanB beanB;
}Copy the code
public class ConfigurationA {
    @Autowired
    public BeanB beanB;

    @Bean
    public BeanA beanA() {
        returnnew BeanA(); }}Copy the code
public class BeanA {
}Copy the code
public class BeanB {
    @Autowired
    public BeanA beanA;
}Copy the code

First of all, I’m not using @Component, @Configuration, etc. Instead, I’m using @import to manually scan beans to make it easier to specify the initialization order of beans. Spring loads the beans in the order in which I @import them. Also, as each Bean is loaded, if the Bean has dependencies that need to be injected, it tries to load the Bean that it depends on.

Briefly, the chain of dependencies looks something like this:

We can find that BeanA BeanB, there is a circular dependencies between ConfigurationA, but don’t panic, all rely on is implemented by the constructor injection, theory seems to automatically mediation.

But in fact, this code will report the following error:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'beanA': Requested bean is currently in creation: Is there an unresolvable circular reference?Copy the code

This is clearly a cyclic dependency that Spring cannot mediate.

This is already a little weird. However, if you try switching BeanA and BeanB declared in the ServiceA class, you’ll notice that the code suddenly works!!

Obviously, swapping the order of the two Bean dependencies essentially tweaks the order in which Spring loads the beans (as we all know, Spring creates beans single-threaded).

explain

I’m sure you’ve spotted the problem. Yes, the problem lies in the ConfigurationA class.

Configuration classes differ from regular beans in that in addition to being managed as beans, configuration classes can also declare other beans internally.

The problem is that the construction of other beans declared in the configuration class is actually part of the business logic of the configuration class. That is, we can only create our own declared beans after we have satisfied all of the config class’s dependencies. (Without this restriction, there is a risk of null Pointers if you use your own dependencies when creating other beans that you declare.)

Therefore, BeanA is no longer weakly dependent on ConfigurationA, but rather strongly dependent on BeanA instance construction.

With this knowledge in mind, let’s examine the two initialization paths separately.

First load BeanA

  1. When Spring tries to load ServiceA, it constructs ServiceA first, and then discovers that it depends on BeanA, so it tries to load BeanA.
  2. Spring wants to construct BeanA, but finds that BeanA is inside ConfigurationA and tries to load ConfigurationA (BeanA is not yet constructed).
  3. Spring constructs an instance of ConfigurationA, then realizes that it depends on BeanB and tries to load BeanB.
  4. Spring constructs an instance of BeanB and then discovers that it depends on BeanA, so it tries to load BeanA.
  5. Spring finds that BeanA has not been instantiated yet, at which point Spring finds itself back in step 2… GG…

To load the BeanB

  1. When Spring tries to load ServiceA, it constructs ServiceA first and then realizes that it depends on BeanB, so it tries to load BeanB.
  2. Spring constructs an instance of BeanB and then discovers that it depends on BeanA, so it tries to load BeanA.
  3. Spring finds that BeanA is inside ConfigurationA and tries to load ConfigurationA (BeanA is still unconstructed at this point);
  4. Spring constructs an instance of ConfigurationA, finds that it depends on BeanB, and that an instance of BeanB already exists, and populates that dependency into ConfigurationA.
  5. Spring finds that ConfigurationA has been constructed and populated with dependencies, and remembers to construct BeanA.
  6. Spring finds that BeanA already has an instance and gives it to BeanB, which completes the dependency for BeanB’s population.
  7. Spring goes back to the process of populating the dependencies for ServiceA, finds that it also relies on BeanA, and populates BeanA to ServiceA.
  8. Spring completes initialization successfully.

conclusion

To sum up the problem, the conclusion is:

In addition to construction-injection leading to strong dependencies, a Bean can also be strongly dependent on exposing its configuration classes.

Code bad smell

I’m already feeling a little sick writing this. Who needs to analyze dependencies like this when they are writing code when they have nothing to do? So is there any way to avoid analyzing this disgusting problem?

There is a way to do this, which is to follow the following code specification ———— do not apply field-level dependency injection to Configuration classes annotated with @Configuration.

Yes, dependency injection of a configuration class is almost equivalent to adding a strong dependency to all beans in the configuration class, greatly increasing the risk of unmediated cyclic dependencies. We should keep dependencies as small as possible, and all dependencies should be directly dependent on the beans that we really need.


BLOG address: www.liangsonghua.com

Pay attention to wechat public number: songhua preserved egg bulletin board, get more exciting!

Introduction to our official account: We share our technical insights from working in JD, as well as JAVA technology and best practices in the industry, most of which are pragmatic, understandable and reproducible