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`, qualifiers. The default `<tx:annotation-driven>` target bean name, `transactionManager`,
is still used if no specifically qualified `TransactionManager` bean is found. 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]] [[tx-custom-attributes]]
== Custom Composed Annotations == Custom Composed Annotations
If you find you repeatedly use the same attributes with `@Transactional` on many different If you find you repeatedly use the same attributes with `@Transactional` on many different methods,
methods, xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[Spring's meta-annotation support] lets you xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[Spring's meta-annotation support]
define custom composed annotations for your specific use cases. For example, consider the lets you define custom composed annotations for your specific use cases. For example, consider the
following annotation definitions: following annotation definitions:
[tabs] [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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.beans.factory.annotation; package org.springframework.beans.factory.annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; 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. * Check whether the named bean declares a qualifier of the given name.
* @param qualifier the qualifier to match * @param qualifier the qualifier to match

View File

@ -139,6 +139,14 @@ public @interface Transactional {
* qualifier value (or the bean name) of a specific * qualifier value (or the bean name) of a specific
* {@link org.springframework.transaction.TransactionManager TransactionManager} * {@link org.springframework.transaction.TransactionManager TransactionManager}
* bean definition. * 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 * @since 4.2
* @see #value * @see #value
* @see org.springframework.transaction.PlatformTransactionManager * @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.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.core.CoroutinesUtils; import org.springframework.core.CoroutinesUtils;
import org.springframework.core.KotlinDetector; 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. // If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource(); TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); 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) { if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {
boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method); 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. * 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 @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 // Do not attempt to lookup tx manager if no tx attributes are set
if (txAttr == null || this.beanFactory == null) { if (txAttr == null || this.beanFactory == null) {
return getTransactionManager(); return getTransactionManager();
@ -511,7 +522,20 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
if (StringUtils.hasText(qualifier)) { if (StringUtils.hasText(qualifier)) {
return determineQualifiedTransactionManager(this.beanFactory, 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); return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);
} }
else { 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) { private TransactionManager determineQualifiedTransactionManager(BeanFactory beanFactory, String qualifier) {
TransactionManager txManager = this.transactionManagerCache.get(qualifier); TransactionManager txManager = this.transactionManagerCache.get(qualifier);
if (txManager == null) { if (txManager == null) {
@ -538,7 +572,6 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
return txManager; return txManager;
} }
@Nullable @Nullable
private PlatformTransactionManager asPlatformTransactionManager(@Nullable Object transactionManager) { private PlatformTransactionManager asPlatformTransactionManager(@Nullable Object transactionManager) {
if (transactionManager == null) { if (transactionManager == null) {

View File

@ -23,7 +23,9 @@ import java.util.Properties;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils; 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.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatException; 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; import static org.springframework.transaction.annotation.RollbackOn.ALL_EXCEPTIONS;
/** /**
@ -255,9 +258,34 @@ class EnableTransactionManagementTests {
assertThat(txManager.commits).isEqualTo(2); assertThat(txManager.commits).isEqualTo(2);
assertThat(txManager.rollbacks).isEqualTo(0); assertThat(txManager.rollbacks).isEqualTo(0);
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(bean::findAllFoos);
ctx.close(); 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 @Test
void spr14322FindsOnInterfaceWithInterfaceProxy() { void spr14322FindsOnInterfaceWithInterfaceProxy() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Spr14322ConfigA.class); AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Spr14322ConfigA.class);
@ -352,6 +380,12 @@ class EnableTransactionManagementTests {
} }
@Service
@Qualifier("qualified")
public static class TransactionalTestBeanSubclass extends TransactionalTestBean {
}
@Configuration @Configuration
static class PlaceholderConfig { static class PlaceholderConfig {
@ -535,6 +569,35 @@ class EnableTransactionManagementTests {
public TransactionalTestBean testBean() { public TransactionalTestBean testBean() {
return new TransactionalTestBean(); 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.setTransactionManager(ptm);
ti = SerializationTestUtils.serializeAndDeserialize(ti); ti = SerializationTestUtils.serializeAndDeserialize(ti);
boolean condition3 = ti.getTransactionManager() instanceof SerializableTransactionManager; assertThat(ti.getTransactionManager() instanceof SerializableTransactionManager).isTrue();
assertThat(condition3).isTrue(); assertThat(ti.getTransactionAttributeSource() instanceof CompositeTransactionAttributeSource).isTrue();
boolean condition2 = ti.getTransactionAttributeSource() instanceof CompositeTransactionAttributeSource;
assertThat(condition2).isTrue();
CompositeTransactionAttributeSource ctas = (CompositeTransactionAttributeSource) ti.getTransactionAttributeSource(); CompositeTransactionAttributeSource ctas = (CompositeTransactionAttributeSource) ti.getTransactionAttributeSource();
boolean condition1 = ctas.getTransactionAttributeSources()[0] instanceof NameMatchTransactionAttributeSource; assertThat(ctas.getTransactionAttributeSources()[0] instanceof NameMatchTransactionAttributeSource).isTrue();
assertThat(condition1).isTrue(); assertThat(ctas.getTransactionAttributeSources()[1] instanceof NameMatchTransactionAttributeSource).isTrue();
boolean condition = ctas.getTransactionAttributeSources()[1] instanceof NameMatchTransactionAttributeSource;
assertThat(condition).isTrue();
} }
@Test @Test
@ -132,7 +128,7 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
PlatformTransactionManager transactionManager = mock(); PlatformTransactionManager transactionManager = mock();
TransactionInterceptor ti = transactionInterceptorWithTransactionManager(transactionManager, null); TransactionInterceptor ti = transactionInterceptorWithTransactionManager(transactionManager, null);
assertThat(ti.determineTransactionManager(new DefaultTransactionAttribute())).isSameAs(transactionManager); assertThat(ti.determineTransactionManager(new DefaultTransactionAttribute(), null)).isSameAs(transactionManager);
} }
@Test @Test
@ -140,7 +136,7 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
PlatformTransactionManager transactionManager = mock(); PlatformTransactionManager transactionManager = mock();
TransactionInterceptor ti = transactionInterceptorWithTransactionManager(transactionManager, null); TransactionInterceptor ti = transactionInterceptorWithTransactionManager(transactionManager, null);
assertThat(ti.determineTransactionManager(null)).isSameAs(transactionManager); assertThat(ti.determineTransactionManager(null, null)).isSameAs(transactionManager);
} }
@Test @Test
@ -148,7 +144,7 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
BeanFactory beanFactory = mock(); BeanFactory beanFactory = mock();
TransactionInterceptor ti = simpleTransactionInterceptor(beanFactory); TransactionInterceptor ti = simpleTransactionInterceptor(beanFactory);
assertThat(ti.determineTransactionManager(null)).isNull(); assertThat(ti.determineTransactionManager(null, null)).isNull();
} }
@Test @Test
@ -158,8 +154,8 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setQualifier("fooTransactionManager"); attribute.setQualifier("fooTransactionManager");
assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
ti.determineTransactionManager(attribute)) .isThrownBy(() -> ti.determineTransactionManager(attribute, null))
.withMessageContaining("'fooTransactionManager'"); .withMessageContaining("'fooTransactionManager'");
} }
@ -174,7 +170,7 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setQualifier("fooTransactionManager"); attribute.setQualifier("fooTransactionManager");
assertThat(ti.determineTransactionManager(attribute)).isSameAs(fooTransactionManager); assertThat(ti.determineTransactionManager(attribute, null)).isSameAs(fooTransactionManager);
} }
@Test @Test
@ -189,7 +185,7 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setQualifier("fooTransactionManager"); attribute.setQualifier("fooTransactionManager");
assertThat(ti.determineTransactionManager(attribute)).isSameAs(fooTransactionManager); assertThat(ti.determineTransactionManager(attribute, null)).isSameAs(fooTransactionManager);
} }
@Test @Test
@ -203,7 +199,7 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setQualifier(""); attribute.setQualifier("");
assertThat(ti.determineTransactionManager(attribute)).isSameAs(defaultTransactionManager); assertThat(ti.determineTransactionManager(attribute, null)).isSameAs(defaultTransactionManager);
} }
@Test @Test
@ -215,11 +211,11 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setQualifier("fooTransactionManager"); attribute.setQualifier("fooTransactionManager");
TransactionManager actual = ti.determineTransactionManager(attribute); TransactionManager actual = ti.determineTransactionManager(attribute, null);
assertThat(actual).isSameAs(txManager); assertThat(actual).isSameAs(txManager);
// Call again, should be cached // Call again, should be cached
TransactionManager actual2 = ti.determineTransactionManager(attribute); TransactionManager actual2 = ti.determineTransactionManager(attribute, null);
assertThat(actual2).isSameAs(txManager); assertThat(actual2).isSameAs(txManager);
verify(beanFactory, times(1)).containsBean("fooTransactionManager"); verify(beanFactory, times(1)).containsBean("fooTransactionManager");
verify(beanFactory, times(1)).getBean("fooTransactionManager", TransactionManager.class); verify(beanFactory, times(1)).getBean("fooTransactionManager", TransactionManager.class);
@ -234,11 +230,11 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
PlatformTransactionManager txManager = associateTransactionManager(beanFactory, "fooTransactionManager"); PlatformTransactionManager txManager = associateTransactionManager(beanFactory, "fooTransactionManager");
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
TransactionManager actual = ti.determineTransactionManager(attribute); TransactionManager actual = ti.determineTransactionManager(attribute, null);
assertThat(actual).isSameAs(txManager); assertThat(actual).isSameAs(txManager);
// Call again, should be cached // Call again, should be cached
TransactionManager actual2 = ti.determineTransactionManager(attribute); TransactionManager actual2 = ti.determineTransactionManager(attribute, null);
assertThat(actual2).isSameAs(txManager); assertThat(actual2).isSameAs(txManager);
verify(beanFactory, times(1)).getBean("fooTransactionManager", TransactionManager.class); verify(beanFactory, times(1)).getBean("fooTransactionManager", TransactionManager.class);
} }
@ -252,11 +248,11 @@ class TransactionInterceptorTests extends AbstractTransactionAspectTests {
given(beanFactory.getBean(TransactionManager.class)).willReturn(txManager); given(beanFactory.getBean(TransactionManager.class)).willReturn(txManager);
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute(); DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
TransactionManager actual = ti.determineTransactionManager(attribute); TransactionManager actual = ti.determineTransactionManager(attribute, null);
assertThat(actual).isSameAs(txManager); assertThat(actual).isSameAs(txManager);
// Call again, should be cached // Call again, should be cached
TransactionManager actual2 = ti.determineTransactionManager(attribute); TransactionManager actual2 = ti.determineTransactionManager(attribute, null);
assertThat(actual2).isSameAs(txManager); assertThat(actual2).isSameAs(txManager);
verify(beanFactory, times(1)).getBean(TransactionManager.class); verify(beanFactory, times(1)).getBean(TransactionManager.class);
} }