Skip to content

Commit 14a461e

Browse files
committed
Consider type-level qualifier annotations for transaction manager selection
Closes gh-24291
1 parent 6461eec commit 14a461e

File tree

6 files changed

+165
-31
lines changed

6 files changed

+165
-31
lines changed

framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -548,12 +548,32 @@ transaction managers, differentiated by the `order`, `account`, and `reactive-ac
548548
qualifiers. The default `<tx:annotation-driven>` target bean name, `transactionManager`,
549549
is still used if no specifically qualified `TransactionManager` bean is found.
550550

551+
[TIP]
552+
====
553+
If all transactional methods on the same class share the same qualifier, consider
554+
declaring a type-level `org.springframework.beans.factory.annotation.Qualifier`
555+
annotation instead. If its value matches the qualifier value (or bean name) of a
556+
specific transaction manager, that transaction manager is going to be used for
557+
transaction definitions without a specific qualifier on `@Transactional` itself.
558+
559+
Such a type-level qualifier can be declared on the concrete class, applying to
560+
transaction definitions from a base class as well. This effectively overrides
561+
the default transaction manager choice for any unqualified base class methods.
562+
563+
Last but not least, such a type-level bean qualifier can serve multiple purposes,
564+
e.g. with a value of "order" it can be used for autowiring purposes (identifying
565+
the order repository) as well as transaction manager selection, as long as the
566+
target beans for autowiring as well as the associated transaction manager
567+
definitions declare the same qualifier value. Such a qualifier value only needs
568+
to be unique with a set of type-matching beans, not having to serve as an id.
569+
====
570+
551571
[[tx-custom-attributes]]
552572
== Custom Composed Annotations
553573

554-
If you find you repeatedly use the same attributes with `@Transactional` on many different
555-
methods, xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[Spring's meta-annotation support] lets you
556-
define custom composed annotations for your specific use cases. For example, consider the
574+
If you find you repeatedly use the same attributes with `@Transactional` on many different methods,
575+
xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[Spring's meta-annotation support]
576+
lets you define custom composed annotations for your specific use cases. For example, consider the
557577
following annotation definitions:
558578

559579
[tabs]

spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.beans.factory.annotation;
1818

19+
import java.lang.reflect.AnnotatedElement;
1920
import java.lang.reflect.Method;
2021
import java.util.LinkedHashMap;
2122
import java.util.Map;
@@ -138,6 +139,19 @@ else if (bf.containsBean(qualifier)) {
138139
}
139140
}
140141

142+
/**
143+
* Determine the {@link Qualifier#value() qualifier value} for the given
144+
* annotated element.
145+
* @param annotatedElement the class, method or parameter to introspect
146+
* @return the associated qualifier value, or {@code null} if none
147+
* @since 6.2
148+
*/
149+
@Nullable
150+
public static String getQualifierValue(AnnotatedElement annotatedElement) {
151+
Qualifier qualifier = AnnotationUtils.getAnnotation(annotatedElement, Qualifier.class);
152+
return (qualifier != null ? qualifier.value() : null);
153+
}
154+
141155
/**
142156
* Check whether the named bean declares a qualifier of the given name.
143157
* @param qualifier the qualifier to match

spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@
139139
* qualifier value (or the bean name) of a specific
140140
* {@link org.springframework.transaction.TransactionManager TransactionManager}
141141
* bean definition.
142+
* <p>Alternatively, as of 6.2, a type-level bean qualifier annotation with a
143+
* {@link org.springframework.beans.factory.annotation.Qualifier#value() qualifier value}
144+
* is also taken into account. If it matches the qualifier value (or bean name)
145+
* of a specific transaction manager, that transaction manager is going to be used
146+
* for transaction definitions without a specific qualifier on this attribute here.
147+
* Such a type-level qualifier can be declared on the concrete class, applying
148+
* to transaction definitions from a base class as well, effectively overriding
149+
* the default transaction manager choice for any unqualified base class methods.
142150
* @since 4.2
143151
* @see #value
144152
* @see org.springframework.transaction.PlatformTransactionManager

spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.beans.factory.BeanFactory;
3636
import org.springframework.beans.factory.BeanFactoryAware;
3737
import org.springframework.beans.factory.InitializingBean;
38+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
3839
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
3940
import org.springframework.core.CoroutinesUtils;
4041
import org.springframework.core.KotlinDetector;
@@ -349,7 +350,7 @@ protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targe
349350
// If the transaction attribute is null, the method is non-transactional.
350351
TransactionAttributeSource tas = getTransactionAttributeSource();
351352
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
352-
final TransactionManager tm = determineTransactionManager(txAttr);
353+
final TransactionManager tm = determineTransactionManager(txAttr, targetClass);
353354

354355
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {
355356
boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
@@ -499,9 +500,19 @@ protected void clearTransactionManagerCache() {
499500

500501
/**
501502
* Determine the specific transaction manager to use for the given transaction.
503+
* @param txAttr the current transaction attribute
504+
* @param targetClass the target class that the attribute has been declared on
505+
* @since 6.2
502506
*/
503507
@Nullable
504-
protected TransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
508+
protected TransactionManager determineTransactionManager(
509+
@Nullable TransactionAttribute txAttr, @Nullable Class<?> targetClass) {
510+
511+
TransactionManager tm = determineTransactionManager(txAttr);
512+
if (tm != null) {
513+
return tm;
514+
}
515+
505516
// Do not attempt to lookup tx manager if no tx attributes are set
506517
if (txAttr == null || this.beanFactory == null) {
507518
return getTransactionManager();
@@ -511,7 +522,20 @@ protected TransactionManager determineTransactionManager(@Nullable TransactionAt
511522
if (StringUtils.hasText(qualifier)) {
512523
return determineQualifiedTransactionManager(this.beanFactory, qualifier);
513524
}
514-
else if (StringUtils.hasText(this.transactionManagerBeanName)) {
525+
else if (targetClass != null) {
526+
// Consider type-level qualifier annotations for transaction manager selection
527+
String typeQualifier = BeanFactoryAnnotationUtils.getQualifierValue(targetClass);
528+
if (StringUtils.hasText(typeQualifier)) {
529+
try {
530+
return determineQualifiedTransactionManager(this.beanFactory, typeQualifier);
531+
}
532+
catch (NoSuchBeanDefinitionException ex) {
533+
// Consider type qualifier as optional, proceed with regular resolution below.
534+
}
535+
}
536+
}
537+
538+
if (StringUtils.hasText(this.transactionManagerBeanName)) {
515539
return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);
516540
}
517541
else {
@@ -528,6 +552,16 @@ else if (StringUtils.hasText(this.transactionManagerBeanName)) {
528552
}
529553
}
530554

555+
/**
556+
* Determine the specific transaction manager to use for the given transaction.
557+
* @deprecated as of 6.2, in favor of {@link #determineTransactionManager(TransactionAttribute, Class)}
558+
*/
559+
@Deprecated
560+
@Nullable
561+
protected TransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
562+
return null;
563+
}
564+
531565
private TransactionManager determineQualifiedTransactionManager(BeanFactory beanFactory, String qualifier) {
532566
TransactionManager txManager = this.transactionManagerCache.get(qualifier);
533567
if (txManager == null) {
@@ -538,7 +572,6 @@ private TransactionManager determineQualifiedTransactionManager(BeanFactory bean
538572
return txManager;
539573
}
540574

541-
542575
@Nullable
543576
private PlatformTransactionManager asPlatformTransactionManager(@Nullable Object transactionManager) {
544577
if (transactionManager == null) {

spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
import org.junit.jupiter.api.Test;
2424

2525
import org.springframework.aop.support.AopUtils;
26+
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
2627
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.beans.factory.annotation.Qualifier;
2729
import org.springframework.context.ConfigurableApplicationContext;
2830
import org.springframework.context.annotation.AdviceMode;
2931
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@@ -46,6 +48,7 @@
4648

4749
import static org.assertj.core.api.Assertions.assertThat;
4850
import static org.assertj.core.api.Assertions.assertThatException;
51+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
4952
import static org.springframework.transaction.annotation.RollbackOn.ALL_EXCEPTIONS;
5053

5154
/**
@@ -255,9 +258,34 @@ void spr11915TransactionManagerAsManualSingleton() {
255258
assertThat(txManager.commits).isEqualTo(2);
256259
assertThat(txManager.rollbacks).isEqualTo(0);
257260

261+
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(bean::findAllFoos);
262+
258263
ctx.close();
259264
}
260265

266+
@Test
267+
void gh24291TransactionManagerViaQualifierAnnotation() {
268+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh24291Config.class);
269+
TransactionalTestBean bean = ctx.getBean(TransactionalTestBean.class);
270+
CallCountingTransactionManager txManager = ctx.getBean("qualifiedTransactionManager", CallCountingTransactionManager.class);
271+
272+
bean.saveQualifiedFoo();
273+
assertThat(txManager.begun).isEqualTo(1);
274+
assertThat(txManager.commits).isEqualTo(1);
275+
assertThat(txManager.rollbacks).isEqualTo(0);
276+
277+
bean.saveQualifiedFooWithAttributeAlias();
278+
assertThat(txManager.begun).isEqualTo(2);
279+
assertThat(txManager.commits).isEqualTo(2);
280+
assertThat(txManager.rollbacks).isEqualTo(0);
281+
282+
bean.findAllFoos();
283+
assertThat(txManager.begun).isEqualTo(3);
284+
assertThat(txManager.commits).isEqualTo(3);
285+
assertThat(txManager.rollbacks).isEqualTo(0);
286+
287+
ctx.close();
288+
}
261289
@Test
262290
void spr14322FindsOnInterfaceWithInterfaceProxy() {
263291
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Spr14322ConfigA.class);
@@ -352,6 +380,12 @@ public void saveQualifiedFooWithAttributeAlias() {
352380
}
353381

354382

383+
@Service
384+
@Qualifier("qualified")
385+
public static class TransactionalTestBeanSubclass extends TransactionalTestBean {
386+
}
387+
388+
355389
@Configuration
356390
static class PlaceholderConfig {
357391

@@ -535,6 +569,35 @@ public void initializeApp(ConfigurableApplicationContext applicationContext) {
535569
public TransactionalTestBean testBean() {
536570
return new TransactionalTestBean();
537571
}
572+
573+
@Bean
574+
public CallCountingTransactionManager otherTxManager() {
575+
return new CallCountingTransactionManager();
576+
}
577+
}
578+
579+
580+
@Configuration
581+
@EnableTransactionManagement
582+
@Import(PlaceholderConfig.class)
583+
static class Gh24291Config {
584+
585+
@Autowired
586+
public void initializeApp(ConfigurableApplicationContext applicationContext) {
587+
applicationContext.getBeanFactory().registerSingleton(
588+
"qualifiedTransactionManager", new CallCountingTransactionManager());
589+
applicationContext.getBeanFactory().registerAlias("qualifiedTransactionManager", "qualified");
590+
}
591+
592+
@Bean
593+
public TransactionalTestBeanSubclass testBean() {
594+
return new TransactionalTestBeanSubclass();
595+
}
596+
597+
@Bean
598+
public CallCountingTransactionManager otherTxManager() {
599+
return new CallCountingTransactionManager();
600+
}
538601
}
539602

540603

0 commit comments

Comments
 (0)