Skip to content

Commit 14a461e

Browse files
committedMar 6, 2024·
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
 

Diff for: ‎framework-docs/modules/ROOT/pages/data-access/transaction/declarative/annotations.adoc

+23-3
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]

Diff for: ‎spring-beans/src/main/java/org/springframework/beans/factory/annotation/BeanFactoryAnnotationUtils.java

+15-1
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

Diff for: ‎spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java

+8
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

Diff for: ‎spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java

+37-4
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) {

Diff for: ‎spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java

+63
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

Diff for: ‎spring-tx/src/test/java/org/springframework/transaction/interceptor/TransactionInterceptorTests.java

+19-23
Original file line numberDiff line numberDiff line change
@@ -116,39 +116,35 @@ void serializableWithCompositeSource() throws Exception {
116116
ti.setTransactionManager(ptm);
117117
ti = SerializationTestUtils.serializeAndDeserialize(ti);
118118

119-
boolean condition3 = ti.getTransactionManager() instanceof SerializableTransactionManager;
120-
assertThat(condition3).isTrue();
121-
boolean condition2 = ti.getTransactionAttributeSource() instanceof CompositeTransactionAttributeSource;
122-
assertThat(condition2).isTrue();
119+
assertThat(ti.getTransactionManager() instanceof SerializableTransactionManager).isTrue();
120+
assertThat(ti.getTransactionAttributeSource() instanceof CompositeTransactionAttributeSource).isTrue();
123121
CompositeTransactionAttributeSource ctas = (CompositeTransactionAttributeSource) ti.getTransactionAttributeSource();
124-
boolean condition1 = ctas.getTransactionAttributeSources()[0] instanceof NameMatchTransactionAttributeSource;
125-
assertThat(condition1).isTrue();
126-
boolean condition = ctas.getTransactionAttributeSources()[1] instanceof NameMatchTransactionAttributeSource;
127-
assertThat(condition).isTrue();
122+
assertThat(ctas.getTransactionAttributeSources()[0] instanceof NameMatchTransactionAttributeSource).isTrue();
123+
assertThat(ctas.getTransactionAttributeSources()[1] instanceof NameMatchTransactionAttributeSource).isTrue();
128124
}
129125

130126
@Test
131127
void determineTransactionManagerWithNoBeanFactory() {
132128
PlatformTransactionManager transactionManager = mock();
133129
TransactionInterceptor ti = transactionInterceptorWithTransactionManager(transactionManager, null);
134130

135-
assertThat(ti.determineTransactionManager(new DefaultTransactionAttribute())).isSameAs(transactionManager);
131+
assertThat(ti.determineTransactionManager(new DefaultTransactionAttribute(), null)).isSameAs(transactionManager);
136132
}
137133

138134
@Test
139135
void determineTransactionManagerWithNoBeanFactoryAndNoTransactionAttribute() {
140136
PlatformTransactionManager transactionManager = mock();
141137
TransactionInterceptor ti = transactionInterceptorWithTransactionManager(transactionManager, null);
142138

143-
assertThat(ti.determineTransactionManager(null)).isSameAs(transactionManager);
139+
assertThat(ti.determineTransactionManager(null, null)).isSameAs(transactionManager);
144140
}
145141

146142
@Test
147143
void determineTransactionManagerWithNoTransactionAttribute() {
148144
BeanFactory beanFactory = mock();
149145
TransactionInterceptor ti = simpleTransactionInterceptor(beanFactory);
150146

151-
assertThat(ti.determineTransactionManager(null)).isNull();
147+
assertThat(ti.determineTransactionManager(null, null)).isNull();
152148
}
153149

154150
@Test
@@ -158,9 +154,9 @@ void determineTransactionManagerWithQualifierUnknown() {
158154
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
159155
attribute.setQualifier("fooTransactionManager");
160156

161-
assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() ->
162-
ti.determineTransactionManager(attribute))
163-
.withMessageContaining("'fooTransactionManager'");
157+
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
158+
.isThrownBy(() -> ti.determineTransactionManager(attribute, null))
159+
.withMessageContaining("'fooTransactionManager'");
164160
}
165161

166162
@Test
@@ -174,7 +170,7 @@ void determineTransactionManagerWithQualifierAndDefault() {
174170
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
175171
attribute.setQualifier("fooTransactionManager");
176172

177-
assertThat(ti.determineTransactionManager(attribute)).isSameAs(fooTransactionManager);
173+
assertThat(ti.determineTransactionManager(attribute, null)).isSameAs(fooTransactionManager);
178174
}
179175

180176
@Test
@@ -189,7 +185,7 @@ void determineTransactionManagerWithQualifierAndDefaultName() {
189185
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
190186
attribute.setQualifier("fooTransactionManager");
191187

192-
assertThat(ti.determineTransactionManager(attribute)).isSameAs(fooTransactionManager);
188+
assertThat(ti.determineTransactionManager(attribute, null)).isSameAs(fooTransactionManager);
193189
}
194190

195191
@Test
@@ -203,7 +199,7 @@ void determineTransactionManagerWithEmptyQualifierAndDefaultName() {
203199
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
204200
attribute.setQualifier("");
205201

206-
assertThat(ti.determineTransactionManager(attribute)).isSameAs(defaultTransactionManager);
202+
assertThat(ti.determineTransactionManager(attribute, null)).isSameAs(defaultTransactionManager);
207203
}
208204

209205
@Test
@@ -215,11 +211,11 @@ void determineTransactionManagerWithQualifierSeveralTimes() {
215211

216212
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
217213
attribute.setQualifier("fooTransactionManager");
218-
TransactionManager actual = ti.determineTransactionManager(attribute);
214+
TransactionManager actual = ti.determineTransactionManager(attribute, null);
219215
assertThat(actual).isSameAs(txManager);
220216

221217
// Call again, should be cached
222-
TransactionManager actual2 = ti.determineTransactionManager(attribute);
218+
TransactionManager actual2 = ti.determineTransactionManager(attribute, null);
223219
assertThat(actual2).isSameAs(txManager);
224220
verify(beanFactory, times(1)).containsBean("fooTransactionManager");
225221
verify(beanFactory, times(1)).getBean("fooTransactionManager", TransactionManager.class);
@@ -234,11 +230,11 @@ void determineTransactionManagerWithBeanNameSeveralTimes() {
234230
PlatformTransactionManager txManager = associateTransactionManager(beanFactory, "fooTransactionManager");
235231

236232
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
237-
TransactionManager actual = ti.determineTransactionManager(attribute);
233+
TransactionManager actual = ti.determineTransactionManager(attribute, null);
238234
assertThat(actual).isSameAs(txManager);
239235

240236
// Call again, should be cached
241-
TransactionManager actual2 = ti.determineTransactionManager(attribute);
237+
TransactionManager actual2 = ti.determineTransactionManager(attribute, null);
242238
assertThat(actual2).isSameAs(txManager);
243239
verify(beanFactory, times(1)).getBean("fooTransactionManager", TransactionManager.class);
244240
}
@@ -252,11 +248,11 @@ void determineTransactionManagerDefaultSeveralTimes() {
252248
given(beanFactory.getBean(TransactionManager.class)).willReturn(txManager);
253249

254250
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
255-
TransactionManager actual = ti.determineTransactionManager(attribute);
251+
TransactionManager actual = ti.determineTransactionManager(attribute, null);
256252
assertThat(actual).isSameAs(txManager);
257253

258254
// Call again, should be cached
259-
TransactionManager actual2 = ti.determineTransactionManager(attribute);
255+
TransactionManager actual2 = ti.determineTransactionManager(attribute, null);
260256
assertThat(actual2).isSameAs(txManager);
261257
verify(beanFactory, times(1)).getBean(TransactionManager.class);
262258
}

0 commit comments

Comments
 (0)
Please sign in to comment.