Consider type-level qualifier annotations for transaction manager selection

Closes gh-24291
This commit is contained in:
Juergen Hoeller 2024-03-06 13:36:33 +01:00
parent 6461eec582
commit 14a461e795
6 changed files with 165 additions and 31 deletions

View File

@ -548,12 +548,32 @@ transaction managers, differentiated by the `order`, `account`, and `reactive-ac
qualifiers. The default `<tx:annotation-driven>` target bean name, `transactionManager`,
is still used if no specifically qualified `TransactionManager` bean is found.
[TIP]
====
If all transactional methods on the same class share the same qualifier, consider
declaring a type-level `org.springframework.beans.factory.annotation.Qualifier`
annotation instead. If its value matches the qualifier value (or bean name) of a
specific transaction manager, that transaction manager is going to be used for
transaction definitions without a specific qualifier on `@Transactional` itself.
Such a type-level qualifier can be declared on the concrete class, applying to
transaction definitions from a base class as well. This effectively overrides
the default transaction manager choice for any unqualified base class methods.
Last but not least, such a type-level bean qualifier can serve multiple purposes,
e.g. with a value of "order" it can be used for autowiring purposes (identifying
the order repository) as well as transaction manager selection, as long as the
target beans for autowiring as well as the associated transaction manager
definitions declare the same qualifier value. Such a qualifier value only needs
to be unique with a set of type-matching beans, not having to serve as an id.
====
[[tx-custom-attributes]]
== Custom Composed Annotations
If you find you repeatedly use the same attributes with `@Transactional` on many different
methods, xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[Spring's meta-annotation support] lets you
define custom composed annotations for your specific use cases. For example, consider the
If you find you repeatedly use the same attributes with `@Transactional` on many different methods,
xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[Spring's meta-annotation support]
lets you define custom composed annotations for your specific use cases. For example, consider the
following annotation definitions:
[tabs]

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.beans.factory.annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
@ -138,6 +139,19 @@ public abstract class BeanFactoryAnnotationUtils {
}
}
/**
* Determine the {@link Qualifier#value() qualifier value} for the given
* annotated element.
* @param annotatedElement the class, method or parameter to introspect
* @return the associated qualifier value, or {@code null} if none
* @since 6.2
*/
@Nullable
public static String getQualifierValue(AnnotatedElement annotatedElement) {
Qualifier qualifier = AnnotationUtils.getAnnotation(annotatedElement, Qualifier.class);
return (qualifier != null ? qualifier.value() : null);
}
/**
* Check whether the named bean declares a qualifier of the given name.
* @param qualifier the qualifier to match

View File

@ -139,6 +139,14 @@ public @interface Transactional {
* qualifier value (or the bean name) of a specific
* {@link org.springframework.transaction.TransactionManager TransactionManager}
* bean definition.
* <p>Alternatively, as of 6.2, a type-level bean qualifier annotation with a
* {@link org.springframework.beans.factory.annotation.Qualifier#value() qualifier value}
* is also taken into account. If it matches the qualifier value (or bean name)
* of a specific transaction manager, that transaction manager is going to be used
* for transaction definitions without a specific qualifier on this attribute here.
* Such a type-level qualifier can be declared on the concrete class, applying
* to transaction definitions from a base class as well, effectively overriding
* the default transaction manager choice for any unqualified base class methods.
* @since 4.2
* @see #value
* @see org.springframework.transaction.PlatformTransactionManager

View File

@ -35,6 +35,7 @@ import reactor.core.publisher.Mono;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.core.CoroutinesUtils;
import org.springframework.core.KotlinDetector;
@ -349,7 +350,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
// If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final TransactionManager tm = determineTransactionManager(txAttr);
final TransactionManager tm = determineTransactionManager(txAttr, targetClass);
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {
boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
@ -499,9 +500,19 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
/**
* Determine the specific transaction manager to use for the given transaction.
* @param txAttr the current transaction attribute
* @param targetClass the target class that the attribute has been declared on
* @since 6.2
*/
@Nullable
protected TransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
protected TransactionManager determineTransactionManager(
@Nullable TransactionAttribute txAttr, @Nullable Class<?> targetClass) {
TransactionManager tm = determineTransactionManager(txAttr);
if (tm != null) {
return tm;
}
// Do not attempt to lookup tx manager if no tx attributes are set
if (txAttr == null || this.beanFactory == null) {
return getTransactionManager();
@ -511,7 +522,20 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
if (StringUtils.hasText(qualifier)) {
return determineQualifiedTransactionManager(this.beanFactory, qualifier);
}
else if (StringUtils.hasText(this.transactionManagerBeanName)) {
else if (targetClass != null) {
// Consider type-level qualifier annotations for transaction manager selection
String typeQualifier = BeanFactoryAnnotationUtils.getQualifierValue(targetClass);
if (StringUtils.hasText(typeQualifier)) {
try {
return determineQualifiedTransactionManager(this.beanFactory, typeQualifier);
}
catch (NoSuchBeanDefinitionException ex) {
// Consider type qualifier as optional, proceed with regular resolution below.
}
}
}
if (StringUtils.hasText(this.transactionManagerBeanName)) {
return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);
}
else {
@ -528,6 +552,16 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
}
}
/**
* Determine the specific transaction manager to use for the given transaction.
* @deprecated as of 6.2, in favor of {@link #determineTransactionManager(TransactionAttribute, Class)}
*/
@Deprecated
@Nullable
protected TransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
return null;
}
private TransactionManager determineQualifiedTransactionManager(BeanFactory beanFactory, String qualifier) {
TransactionManager txManager = this.transactionManagerCache.get(qualifier);
if (txManager == null) {
@ -538,7 +572,6 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
return txManager;
}
@Nullable
private PlatformTransactionManager asPlatformTransactionManager(@Nullable Object transactionManager) {
if (transactionManager == null) {

View File

@ -23,7 +23,9 @@ import java.util.Properties;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@ -46,6 +48,7 @@ import org.springframework.transaction.testfixture.CallCountingTransactionManage
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
import static org.springframework.transaction.annotation.RollbackOn.ALL_EXCEPTIONS;
/**
@ -255,9 +258,34 @@ class EnableTransactionManagementTests {
assertThat(txManager.commits).isEqualTo(2);
assertThat(txManager.rollbacks).isEqualTo(0);
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(bean::findAllFoos);
ctx.close();
}
@Test
void gh24291TransactionManagerViaQualifierAnnotation() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh24291Config.class);
TransactionalTestBean bean = ctx.getBean(TransactionalTestBean.class);
CallCountingTransactionManager txManager = ctx.getBean("qualifiedTransactionManager", CallCountingTransactionManager.class);
bean.saveQualifiedFoo();
assertThat(txManager.begun).isEqualTo(1);
assertThat(txManager.commits).isEqualTo(1);
assertThat(txManager.rollbacks).isEqualTo(0);
bean.saveQualifiedFooWithAttributeAlias();
assertThat(txManager.begun).isEqualTo(2);
assertThat(txManager.commits).isEqualTo(2);
assertThat(txManager.rollbacks).isEqualTo(0);
bean.findAllFoos();
assertThat(txManager.begun).isEqualTo(3);
assertThat(txManager.commits).isEqualTo(3);
assertThat(txManager.rollbacks).isEqualTo(0);
ctx.close();
}
@Test
void spr14322FindsOnInterfaceWithInterfaceProxy() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Spr14322ConfigA.class);
@ -352,6 +380,12 @@ class EnableTransactionManagementTests {
}
@Service
@Qualifier("qualified")
public static class TransactionalTestBeanSubclass extends TransactionalTestBean {
}
@Configuration
static class PlaceholderConfig {
@ -535,6 +569,35 @@ class EnableTransactionManagementTests {
public TransactionalTestBean testBean() {
return new TransactionalTestBean();
}
@Bean
public CallCountingTransactionManager otherTxManager() {
return new CallCountingTransactionManager();
}
}
@Configuration
@EnableTransactionManagement
@Import(PlaceholderConfig.class)
static class Gh24291Config {
@Autowired
public void initializeApp(ConfigurableApplicationContext applicationContext) {
applicationContext.getBeanFactory().registerSingleton(
"qualifiedTransactionManager", new CallCountingTransactionManager());
applicationContext.getBeanFactory().registerAlias("qualifiedTransactionManager", "qualified");
}
@Bean
public TransactionalTestBeanSubclass testBean() {
return new TransactionalTestBeanSubclass();
}
@Bean
public CallCountingTransactionManager otherTxManager() {
return new CallCountingTransactionManager();
}
}

View File

@ -116,15 +116,11 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
ti.setTransactionManager(ptm);
ti = SerializationTestUtils.serializeAndDeserialize(ti);
boolean condition3 = ti.getTransactionManager() instanceof SerializableTransactionManager;
assertThat(condition3).isTrue();
boolean condition2 = ti.getTransactionAttributeSource() instanceof CompositeTransactionAttributeSource;
assertThat(condition2).isTrue();
assertThat(ti.getTransactionManager() instanceof SerializableTransactionManager).isTrue();
assertThat(ti.getTransactionAttributeSource() instanceof CompositeTransactionAttributeSource).isTrue();
CompositeTransactionAttributeSource ctas = (CompositeTransactionAttributeSource) ti.getTransactionAttributeSource();
boolean condition1 = ctas.getTransactionAttributeSources()[0] instanceof NameMatchTransactionAttributeSource;
assertThat(condition1).isTrue();
boolean condition = ctas.getTransactionAttributeSources()[1] instanceof NameMatchTransactionAttributeSource;
assertThat(condition).isTrue();
assertThat(ctas.getTransactionAttributeSources()[0] instanceof NameMatchTransactionAttributeSource).isTrue();
assertThat(ctas.getTransactionAttributeSources()[1] instanceof NameMatchTransactionAttributeSource).isTrue();
}
@Test
@ -132,7 +128,7 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
PlatformTransactionManager transactionManager = mock();
TransactionInterceptor ti = transactionInterceptorWithTransactionManager(transactionManager, null);
assertThat(ti.determineTransactionManager(new DefaultTransactionAttribute())).isSameAs(transactionManager);
assertThat(ti.determineTransactionManager(new DefaultTransactionAttribute(), null)).isSameAs(transactionManager);
}
@Test
@ -140,7 +136,7 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
PlatformTransactionManager transactionManager = mock();
TransactionInterceptor ti = transactionInterceptorWithTransactionManager(transactionManager, null);
assertThat(ti.determineTransactionManager(null)).isSameAs(transactionManager);
assertThat(ti.determineTransactionManager(null, null)).isSameAs(transactionManager);
}
@Test
@ -148,7 +144,7 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
BeanFactory beanFactory = mock();
TransactionInterceptor ti = simpleTransactionInterceptor(beanFactory);
assertThat(ti.determineTransactionManager(null)).isNull();
assertThat(ti.determineTransactionManager(null, null)).isNull();
}
@Test
@ -158,9 +154,9 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setQualifier("fooTransactionManager");
assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() ->
ti.determineTransactionManager(attribute))
.withMessageContaining("'fooTransactionManager'");
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
.isThrownBy(() -> ti.determineTransactionManager(attribute, null))
.withMessageContaining("'fooTransactionManager'");
}
@Test
@ -174,7 +170,7 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setQualifier("fooTransactionManager");
assertThat(ti.determineTransactionManager(attribute)).isSameAs(fooTransactionManager);
assertThat(ti.determineTransactionManager(attribute, null)).isSameAs(fooTransactionManager);
}
@Test
@ -189,7 +185,7 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setQualifier("fooTransactionManager");
assertThat(ti.determineTransactionManager(attribute)).isSameAs(fooTransactionManager);
assertThat(ti.determineTransactionManager(attribute, null)).isSameAs(fooTransactionManager);
}
@Test
@ -203,7 +199,7 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setQualifier("");
assertThat(ti.determineTransactionManager(attribute)).isSameAs(defaultTransactionManager);
assertThat(ti.determineTransactionManager(attribute, null)).isSameAs(defaultTransactionManager);
}
@Test
@ -215,11 +211,11 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setQualifier("fooTransactionManager");
TransactionManager actual = ti.determineTransactionManager(attribute);
TransactionManager actual = ti.determineTransactionManager(attribute, null);
assertThat(actual).isSameAs(txManager);
// Call again, should be cached
TransactionManager actual2 = ti.determineTransactionManager(attribute);
TransactionManager actual2 = ti.determineTransactionManager(attribute, null);
assertThat(actual2).isSameAs(txManager);
verify(beanFactory, times(1)).containsBean("fooTransactionManager");
verify(beanFactory, times(1)).getBean("fooTransactionManager", TransactionManager.class);
@ -234,11 +230,11 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
PlatformTransactionManager txManager = associateTransactionManager(beanFactory, "fooTransactionManager");
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
TransactionManager actual = ti.determineTransactionManager(attribute);
TransactionManager actual = ti.determineTransactionManager(attribute, null);
assertThat(actual).isSameAs(txManager);
// Call again, should be cached
TransactionManager actual2 = ti.determineTransactionManager(attribute);
TransactionManager actual2 = ti.determineTransactionManager(attribute, null);
assertThat(actual2).isSameAs(txManager);
verify(beanFactory, times(1)).getBean("fooTransactionManager", TransactionManager.class);
}
@ -252,11 +248,11 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
given(beanFactory.getBean(TransactionManager.class)).willReturn(txManager);
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
TransactionManager actual = ti.determineTransactionManager(attribute);
TransactionManager actual = ti.determineTransactionManager(attribute, null);
assertThat(actual).isSameAs(txManager);
// Call again, should be cached
TransactionManager actual2 = ti.determineTransactionManager(attribute);
TransactionManager actual2 = ti.determineTransactionManager(attribute, null);
assertThat(actual2).isSameAs(txManager);
verify(beanFactory, times(1)).getBean(TransactionManager.class);
}