Skip to content

Circular dependency in @SpringBootTest for an app that starts normally #12280

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
vpavic opened this issue Feb 28, 2018 · 10 comments
Closed

Circular dependency in @SpringBootTest for an app that starts normally #12280

vpavic opened this issue Feb 28, 2018 · 10 comments
Assignees
Labels
for: external-project For an external project and not something we can fix

Comments

@vpavic
Copy link
Contributor

vpavic commented Feb 28, 2018

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')
 }
@vpavic vpavic changed the title Circular dependency in @SpringBootTest for an app that Circular dependency in @SpringBootTest for an app that starts normally Feb 28, 2018
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Feb 28, 2018
@wilkinsona wilkinsona self-assigned this Feb 28, 2018
@wilkinsona
Copy link
Member

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:

  • webSocketBrokerConfig
  • sessionRepository (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.

@wilkinsona
Copy link
Member

wilkinsona commented Feb 28, 2018

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

@wilkinsona
Copy link
Member

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)

@vpavic
Copy link
Contributor Author

vpavic commented Feb 28, 2018

Thanks for the detailed analysis @wilkinsona!

@wilkinsona
Copy link
Member

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.

@wilkinsona
Copy link
Member

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.

@wilkinsona wilkinsona added for: external-project For an external project and not something we can fix and removed status: waiting-for-triage An issue we've not yet triaged labels Apr 30, 2018
@snicoll
Copy link
Member

snicoll commented May 7, 2018

@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.

@vpavic
Copy link
Contributor Author

vpavic commented May 10, 2018

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.

@mickknutson
Copy link

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:

  • Spring Boot 2.0.4 (and tested with 2.1.0.M2)
  • Spring Batch 4
  • Spring Web (and tested with webflux)
  • H2 DB
  • With batch config extending DefaultBatchConfigurer

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.

@wilkinsona
Copy link
Member

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
for: external-project For an external project and not something we can fix
Projects
None yet
Development

No branches or pull requests

5 participants