This was originally reported as spring-projects/spring-session#995.
An application that starts up normally (either using java -jar, Gradle bootRun task or IDE) fails in @SpringBootTest with reported circular dependency:
Error starting ApplicationContext. To display the auto-configuration report re-run your application with 'debug' enabled.
2018-02-28 10:26:38.337 ERROR 27047 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
demoController defined in file [/home/vedran/dev/projects/misc/Spring-Session-Circular-Dependency-Issue/out/production/classes/com/example/demo/DemoController.class]
โ
demoRepository
โ
(inner bean)#64b0d1fa
โโโโโโโ
| entityManagerFactory
โ โ
| webSocketBrokerConfig (field private org.springframework.session.SessionRepository org.springframework.session.web.socket.config.annotation.AbstractSessionWebSocketMessageBrokerConfigurer.sessionRepository)
โ โ
| sessionRepository defined in class path resource [org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration$SpringBootJdbcHttpSessionConfiguration.class]
โ โ
| transactionManager defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]
โ โ
| webSocketRegistryListener
โโโโโโโ
The original report contains sample project that can be used to reproduce the issue. Sample uses Boot 1.5.10.RELEASE but the same behavior is observed with 2.0.0.RC2, by using Gradle 4.x and applying this diff:
diff --git a/build.gradle b/build.gradle
index 8bcba6c..06b4555 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,9 +1,10 @@
buildscript {
ext {
- springBootVersion = '1.5.10.RELEASE'
+ springBootVersion = '2.0.0.RC2'
}
repositories {
mavenCentral()
+ maven { url 'https://repo.spring.io/libs-milestone/' }
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
@@ -13,6 +14,7 @@ buildscript {
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
+apply plugin: 'io.spring.dependency-management'
group = 'com.example'
version = '0.0.1-SNAPSHOT'
@@ -20,6 +22,7 @@ sourceCompatibility = 1.8
repositories {
mavenCentral()
+ maven { url 'https://repo.spring.io/libs-milestone/' }
}
@@ -28,7 +31,7 @@ dependencies {
compile('org.springframework.boot:spring-boot-starter-jdbc')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-websocket')
- compile('org.springframework.session:spring-session')
+ compile('org.springframework.session:spring-session-jdbc')
compile('com.h2database:h2')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
When the EntityManagerFactory bean is created, DataSourceInitializedPublisher post-processes it and publishes the DataSourceInitializedEvent. The publication of this event causes SimpleApplicationEventMulticaster to retrieve all the ApplicationListeners. This results in an attempt being made to create webSocketRegistryListener.
Creation of webSocketRegistryListener triggers the creation of a number of other beans:
webSocketBrokerConfigsessionRepository (produced by JdbcHttpSessionConfiguration.sessionRepository(JdbcOperations, PlatformTransactionManager)transactionManager (a JpaTransactionManager produced by JpaBaseConfiguration.transactionManager())The creation of the JpaTransactionManager requires the injection of an entityManagerFactory. It's found and, for reasons I don't yet understand, is post-processed for a second time. This second round of post-processing (which is happening before the first round has completed) triggers a second attempt to create webSocketRegistryListener due to the post-processed performed by DataSourceInitializedPublisher that is described above. The second attempt to create webSocketRegistryListener fails because it's currently in creation.
It works in the main application case as the application context is an AnnotationConfigEmbeddedWebApplicationContext. The key difference with this context type is that it retrieves all ServletContextInitializer beans early in the application's lifecycle and, crucially, before the context's multicaster has been set. This retrieval triggers the creation of springSessionRepositoryFilter. It ultimately leads to the creation of the EntityManagerFactory bean (via sessionRepository and its PlatformTransactionManager dependency). When the EntityManagerFactory is created and post-processed, the DataSourceInitializedEvent is stored in the context's earlyApplicationEvents as there's no multicaster available at this point. This deferral removes the double post-processing of entityManagerFactory and prevents the attempt to create webSocketRegistryListener
As expected based on the description above, the tests pass if they're configured with a non-mock web environment:
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
Thanks for the detailed analysis @wilkinsona!
The creation of the JpaTransactionManager requires the injection of an entityManagerFactory. It's found and, for reasons I don't yet understand, is post-processed for a second time.
It's post-processed for a second time because FactoryBeanRegistrationSupport.getObjectFromBeanFactory is called twice. The second call happens beneath the first and before the first call has had a chance to add it to the factoryBeanObjectCache.
I'm pretty sure that this is a Framework bug as a factory bean the produces in singleton is being asked for its bean twice. My understanding of the factory bean contract is that that should not happen. There's also a comment in the code that suggests that this sort of circular lookup should be handled:
// Only post-process and store if not put there already during getObject() call above
// (e.g. because of circular reference processing triggered by custom getBean calls)
That handling doesn't seem to work in the case. I suspect it's because the cycle is occurring during the post-processing.
I've opened SPR-16783. I managed to produce a similar, although not identical, failure without Boot's involvement. I'm going to close this one as I'm hopeful that a fix will be made in Framework. If that turns out not to be the case, we can re-open this issue.
@vpavic there is a fix in the upcoming 5.0.6.BUILD-SNAPSHOT that Spring Boot 2.0.2 will use. I've confirmed the sample @wilkinsona created runs fine now.
Thanks - I've confirmed that the sample app provided in spring-projects/spring-session#995 works with Boot 1.5.13.RELEASE/Framework 4.3.17.RELEASE.
Can someone please suggest to me how and where I can open a ticket that seems the exact same issue, but this issue is with:
I have tested this and it does not happen with HSQL nor Derby.
As soon as I move the dataSource configuration into a separate config, this is resolved.
@mickknutson Thanks for asking. Let's start with a Boot issue. We can move to another project if that turns out to be necessary, but it sounds like some initial diagnosis at the Boot level will be needed at least. If/when you open an issue, please provide a minimal sample that reproduces the behaviour you've described.
Most helpful comment
As expected based on the description above, the tests pass if they're configured with a non-mock web environment: