System

OS: macOS 11.4 JDK: AdoptOpenJDK-16.0.1+9 Gradle: 7.1.1 Spring Boot: 2.5.2

Demo

demo(GitHub)

Run

gradle bootJar java -jar build/libs/demo-0.0.1-SNAPSHOT.jar

Error

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'webSecurityConfig': Unsatisfied dependency expressed through method 'setContentNegotationStrategy' parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration': Unsatisfied dependency expressed through method 'setConfigurers' parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'webConfig': Unsatisfied dependency expressed through field 'requestMappingHandlerAdapter'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'requestMappingHandlerAdapter' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Unsatisfied dependency expressed through method 'requestMappingHandlerAdapter' parameter 0; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'mvcContentNegotiationManager': Requested bean is currently in creation: Is there an unresolvable circular reference?

Comment From: wilkinsona

Thanks for the sample. I've reproduced the problem entirely on macOS. When run using java -jar I see the BeanCurrentlyInCreationException. When run with gradlew bootRun or using the main method in my IDE the app starts successfully. The root cause of the problem is a circular dependency. In one scenario, Spring Framework is able to break this cycle but in the other it is not.

The cycle is due to your WebConfig class. It is a WebMvcConfigurer which means that it is a dependency of DelegatingWebMvcConfiguration via its setConfigurers(List<WebMvcConfigurer> configurers) method. It also depends on the RequestMappingHandlerAdapter bean via its @Autowired field. This bean is defined by WebMvcConfigurationSupport, the super-class of DelegatingWebMvcConfiguration.

The difference in behaviour is due to the different order in which the application's beans are processed. When webConfig is processed first, Spring Framework is able to break this cycle. If you enable trace-level logging for org.springframework.beans, you can see this cycle being broken via eager caching and reuse of the partially initialized bean:

2021-07-20 09:24:40.740 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Creating instance of bean 'webConfig'
2021-07-20 09:24:40.741 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Eagerly caching bean 'webConfig' to allow for resolving potential circular references
2021-07-20 09:24:40.742 DEBUG 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'requestMappingHandlerAdapter'
2021-07-20 09:24:40.742 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Creating instance of bean 'requestMappingHandlerAdapter'
2021-07-20 09:24:40.742 DEBUG 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration'
2021-07-20 09:24:40.742 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Creating instance of bean 'org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration'
2021-07-20 09:24:40.744 DEBUG 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'spring.resources-org.springframework.boot.autoconfigure.web.ResourceProperties'
2021-07-20 09:24:40.744 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Creating instance of bean 'spring.resources-org.springframework.boot.autoconfigure.web.ResourceProperties'
2021-07-20 09:24:40.746 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Eagerly caching bean 'spring.resources-org.springframework.boot.autoconfigure.web.ResourceProperties' to allow for resolving potential circular references
2021-07-20 09:24:40.746 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Returning cached instance of singleton bean 'org.springframework.boot.context.properties.BoundConfigurationProperties'
2021-07-20 09:24:40.748 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Finished creating instance of bean 'spring.resources-org.springframework.boot.autoconfigure.web.ResourceProperties'
2021-07-20 09:24:40.748 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Returning cached instance of singleton bean 'spring.mvc-org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties'
2021-07-20 09:24:40.749 DEBUG 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'spring.web-org.springframework.boot.autoconfigure.web.WebProperties'
2021-07-20 09:24:40.749 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Creating instance of bean 'spring.web-org.springframework.boot.autoconfigure.web.WebProperties'
2021-07-20 09:24:40.749 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Eagerly caching bean 'spring.web-org.springframework.boot.autoconfigure.web.WebProperties' to allow for resolving potential circular references
2021-07-20 09:24:40.749 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Returning cached instance of singleton bean 'org.springframework.boot.context.properties.BoundConfigurationProperties'
2021-07-20 09:24:40.750 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Finished creating instance of bean 'spring.web-org.springframework.boot.autoconfigure.web.WebProperties'
2021-07-20 09:24:40.750 DEBUG 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration' via constructor to bean named 'spring.resources-org.springframework.boot.autoconfigure.web.ResourceProperties'
2021-07-20 09:24:40.750 DEBUG 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration' via constructor to bean named 'spring.mvc-org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties'
2021-07-20 09:24:40.750 DEBUG 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration' via constructor to bean named 'spring.web-org.springframework.boot.autoconfigure.web.WebProperties'
2021-07-20 09:24:40.750 DEBUG 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration' via constructor to bean named 'org.springframework.beans.factory.support.DefaultListableBeanFactory@43aaf813'
2021-07-20 09:24:40.754 TRACE 11173 --- [           main] o.s.beans.CachedIntrospectionResults     : Getting BeanInfo for class [org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration]
2021-07-20 09:24:40.758 TRACE 11173 --- [           main] o.s.beans.CachedIntrospectionResults     : Caching PropertyDescriptors for class [org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration]
2021-07-20 09:24:40.758 TRACE 11173 --- [           main] o.s.beans.CachedIntrospectionResults     : Found bean property 'applicationContext' of type [org.springframework.context.ApplicationContext]
2021-07-20 09:24:40.758 TRACE 11173 --- [           main] o.s.beans.CachedIntrospectionResults     : Found bean property 'class' of type [java.lang.Class]
2021-07-20 09:24:40.758 TRACE 11173 --- [           main] o.s.beans.CachedIntrospectionResults     : Found bean property 'configurers' of type [java.util.List]
2021-07-20 09:24:40.758 TRACE 11173 --- [           main] o.s.beans.CachedIntrospectionResults     : Found bean property 'resourceLoader' of type [org.springframework.core.io.ResourceLoader]
2021-07-20 09:24:40.758 TRACE 11173 --- [           main] o.s.beans.CachedIntrospectionResults     : Found bean property 'servletContext' of type [javax.servlet.ServletContext]
2021-07-20 09:24:40.761 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Eagerly caching bean 'org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration' to allow for resolving potential circular references
2021-07-20 09:24:40.762 TRACE 11173 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Returning eagerly cached instance of singleton bean 'webConfig' that is not fully initialized yet - a consequence of a circular reference

When webSecurityConfig is processed first, the cycle changes slightly and Spring Framework is unable to break it.

webSecurityConfig is a WebSecurityConfigurerAdapter which requires a ContentNegotiationStrategy bean via its setContentNegotationStrategy method. This dependency is satisfied by the mvcContentNegotiationManager bean defined by WebMvcConfigurationSupport. To create the mvcContentNegotiationManager bean, DelegatingWebMvcConfiguration (the sub-class of WebMvcConfigurationSupport) must be initialized first. This requires the creation of all WebMvcConfigurer beans to satisfy its setConfigurers(List<WebMvcConfigurer> configurers) method. We're now at the same point as we were in the cycle described above with WebConfig being created and this requiring DelegatingWebMvcConfiguration to have been initialized so that RequestMappingHandlerAdapter can be injected into WebConfig. Unlike the previous cycle, DelegatingWebMvcConfiguration can't be eagerly cached as it hasn't been created yet so the cycle cannot be broken.

You can avoid the problem by ensuring that you don't inject any Spring MVC infrastructure (such as RequestMappingHandlerAdapter) into a WebMvcConfigurer as they are involved in configuring Spring MVC infrastructure.

With that said, I'd like to understand why the classpath ordering isn't consistent. Ideally, the class files should be written into the jar file in the exact same order as they appear on the classpath when using bootRun. If this ordering was consistent, the behaviour at runtime would also be consistent.

Comment From: wilkinsona

When Spring Framework is resolving resources from jar files, it honours the ordering of the entries in the jar file and, by default, Gradle writes the files into the jar file in the order that they're provided to it by the file system. I believe this explains the different behaviour on macOS and Windows – the file systems on the two OSs are presenting the files to Gradle in two different orders so Gradle writes them into the jar files in two different orders. As a result, they're processed by Spring Framework in two different orders.

The difference between bootRun and java -jar on macOS is due to Spring Framework behaving differently when resolving resources from the file system, as it is in the bootRun case. In this situation, its PathMatchingResourcePatternResolver sorts the files in each directory by their names:

protected File[] listDirectory(File dir) {
    File[] files = dir.listFiles();
    if (files == null) {
        if (logger.isInfoEnabled()) {
            logger.info("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
        }
        return new File[0];
    }
    Arrays.sort(files, Comparator.comparing(File::getName));
    return files;
}

In the example application, this causes WebConfig to be processed before WebSecurityConfigurer which is the reverse of the filesystem's default ordering of the two files.

You can make the behaviour more consistent by configuring Gradle to use a reproducible order for the files in the jar:

bootJar {
    reproducibleFileOrder = true
}

This sorts BOOT-INF/classes by path and WebConfig.class therefore appears in the jar before WebSecurityConfig.class. This isn't identical to Spring Framework's file system-based ordering but it's very close. It will also guarantee the same ordering of entries in jars built on different operating systems.

I think we should enable reproducibleFileOrder by default for bootJar but probably only in 2.6. There is a small chance that it will be a breaking change for those with circular dependencies that are relying on the current ordering for the cycle to be breakable.

I don't think this problem applies to Maven. Its jars are reproducible by default as of version 3.2.0 of the jar plugin.

Comment From: icuxika

@wilkinsona Thanks for your patience. I have tried to apply this configuration on my project and it works well now. Your reply will help me learn Spring Framework better.

Comment From: wilkinsona

@icuxika You're welcome. I've re-opened the issue as I'd like to use it to enable reproducible ordering by default.

Comment From: wilkinsona

I don't think this problem applies to Maven. Its jars are reproducible by default as of version 3.2.0 of the jar plugin.

I was mistaken here. You have to opt in with Maven by setting project.build.outputTimestamp.

When opted in with Maven, entries beneath BOOT-INF/classes are ordered but the order of jars beneath BOOT-INF/lib is unchanged. By contrast, if you're building a war project, the entries beneath WEB-INF/lib are ordered.

Given that this is opt-in with Maven I'm not so sure that we should enable reproducible ordering by default with Gradle. I also think we should probably change repackaging with Maven so that its ordering behaviour aligns with the war plugin.

Comment From: wilkinsona

We discussed this today and decided not to do anything by default. https://github.com/spring-projects/spring-boot/issues/27436 has been opened to align the behaviour of a repackaged fat jar with Maven's war plugin.