1

I am trying to migrate a Spring Boot project, version 2.3.0.M3, that have used JDBC template to R2DBC. The project also uses Liquibase so I cannot get rid of JDBC altogether. I have both the spring-boot-starter-data-r2dbc and the spring-boot-starter-jdbc dependencies in the project with which I get the following exception when trying to run one of my tests:

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single matching bean but found 2: transactionManager,connectionFactoryTransactionManager

    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1180)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:416)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.determineTransactionManager(TransactionAspectSupport.java:480)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:335)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:99)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
...

The bean connectionFactoryTransaction manager is defined like this in the Spring class R2dbcTransactionManagerAutoConfiguration:

    @Bean
    @ConditionalOnMissingBean(ReactiveTransactionManager.class)
    public R2dbcTransactionManager connectionFactoryTransactionManager(ConnectionFactory connectionFactory) {
        return new R2dbcTransactionManager(connectionFactory);
    }

The bean transactionManager is defined like this in the Spring class DataSourceTransactionManagerAutoConfiguration:

   @Bean
   @ConditionalOnMissingBean(PlatformTransactionManager.class)
   DataSourceTransactionManager transactionManager(DataSource dataSource,
           ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
       DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
       transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));
       return transactionManager;
   }

As can be seen, the @ConditionalOnMissingBean annotation contains different types which will cause an instance of both beans to be created. However, in the Spring class TransactionAspectSupport there is this line of code in the determineTransactionManager method:

defaultTransactionManager = this.beanFactory.getBean(TransactionManager.class);

Since both of the transaction manager types, DataSourceTransactionManager and R2dbcTransactionManager, implement the TransactionManager interface, both the transaction manager beans as above will be matched and the error will occur.

I am now reaching out to hear if there is anyone who has managed to solve or work around this issue?
Thanks in advance!

  • 1
    Remove the JDBC starter, liquibase has it's own implemenation. Specify the `spring.liquibase.url` property and friends to have a dedicated connection for liquibase which won't interfere with R2DBC. – M. Deinum Mar 25 '20 at 13:06

2 Answers2

2

It's possible to have spring-boot-starter-jdbc and spring-boot-starter-data-r2dbc co-exist. There is a class org.springframework.transaction.annotation.TransactionManagementConfigurer that can be used to resolve the conflict. Spring Boot 2.3.0 seems to disable automatic datasource config when r2dbc is present. It's possible to manually import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration class to make both co-exist.

@Bean
TransactionManagementConfigurer transactionManagementConfigurer(ReactiveTransactionManager reactiveTransactionManager) {
    return new TransactionManagementConfigurer() {
        @Override
        public TransactionManager annotationDrivenTransactionManager() {
            return reactiveTransactionManager;
        }
    };
}
Lari Hotari
  • 5,190
  • 1
  • 36
  • 43
1

With inspiration from M. Deinums answer (thanks!), I applied the following steps to my project and the test that failed earlier now runs successfully:

  • Remove the spring-boot-starter-jdbc dependency.
  • Add a dependency to spring-jdbc.
  • Add a dependency to HikariCP (com.zaxxer).
  • Add spring.liquibase user and password properties (I already had the url and change-log properties).
  • Remove all spring.datasource properties (I had url and drive-class-name).

I had the spring.r2dbc properties username, password and url defined which I did not need to change.

Update:
In addition, I used Testcontainers in the tests and could not assign a static port. In order to be able to configure the database port on Liquibase, I overrode a bean name liquibase of the type SpringLiquibase and created a DataSource (not exposed as a bean) in the liquibase bean creation method and set it on the liquibase bean.

  • You don't need Spring JDBC (liquibase works without it) nThe only dependency you should need is the liquibase on (and a connection pool). – M. Deinum Mar 25 '20 at 13:45
  • 2
    Unfortunately I get the following exception if I remove the Spring JDBC dependency: Caused by: java.lang.ClassNotFoundException: org.springframework.jdbc.core.ConnectionCallback at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521) ... 66 more –  Mar 25 '20 at 14:09
  • According to the Spring Boot documentation you should be able to only use the `liquibase-core` dependency and the custom properties and that would be all that is needed. Liquibase itsel doesn't depend on Spring JDBC, so it must be the fact that you include a connection pool that triggers the JDBC configuration in Spring. Basically removing the `spring-boot-starter-jdbc` and replacing them with `spring-jdbc` and HikariCIP is effectively replacing `spring-boot-starter-jdbc` with its individual dependencies (hence the effect is the same). – M. Deinum Mar 25 '20 at 14:48