avatarGabriel Shanahan

Summary

The article discusses a Spring Boot application's circular dependency issue between ContractService and SomeApiContractMapper, and how the author resolved it through refactoring and the strategic use of field injection and class naming.

Abstract

The author encountered a circular dependency problem in a Spring Boot application where ContractService and SomeApiContractMapper required each other for initialization. Despite attempting to resolve the issue by switching to field injection, the error persisted. The author discovered that the initialization order was determined by the class loader, which loaded classes in a non-deterministic manner, leading to inconsistent bean initialization. By renaming SomeApiContractMapper to AbcApiContractMapper, the author managed to influence the class loading order, thus avoiding the circular dependency error. However, the author emphasizes that the true solution lies in refactoring the code to eliminate the circular dependency, which is indicative of a design flaw. The article also touches on the use of @DependsOn and @Lazy annotations as temporary workarounds, but ultimately advocates for a cleaner design to address the root cause.

Opinions

  • The author suggests that Spring's handling of component initialization is less intelligent than expected, as it does not perform a deterministic analysis to prevent circular dependencies.
  • The author expresses dissatisfaction with the non-deterministic nature of bean initialization in Spring, which is influenced by the order in which classes are loaded by the class loader.
  • The author considers the use of @DependsOn and @Lazy annotations as suboptimal solutions to the circular dependency problem, highlighting their limitations and potential drawbacks.
  • The author strongly recommends refactoring as the best approach to resolving circular dependencies, viewing them as symptoms of deeper design issues in the codebase.
  • The author believes that proper organization of logic related to the same problem in the same place can significantly improve code clarity and maintainability.

A Spring Boot field injection gotcha

A deep dive into circular dependencies, the way Spring Boot registers components, and how the name of a class can be the difference between functional and non-functional code

A couple of days ago I ran into a fairly standard problem with cyclic dependencies between two beans. A ContractService was responsible for sending a Contract to a 3rd party API called SomeAPI. This involved mapping it to the model expected by the API, which was done by SomeApiContractMapper, a different component that was injected into ContractService. Nothing fancy.

In this particular scenario, the mapping involved calculating a certain value from the contents of the Contract — a calculation which was exposed as a method on ContractService. This meant that ContractService needed to inject SomeApiContractMapper, and SomeApiContractMapper needed to inject ContractService.

@Service
public class ContractService {
    private final SomeApiContractMapper someApiContractMapper;

    public ContractService(
        SomeApiContractMapper someApiContractMapper
    ) {
        this.someApiContractMapper = someApiContractMapper;
    }
    
    // Stuff
}

// ...

@Component
public class SomeApiContractMapper {
    private final ContractService contractService;

    public SomeApiContractMapper(
        ContractService contractService
    ) {
        this.contractService = contractService;
    }

    // Stuff
}

The actual code was written in Kotlin, however I’m translating to Java to target a wider audience. Constructor injection is the norm in Kotlin, and is preferred in Java as well.

As per the documentation of de>AutowiredAnnotationBeanPostProcess, if a class only declares a single constructor, it will always be used, even if not annotated with @Autowired

In reality, the situation was a little more involved than what I’m describing here, so I didn’t realize I had introduced a dependency cycle until after I tried to build the app.

For reasons that are not relevant to this discussions, refactoring the application to remove the cycle (which is what you should always try to do) wasn’t something I wanted to get into. “Not to worry”, I thought to myself, “this is precisely what field injection was meant to solve” and changed the definition of SomeApiContractMapper accordingly:

@Component
public class SomeApiContractMapper {
    @Autowired
    private ContractService contractService;

    // Stuff
}

To my astonishment, this did not solve the error.

This really bugged me, because it made no sense. Obviously, Spring should be initializing SomeApiContractMapper first, since it doesn’t use constructor injection, and initialize ContractService next, which should work fine. So what was the problem?

On a hunch, I did something crazy — I changed the name of SomeApiContractMapper to AbcApiContractMapper. And lo and behold, the app started up.

I wasn’t happy. Not one bit.

It turns out that Spring isn’t as smart as one might like to think. One might assume that, once candidate components are loaded, some sort of analyzer is run to determine which components to initialize first, to avoid these types of problems. At the very least, one would hope that the list is deterministically sorted before initialization proceeds.

Unfortunately, this is not the case. As of Spring 5.3.22, components are loaded by instances of ClassPathBeanDefinitionScanner, specifically in the method scanCandidateComponents(String basePackage) of its parent, ClassPathScanningCandidateComponentProvider. There, a list of resources is loaded by delegating to an implementation of ResourcePatternResolver. All instances of ApplicationContext implement this interface, and by default when using Spring Boot, an instance of AnnotationConfigApplicationContext is used, which in turn delegates to an instance of PathMatchingResourcePatternResolver. Eventually, the method doFindAllClassPathResources(String path) is called, which uses a class loader to load the classes.

As it turns out, the output from this process is then used verbatim, and no further sorting or analysis is performed. In other words, the order in which beans are initialized is determined (in part) by the order in which the corresponding classes are loaded by the class loader. This order is almost certainly OS dependent, since it must, at some point, delegate to a “load files in directory X” system call, and file ordering is OS dependent. Unless you’re using a custom class loader and dealing with this explicitly, this means that, fundamentally, the order in which beans are initialized is non-deterministic.

This is the root cause of the observed behavior — the class loader implementation in my environment loads classes in alphabetical order. When the class having the field-level injection is named SomeApiContractMapper, it is loaded after ContractService. This means that ContractService gets initialized first, a candidate constructor for autowiring is detected, which in turn causes an attempt to initializeSomeApiContractMapper, which finds out it needs an instance of ContractService and fails. It does not matter that ContractService is injected via field injection — while field injection does happens after constructor injection (see doCreateBean, which first calls createBeanInstance, which is where constructor injection happens, and then deals with additional autowiring) it still happens in the same phase of the configuration.

However, when the class is named AbcApiContractMapper, no constructor autowiring is performed, and the bean gets loaded (and, therefore, initialized) before ContractService, and this problem never happens — once we get to initializing ContractService, AbcApiContractMapper is already initialized, cached and ready to be injected. We’re assuming the spring.main.allow-circular-references property is set to true — it is false by default starting in Spring Boot 2.6, which caused builds to suddenly break after upgrading (ours included).

All this is confirmed by turning on trace logging, and looking at the difference between the runs of ClassPathBeanDefinitionScanner for different choices of the mapper name.

Possible solutions

For pedagogical reasons, I will briefly mention that there are ways to circumvent this.

One possibility is using the @DependsOn annotation, which solves the problem:

@Component
@DependsOn("someApiContractMapper")
public class ContractService {
    // ...
}

This still requires the allow-circular-references property to be set to true, and is a terrible idea for many other reasons, such as hardwiring class names.

Another possibility is to mark the autowired field as @Lazy:

@Component
public class SomeApiContractMapper {

    @Autowired
    @Lazy
    private ContractService contractService;

    // ...
}

This does not require the allow-circular-references property to be set, and, if there really isn’t any way you can refactor your code to remove dependency cycles, is probably what I would recommend.

However, whenever possible, refactor your code. If you’ve got a circular dependency, it almost always means you haven’t designed your code right — it’s a symptom of a deeper problem, a design problem, which will not get solved by throwing annotations at it and is almost certain to cause more and more problems down the road. Refactoring it will only get more costly as time progresses, and will never again be as easy as it is now.

In my specific instance, the correct solution was to realize that the logic that deals with propagating contracts to a third-party API deserved a special service of its own, because, from a business perspective, there was actually a lot more that fit nicely into this service — for example, marking a Contract as signed or checking it’s state in the remote systems, and other use-cases. Before we refactored, these implementations were sort of randomly strewn across several services. By removing this logic from ContractService and into SomeApiContractService, we were free to inject ContractService in the mapper, and also increased clarity of the codebase as a whole. The deeper problem was that we weren’t concentrating logic related to the same problem in the same place.

Have you heard about the Kotlin Primer? It’s an opinionated guide to the Kotlin language, and will transform anyone who knows Java into a Kotlin expert. Check it out!

Java
Kotlin
Spring
Spring Boot
Dependency Injection
Recommended from ReadMedium
avatarMohammed Taoufik Lahmidi
Sending Headers in Feign Client

2 min read