diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc index 56f421e47e..c00d3d0725 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc @@ -548,12 +548,32 @@ transaction managers, differentiated by the `order`, `account`, and `reactive-ac qualifiers. The default `` 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] diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java index fd41a5cfe0..4036b74f59 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java @@ -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 diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java index a854e1dfe5..4549186f54 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java @@ -139,6 +139,14 @@ public @interface Transactional { * qualifier value (or the bean name) of a specific * {@link org.springframework.transaction.TransactionManager TransactionManager} * bean definition. + *

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 diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java index 08e39499b2..7d8f1c476f 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java @@ -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) { diff --git a/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java b/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java index 49ac8a30d3..5616b945ee 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java @@ -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(); + } } diff --git a/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionInterceptorTests.java b/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionInterceptorTests.java index 6cacda657e..adea2bb54a 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionInterceptorTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionInterceptorTests.java @@ -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); }