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

@@ -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);
}