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
?
Comment From: snicoll
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.
Comment From: zachmarshall
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?
Comment From: snicoll
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.
Comment From: zachmarshall
@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.
Comment From: snicoll
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.
Comment From: zachmarshall
Yes, I think a DUP of #9528 makes sense. Thanks for looking and opening that issue.
Comment From: snicoll
Duplicate of #9528
Comment From: rvit34
@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
Comment From: awesomekosm
@rvit34 Can probably wrap the DataSource
class DSHolder {
DataSource dataSource
}
ConfigurationProperties
probably won't work so have to do it old way.
Comment From: AleksandarTokarev
@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?
Comment From: rvit34
@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()
}
Comment From: wuyupengwoaini
@AleksandarTokarev The easiest solution is to exclude DataSourceAutoConfiguration.class
like the following:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
Comment From: msangel
Can't say this bug is closed, as I need to use this initializer on the secondary datasource, that cannot be marked as @Primary
Can this be made as @ConditionalOnMissing with custom implementations (where the propper db will be referencing to).
Still open question when there will be more then 2 DBs and some of them will require this.
Comment From: snicoll
@msangel please review the history. This issue has been closed as a duplicate. If you follow the links you'll see that this has been reworked heavily for the upcoming Spring Boot 2.5 version.
Comment From: Xlong99
If the version you are using still has this issues and u use mybatis-starter,And need to configure multiple data source beans in Beanfactory
i also find a work around ---by annotation @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class})
to exclude the autoconfiguration class of data source,and register datasource by @Bean
yourself
like this:
/**
* DataSourceA
*
* @return
*/
@Bean(name = "DataSourceA", initMethod = "init")
public DataSource DataSourceA() {
DataSource dataSourceA = new DataSource();
//........
return dataSourceA;
}
/**
* DataSourceB
*
* @return
*/
@Bean(name = "DataSourceB", initMethod = "init")
public DataSource oldDataSource() {
DataSource dataSourceB = new DataSource();
//........
return dataSourceB;
}
/**
* DynamicDataSource
*
* @return
*/
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dynamicDataSource(@Qualifier("DataSourceA") DataSource DataSourceA,
@Qualifier("DataSourceB") DataSource DataSourceB) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceEnum.NEW, DataSourceA);
targetDataSources.put(DataSourceEnum.OLD, DataSourceB);
DynamicDataSource dataSource = new DynamicDataSource();
// AbstractRoutingDataSource
dataSource.setTargetDataSources(targetDataSources);
//
dataSource.setDefaultTargetDataSource(DataSourceA);
return dataSource;
}
/**
* SqlSessionFactory
*
* @param dataSource
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource)
throws Exception {
final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
//.....
sessionFactoryBean.setDataSource(dataSource);
SqlSessionFactory sqlSessionFactory = sessionFactoryBean.getObject();
return sqlSessionFactory;
}`