Skip to content

Conflicting transactionInterceptor bean when using Spring Boot 3.0.2 and Couchbase Starter #1665

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
cjp421 opened this issue Feb 17, 2023 · 6 comments · Fixed by #1679
Closed
Assignees
Labels
status: feedback-provided Feedback has been provided type: regression A regression from a previous release

Comments

@cjp421
Copy link

cjp421 commented Feb 17, 2023

Originally created spring-projects/spring-boot#34209 but was instructed to create a new issue here.

Receiving the following error when using Spring Boot 3.0.2 and spring-boot-starter-data-couchbase:

The bean 'transactionInterceptor', defined in class path resource [org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.class], could not be registered. A bean with that name has already been defined in class path resource [com/example/demo/CouchbaseConfig.class] and overriding is disabled.

To recreate:

  1. go to start.spring.io and create a new Spring Boot 3.0.2 project with the couchbase starter dependency
  2. Create a configuration class extending AbstractCouchbaseConfiguration
public CouchbaseConfig extends AbstractCouchbaseConfiguration {
    // override required methods
}

3, Try and start the application ./gradlew bootRun

Is there something I'm missing with Spring Boot 3.0.2 and Couchbase starter?

https://github.com/spring-projects/spring-data-couchbase/blob/main/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java#L377-L388

https://github.com/spring-projects/spring-framework/blob/main/spring-tx/src/main/java/org/springframework/transaction/annotation/ProxyTransactionManagementConfiguration.java#L65-L74

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Feb 17, 2023
@babltiga
Copy link
Contributor

@cjp421 I think you are missing one of the Requirements documented in the official docs. That is

Set spring.main.allow-bean-definition-overriding=true either in application.properties or as a SpringApplicationBuilder property.

@cjp421
Copy link
Author

cjp421 commented Feb 17, 2023

I don't know if the documentation should be improved? I'm not using @Transactional so I wouldn't have thought to look there. Also, am i wrong in my assumption is that my app should generally just "work" when creating a new project from Spring Initializr?

It was also suggested that the couchbase starter should use a different name or use a bean post processor to modify the interceptor provided by the framework. Would that be a more elegant solution than just allowing bean overrides any time the couchbase starter is used?

spring-projects/spring-boot#34209 (comment)

@mikereiche
Copy link
Collaborator

I didn't realize this was preventing the application to initialize as all our test application have the property set. I would love to fix this, but I don't have any great solutions.

I don't know if the documentation should be improved? I'm not using @transactional so I wouldn't have thought to look there.

The application property should be mentioned very near the beginning - perhaps in the Installation and Configuration section. Also If the documentation reference the exception, then a search of the documentation would find the explanation.

my app should generally just "work" when creating a new project from Spring Initializr?

It should, and it would if we omit the transactionInterceptor bean, but then couchbase transactions won't work, unless the user included the transactionInterceptor bean in their Configuration class, and the application property. This is probably the best solution.

It was also suggested that the couchbase starter should use a different name or use a bean post processor to modify the interceptor provided by the framework. Would that be a more elegant solution than just allowing bean overrides any time the couchbase starter is used?

As far as I can tell, the bean must have that name, as that is what the framework uses for the bean dependency in transactionAdvisor() in ProxyTransactionManagementConfiguration. A post-processor won't help as the critical difference is in the actual processing, not the post-processing.

@mikereiche mikereiche added type: regression A regression from a previous release status: feedback-provided Feedback has been provided and removed status: waiting-for-triage An issue we've not yet triaged labels Feb 17, 2023
@mikereiche mikereiche self-assigned this Feb 17, 2023
@wilkinsona
Copy link
Member

IMO, a post-processor could help here. For example, I think you could replace the transactionInterceptor method in AbstractCouchbaseConfiguration with the following bean post-processor:

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static BeanPostProcessor transactionInterceptorCustomizer(
		ObjectProvider<TransactionManager> transactionManagerProvider) {
	return new BeanPostProcessor() {

		@Override
		public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
			if (bean instanceof TransactionInterceptor) {
				TransactionAttributeSource transactionAttributeSource = new AnnotationTransactionAttributeSource();
				TransactionManager transactionManager = transactionManagerProvider.getObject();
				TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(transactionManager,
						transactionAttributeSource);
				interceptor.setTransactionAttributeSource(transactionAttributeSource);
				if (transactionManager != null) {
					interceptor.setTransactionManager(transactionManager);
				}
				return interceptor;
			}
			return bean;
		}

	};
}

This is untested but hopefully it will at least serve as a useful starting point. You may want to do a bean name check as well in case there's another TransactionInterceptor bean with another name.

@mikereiche
Copy link
Collaborator

mikereiche commented Feb 21, 2023

Thanks, I appreciate the effort. And thanks for showing what a postProcessor is.

[EDIT: I did find a way around this but it seems like a terrible hack]

But - transactionInterceptorCustomizer is not getting called for transactionInterceptor (it does get called for a lot of other beans). It looks like the issue is that TransactionInterceptor is instantiated (including the postProcessors being called) before the BeanPostProcessor provided by transactionInterceptorCustomizer is registered as a post-processor because org.springframework.transaction.config.internalTransactionAdvisor is considered for an advisor.

during the creation of the transactionInterceptorCustomizer bean, the call to findCandidateAdvisors() results in transactionInterceptor being instantiated and the postProcessors being called - but at that point, the transactionInterceptorCustomizer postProcessor does not yet exist.

  protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
    List<Advisor> candidateAdvisors = this.findCandidateAdvisors();
    List<Advisor> eligibleAdvisors = this.findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
    this.extendAdvisors(eligibleAdvisors);
    if (!eligibleAdvisors.isEmpty()) {
      eligibleAdvisors = this.sortAdvisors(eligibleAdvisors);
    }

    return eligibleAdvisors;
  }

To allow the BeanPostProcessor provided by transactionInterceptorCustomizer to be called as a post-processor for transactionInterceptor when transactionInterceptor is being created/initialized DURING the creation/initialization of the BeanPostProcessor, it's necessary that transactionInterceptorCustomizer registers the BeanPostProcessor it is creating.

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static BeanPostProcessor transactionInterceptorCustomizer(
	ObjectProvider<TransactionManager> transactionManagerProvider,
	ConfigurableListableBeanFactory beanFactory){

	BeanPostProcessor processor =  new BeanPostProcessor() {
		@Override
		public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
			if (bean instanceof TransactionInterceptor) {
				TransactionAttributeSource transactionAttributeSource = new AnnotationTransactionAttributeSource();
				TransactionManager transactionManager = transactionManagerProvider.getObject();
				TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(transactionManager,
					transactionAttributeSource);
				interceptor.setTransactionAttributeSource(transactionAttributeSource);
				if (transactionManager != null) {
					interceptor.setTransactionManager(transactionManager);
				}
				return interceptor;
			}
			return bean;
		}

	};
	beanFactory.addBeanPostProcessor(processor); // <- REGISTER HERE
	return processor;
}

@wilkinsona
Copy link
Member

Ah, sorry. I hadn't considered the timing of the creation of the interceptor bean and the effect that would have on being able to post-process it. I agree that calling addBeanPostProcessor directly from the @Bean method is a terrible hack. Hopefully it won't come to that. I've asked the Framework team if someone can take a look and provide some guidance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: feedback-provided Feedback has been provided type: regression A regression from a previous release
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants