It appears that when there are two DataSource beans created and one bean factory method calls the other DataSource's bean factory method (e.g. in a delegate pattern), DataSourceInitializer triggers a circular bean reference. It doesn't help that one bean is tagged @Primary.
Apologies if I'm doing something incorrectly. I posted at Stack Overflow and did not get responses. I reviewed the doc and I believe I've followed that, and it does not mention this delegate pattern.
I've created a sample repository with two junit tests that demonstrate the issue. The full stack trace is also available there along with some analysis. In summary, the @Primary DataSource bean triggers creation of the subordinate DataSource bean which completes successfully. The creation of the subordinate bean triggers DataSourceInitializer which asks the bean factory for the DataSource and the bean factory selects the @Primary which is still in creation.
I've reviewed both open and closed issues and found several relevant issues: #2383 is similar but isn't using @Primary, #8068 seems unrelated, #7652 mentions tricks needed for a similar setup but doesn't have the same relationship between the two beans, #5541 implies this should work but again doesn't have one factory bean calling the other factory bean, #5104 suggests using @Primary which doesn't help in this case. #2784 seems to be the closest match and was closed per 9c733128ac942ac4675b27d2d358afa941fea5fd. However, 9c733128ac942ac4675b27d2d358afa941fea5fd only seems to update the Rabbit and JMS configuration. Should @ConditionalOnSingleCandidate also be used for the DataSourceInitializer?
Should @ConditionalOnSingleCandidate also be used for the DataSourceInitializer?
We intend to do that, yes see #6560. There could be a way to break that cycle though.
Thanks and sorry I missed that issue in my search. I see you've asked to keep convo here and will do so.
I do agree that I would like to resolve this without requiring that approach. Why does DataSourceInitializer even exist as a bean? Why not do the actual work in DataSourceInitializerPostProcessor on either each and every DataSource or only on @Primary beans?
Ironically enough, that test passes in IntelliJ IDEA.
I can see where the problem comes from now. The BPP attempts to fully initialize DataSourceInitializer as soon as it sees a DataSource. The problem in your example is that the secondary one (that's not primary) is the first one to be post-processed so it early triggers the initialization of the DataSourceInitalizer and that has the effect of requesting the primary DataSource to be resolved.
I am not sure I get why IJ does not reveal the cycle. But I think I have an idea to fix it.
@snicoll thanks, that was my analysis as well. I did find a workaround - by using @DependsOn("dataSourceInitializer") on the @Primary bean definition, spring eagerly creates the dataSourceInitializer bean before the primary is marked as in creation. Kind of a hack but works for now.
Given you have a workaround and changing things in the BPP may break other use cases, I am tempted to close this issue as a duplicate of #9528. Let see what the rest of the team think.
Yes, I think a DUP of #9528 makes sense. Thanks for looking and opening that issue.
Duplicate of #9528
@zachmarshall @snicoll it seems that your workaround does not work in Spring Boot 2.0 app with two secondary data sources and one primary RoutingDataSource.
This is my config:
@Configuration
//@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
public class MultiDataSourceConfig {
@Bean
@DependsOn("dataSourceInitializer")
@Primary
public DataSource routingDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("write", masterDataSource);
dataSourceMap.put("read", slaveDataSource);
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "db.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "db.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
}
ReplicationRoutingDataSource:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? "read" : "write";
}
}
What I get is:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'dataSourceInitializer' available
When I tried to exclude some auto configurations and commented out @DependsOn("dataSourceInitializer") I get the issue from title:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'routingDataSource' defined in class path resource [MultiDataSourceConfig.class]: Unsatisfied dependency expressed through method 'routingDataSource' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'masterDataSource' defined in class path resource [MultiDataSourceConfig.class]: Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker': Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'routingDataSource': Requested bean is currently in creation: Is there an unresolvable circular reference?
Any thoughts how to fix it?
Spring Boot 2.0.0.RELEASE
Spring Data JPA 2.0.5.RELEASE
@rvit34 Can probably wrap the DataSource
class DSHolder {
DataSource dataSource
}
ConfigurationProperties probably won't work so have to do it old way.
@zachmarshall
I don't have such bean @DependsOn("dataSourceInitializer"). Can you provide an example so I can use this in my application?
I am getting this issue after upgrading to Spring Boot 2.1.3 from 1.5.X
I am getting the same error as @rvit34
No bean named 'dataSourceInitializer' available
@rvit34 Did you find a solution?
@AleksandarTokarev I cleaned my code above and forget about solution. But I remember that it worked. Now I am using similar configuration with SB 2.1/2.2 and Kotlin. And It works:
@Configuration
class DBAutoConfiguration {
@Bean("masterDataSourceProperties")
@Primary
@ConfigurationProperties("spring.datasource")
fun masterDataSourceProperties() = DataSourceProperties()
@Bean("slaveDataSourceProperties")
@ConditionalOnProperty("spring.slave-datasource.enabled", havingValue = "true")
@ConfigurationProperties("spring.slave-datasource")
fun slaveDataSourceProperties() = DataSourceProperties()
@Bean("masterDataSource")
@ConfigurationProperties("spring.datasource")
fun masterDataSource(@Qualifier("masterDataSourceProperties") masterProperties: DataSourceProperties) = masterProperties.initializeDataSourceBuilder().build()
@Bean("slaveDataSource")
@ConditionalOnProperty("spring.slave-datasource.enabled", havingValue = "true")
@ConfigurationProperties("spring.slave-datasource")
fun slaveDataSource(@Qualifier("slaveDataSourceProperties") slaveProperties: DataSourceProperties) = slaveProperties.initializeDataSourceBuilder().build()
@Bean("routingDataSource")
@DependsOn("masterDataSource")
@Primary
fun routingDataSource(
@Qualifier("masterDataSource") masterDataSource: DataSource,
@Qualifier("slaveDataSource") slaveDataSource: DataSource?
): DataSource {
val dataSource = RoutingDataSource()
dataSource.setTargetDataSources(mapOf(
if (slaveDataSource != null) {
DataSourceType.MASTER to masterDataSource
DataSourceType.SLAVE to slaveDataSource
} else {
DataSourceType.MASTER to masterDataSource
})
)
dataSource.setDefaultTargetDataSource(masterDataSource)
return dataSource
}
@Bean
@ConditionalOnProperty("spring.slave-datasource.enabled", havingValue = "true")
fun dataSourceConnectionInterceptor() = DataSourceConnectionInterceptor()
}
class RoutingDataSource : AbstractRoutingDataSource() {
override fun determineCurrentLookupKey(): DataSourceType = DataSourceTypeHolder.dataSource
}
enum class DataSourceType {
MASTER, SLAVE
}
object DataSourceTypeHolder {
private val typeHolder = ThreadLocal<DataSourceType>()
val dataSource: DataSourceType
get() = typeHolder.get() ?: DataSourceType.MASTER
fun putDataSource(dataSourceType: DataSourceType) {
typeHolder.set(dataSourceType)
}
fun clear() = typeHolder.remove()
}
@AleksandarTokarev The easiest solution is to exclude DataSourceAutoConfiguration.class
like the following:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
Most helpful comment
@snicoll thanks, that was my analysis as well. I did find a workaround - by using
@DependsOn("dataSourceInitializer")on the@Primarybean definition, spring eagerly creates the dataSourceInitializer bean before the primary is marked as in creation. Kind of a hack but works for now.