Skip to content

Commit d5c7a5e

Browse files
committed
Bean overriding by type uses isAutowireCandidate for matching
This commit uses the bean factory `isAutowiredCandidate` method directly in `BeanOverrideBeanFactoryPostProcessor` to select a single match among multiple candidates when matching by type. The expected consequence, in most cases, is that this will delegate to a `@Qualifier`-aware `QualifierAnnotationAutowireCandidateResolver`. In that sense, bean overriding by-type matching is now potentially taking Qualifier annotations or meta-annotations into account. It also changes the way existing bean definitions are checked in case a bean name has been specified: factory beans are now taken into account when checking the type of an existing definition matches the expected bean override type. Closes gh-32822
1 parent b17d1c5 commit d5c7a5e

File tree

12 files changed

+319
-43
lines changed

12 files changed

+319
-43
lines changed

framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc

+7-6
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@ the test's `ApplicationContext` with a Mockito mock or spy, respectively. In the
66
case, the original bean definition is not replaced, but instead an early instance of the
77
bean is captured and wrapped by the spy.
88

9-
Users are encouraged to make bean overriding as explicit and unambiguous as possible,
10-
typically by specifying a bean `name` in the annotation.
11-
If no bean `name` is specified, the annotated field's type is used to search for candidate
12-
definitions to override.
9+
By default, the annotated field's type is used to search for candidate definitions to
10+
override, but note that `@Qualifier` annotations are also taken into account for the
11+
purpose of matching. Users can also make things entirely explicit by specifying a bean
12+
`name` in the annotation.
13+
1314
Each annotation also defines Mockito-specific attributes to fine-tune the mocking details.
1415

1516
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE_DEFINITION`
1617
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
1718

18-
It requires that at most one candidate definition exists if a bean name is specified,
19-
or exactly one if no bean name is specified.
19+
It requires that at most one matching candidate definition exists if a bean name
20+
is specified, or exactly one if no bean name is specified.
2021

2122
The `@MockitoSpyBean` annotation uses the `WRAP_BEAN`
2223
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy],

framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ but the annotation allows for a specific method name to be provided.
1111
The `@TestBean` annotation uses the `REPLACE_DEFINITION`
1212
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
1313

14-
Users are encouraged to make bean overriding as explicit and unambiguous as possible,
15-
typically by specifying a bean `name` in the annotation.
16-
If no bean `name` is specified, the annotated field's type is used to search for candidate
17-
definitions to override. In that case it is required that exactly one definition matches.
14+
By default, the annotated field's type is used to search for candidate definitions to override.
15+
In that case it is required that exactly one definition matches, but note that `@Qualifier`
16+
annotations are also taken into account for the purpose of matching.
17+
Users can also make things entirely explicit by specifying a bean `name` in the annotation.
1818

1919
The following example shows how to fully configure the `@TestBean` annotation, with
2020
explicit values equivalent to the defaults:

framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc

+4-4
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ In contrast to Spring's autowiring mechanism (for example, resolution of an `@Au
6161
field), the bean overriding infrastructure in the TestContext framework has limited
6262
heuristics it can perform to locate a bean. Either the `BeanOverrideProcessor` can compute
6363
the name of the bean to override, or it can be unambiguously selected given the type of
64-
the annotated field.
64+
the annotated field and its qualifying annotations.
65+
66+
Typically, the bean is selected by type by the `BeanOverrideFactoryPostProcessor`.
67+
Alternatively, the user can directly provide the bean name in the custom annotation.
6568
66-
Typically, the user directly provides the bean name in the custom annotation in order to
67-
make things as explicit as possible. Alternatively, the bean is selected by type by the
68-
`BeanOverrideFactoryPostProcessor`.
6969
Some `BeanOverrideProcessor`s could also internally compute a bean name based on a
7070
convention or another advanced method.
7171
====

spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java

+42-21
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.beans.factory.config.BeanDefinition;
3232
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
3333
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
34+
import org.springframework.beans.factory.config.DependencyDescriptor;
3435
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
3536
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
3637
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -127,28 +128,34 @@ private void registerReplaceDefinition(ConfigurableListableBeanFactory beanFacto
127128

128129
RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata);
129130
String beanName = overrideMetadata.getBeanName();
131+
BeanDefinition existingBeanDefinition = null;
130132
if (beanName == null) {
131-
final String[] candidates = beanFactory.getBeanNamesForType(overrideMetadata.getBeanType());
132-
if (candidates.length != 1) {
133+
Set<String> candidates = getExistingBeanNamesByType(beanFactory, overrideMetadata, true);
134+
if (candidates.size() != 1) {
133135
Field f = overrideMetadata.getField();
134136
throw new IllegalStateException("Unable to select a bean definition to override, " +
135-
candidates.length+ " bean definitions found of type " + overrideMetadata.getBeanType() +
137+
candidates.size() + " bean definitions found of type " + overrideMetadata.getBeanType() +
136138
" (as required by annotated field '" + f.getDeclaringClass().getSimpleName() +
137139
"." + f.getName() + "')");
138140
}
139-
beanName = candidates[0];
141+
beanName = candidates.iterator().next();
142+
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
143+
}
144+
else {
145+
Set<String> candidates = getExistingBeanNamesByType(beanFactory, overrideMetadata, false);
146+
if (candidates.contains(beanName)) {
147+
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
148+
}
149+
else if (enforceExistingDefinition) {
150+
throw new IllegalStateException("Unable to override bean '" + beanName + "'; there is no" +
151+
" bean definition to replace with that name of type " + overrideMetadata.getBeanType());
152+
}
140153
}
141154

142-
BeanDefinition existingBeanDefinition = null;
143-
if (beanFactory.containsBeanDefinition(beanName)) {
144-
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
155+
if (existingBeanDefinition != null) {
145156
copyBeanDefinitionDetails(existingBeanDefinition, beanDefinition);
146157
registry.removeBeanDefinition(beanName);
147158
}
148-
else if (enforceExistingDefinition) {
149-
throw new IllegalStateException("Unable to override bean '" + beanName + "'; there is no" +
150-
" bean definition to replace with that name");
151-
}
152159
registry.registerBeanDefinition(beanName, beanDefinition);
153160

154161
Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null);
@@ -171,33 +178,40 @@ else if (enforceExistingDefinition) {
171178
* phase.
172179
*/
173180
private void registerWrapBean(ConfigurableListableBeanFactory beanFactory, OverrideMetadata metadata) {
174-
Set<String> existingBeanNames = getExistingBeanNames(beanFactory, metadata.getBeanType());
175181
String beanName = metadata.getBeanName();
176182
if (beanName == null) {
177-
if (existingBeanNames.size() != 1) {
183+
Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, metadata, true);
184+
if (candidateNames.size() != 1) {
178185
Field f = metadata.getField();
179186
throw new IllegalStateException("Unable to select a bean to override by wrapping, " +
180-
existingBeanNames.size() + " bean instances found of type " + metadata.getBeanType() +
187+
candidateNames.size() + " bean instances found of type " + metadata.getBeanType() +
181188
" (as required by annotated field '" + f.getDeclaringClass().getSimpleName() +
182189
"." + f.getName() + "')");
183190
}
184-
beanName = existingBeanNames.iterator().next();
191+
beanName = candidateNames.iterator().next();
185192
}
186-
else if (!existingBeanNames.contains(beanName)) {
187-
throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping; " +
188-
"there is no existing bean instance with that name of type " + metadata.getBeanType());
193+
else {
194+
Set<String> candidates = getExistingBeanNamesByType(beanFactory, metadata, false);
195+
if (!candidates.contains(beanName)) {
196+
throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping; there is no" +
197+
" existing bean instance with that name of type " + metadata.getBeanType());
198+
}
189199
}
190200
this.overrideRegistrar.markWrapEarly(metadata, beanName);
191201
this.overrideRegistrar.registerNameForMetadata(metadata, beanName);
192202
}
193203

194-
private RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
204+
RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
195205
RootBeanDefinition definition = new RootBeanDefinition();
196206
definition.setTargetType(metadata.getBeanType());
207+
definition.setQualifiedElement(metadata.getField());
197208
return definition;
198209
}
199210

200-
private Set<String> getExistingBeanNames(ConfigurableListableBeanFactory beanFactory, ResolvableType resolvableType) {
211+
private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory, OverrideMetadata metadata,
212+
boolean checkAutowiredCandidate) {
213+
214+
ResolvableType resolvableType = metadata.getBeanType();
201215
Set<String> beans = new LinkedHashSet<>(
202216
Arrays.asList(beanFactory.getBeanNamesForType(resolvableType, true, false)));
203217
Class<?> type = resolvableType.resolve(Object.class);
@@ -209,7 +223,14 @@ private Set<String> getExistingBeanNames(ConfigurableListableBeanFactory beanFac
209223
beans.add(beanName);
210224
}
211225
}
212-
beans.removeIf(ScopedProxyUtils::isScopedTarget);
226+
if (checkAutowiredCandidate) {
227+
DependencyDescriptor descriptor = new DependencyDescriptor(metadata.getField(), true);
228+
beans.removeIf(beanName -> ScopedProxyUtils.isScopedTarget(beanName) ||
229+
!beanFactory.isAutowireCandidate(beanName, descriptor));
230+
}
231+
else {
232+
beans.removeIf(ScopedProxyUtils::isScopedTarget);
233+
}
213234
return beans;
214235
}
215236

spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@
3030
*
3131
* <p>By default, the bean to override is inferred from the type of the
3232
* annotated field. This requires that exactly one matching definition is
33-
* present in the application context. To explicitly specify a bean name to
34-
* replace, set the {@link #value()} or {@link #name()} attribute.
33+
* present in the application context. A {@code @Qualifier} annotation can be
34+
* used to help disambiguate. Alternatively, you can explicitly specify a bean
35+
* name to replace by setting the {@link #value()} or {@link #name()} attribute.
3536
*
3637
* <p>The instance is created from a zero-argument static factory method in the
3738
* test class whose return type is compatible with the annotated field. In the

spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBean.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
*
3333
* <p>If no explicit {@link #name()} is specified, a target bean definition is
3434
* selected according to the class of the annotated field, and there must be
35-
* exactly one such candidate definition in the context.
35+
* exactly one such candidate definition in the context. A {@code @Qualifier}
36+
* annotation can be used to help disambiguate.
3637
* If a {@link #name()} is specified, either the definition exists in the
3738
* application context and is replaced, or it doesn't and a new one is added to
3839
* the context.

spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBean.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
*
3333
* <p>If no explicit {@link #name()} is specified, a target bean is selected
3434
* according to the class of the annotated field, and there must be exactly one
35-
* such candidate bean.
35+
* such candidate bean. A {@code @Qualifier} annotation can be used to help
36+
* disambiguate.
3637
* If a {@link #name()} is specified, it is required that a target bean of that
3738
* name has been previously registered in the application context.
3839
*

spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java

+36-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.test.context.bean.override;
1818

19+
import java.lang.reflect.Field;
1920
import java.util.Map;
2021
import java.util.Set;
2122
import java.util.function.Predicate;
@@ -24,6 +25,7 @@
2425

2526
import org.springframework.beans.BeanWrapper;
2627
import org.springframework.beans.factory.FactoryBean;
28+
import org.springframework.beans.factory.annotation.Qualifier;
2729
import org.springframework.beans.factory.config.BeanDefinition;
2830
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
2931
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
@@ -75,8 +77,8 @@ void cannotReplaceIfNoBeanMatching() {
7577

7678
assertThatIllegalStateException()
7779
.isThrownBy(context::refresh)
78-
.withMessage("Unable to override bean 'explicit'; " +
79-
"there is no bean definition to replace with that name");
80+
.withMessage("Unable to override bean 'explicit'; there is no bean definition " +
81+
"to replace with that name of type org.springframework.test.context.bean.override.example.ExampleService");
8082
}
8183

8284
@Test
@@ -176,6 +178,26 @@ void copyDefinitionPrimaryFallbackAndScope() {
176178
.matches(Predicate.not(BeanDefinition::isPrototype), "!isPrototype");
177179
}
178180

181+
@Test
182+
void createDefinitionShouldSetQualifierElement() {
183+
AnnotationConfigApplicationContext context = createContext(QualifiedBean.class);
184+
context.registerBeanDefinition("singleton", new RootBeanDefinition(String.class, () -> "ORIGINAL"));
185+
context.register(QualifiedBean.class);
186+
187+
assertThatNoException().isThrownBy(context::refresh);
188+
189+
assertThat(context.getBeanDefinition("singleton"))
190+
.isInstanceOfSatisfying(RootBeanDefinition.class, this::isTheValueField);
191+
}
192+
193+
194+
private void isTheValueField(RootBeanDefinition def) {
195+
assertThat(def.getQualifiedElement()).isInstanceOfSatisfying(Field.class, field -> {
196+
assertThat(field.getDeclaringClass()).isEqualTo(QualifiedBean.class);
197+
assertThat(field.getName()).as("annotated field name")
198+
.isEqualTo("value");
199+
});
200+
}
179201

180202
private AnnotationConfigApplicationContext createContext(Class<?>... classes) {
181203
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@@ -260,6 +282,18 @@ static String useThis() {
260282
}
261283
}
262284

285+
static class QualifiedBean {
286+
287+
@Qualifier("preferThis")
288+
@ExampleBeanOverrideAnnotation(beanName = "singleton",
289+
value = "useThis", createIfMissing = false)
290+
private String value;
291+
292+
static String useThis() {
293+
return "USED THIS";
294+
}
295+
}
296+
263297
static class TestFactoryBean implements FactoryBean<Object> {
264298

265299
@Override

spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanByTypeIntegrationTests.java

+50
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
import org.junit.platform.testkit.engine.EngineExecutionResults;
2222
import org.junit.platform.testkit.engine.EngineTestKit;
2323

24+
import org.springframework.beans.factory.annotation.Qualifier;
2425
import org.springframework.context.ApplicationContext;
2526
import org.springframework.context.annotation.Bean;
2627
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.test.context.bean.override.example.CustomQualifier;
2729
import org.springframework.test.context.bean.override.example.ExampleService;
2830
import org.springframework.test.context.bean.override.example.RealExampleService;
2931
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@@ -39,10 +41,26 @@ public class TestBeanByTypeIntegrationTests {
3941
@TestBean
4042
ExampleService anyNameForService;
4143

44+
@TestBean(methodName = "someString")
45+
@Qualifier("prefer")
46+
StringBuilder anyNameForStringBuilder;
47+
48+
@TestBean(methodName = "someString2")
49+
@CustomQualifier
50+
StringBuilder anyNameForStringBuilder2;
51+
4252
static ExampleService anyNameForServiceTestOverride() {
4353
return new RealExampleService("Mocked greeting");
4454
}
4555

56+
static StringBuilder someString() {
57+
return new StringBuilder("Prefer TestBean String");
58+
}
59+
60+
static StringBuilder someString2() {
61+
return new StringBuilder("CustomQualifier TestBean String");
62+
}
63+
4664
@Test
4765
void overrideIsFoundByType(ApplicationContext ctx) {
4866
assertThat(this.anyNameForService)
@@ -52,6 +70,21 @@ void overrideIsFoundByType(ApplicationContext ctx) {
5270
assertThat(this.anyNameForService.greeting()).isEqualTo("Mocked greeting");
5371
}
5472

73+
@Test
74+
void overrideIsFoundByTypeWithQualifierDisambiguation(ApplicationContext ctx) {
75+
assertThat(this.anyNameForStringBuilder)
76+
.as("direct qualifier")
77+
.isSameAs(ctx.getBean("two"))
78+
.hasToString("Prefer TestBean String");
79+
80+
assertThat(this.anyNameForStringBuilder2)
81+
.as("meta qualifier")
82+
.isSameAs(ctx.getBean("three"))
83+
.hasToString("CustomQualifier TestBean String");
84+
85+
assertThat(ctx.getBean("one")).as("no qualifier needed").hasToString("Prod One");
86+
}
87+
5588
@Test
5689
void zeroCandidates() {
5790
Class<?> caseClass = CaseNone.class;
@@ -92,6 +125,23 @@ static class ConfigByType {
92125
ExampleService bean1() {
93126
return new RealExampleService("Production hello");
94127
}
128+
129+
@Bean("one")
130+
StringBuilder beanString1() {
131+
return new StringBuilder("Prod One");
132+
}
133+
134+
@Bean("two")
135+
@Qualifier("prefer")
136+
StringBuilder beanString2() {
137+
return new StringBuilder("Prod Two");
138+
}
139+
140+
@Bean("three")
141+
@CustomQualifier
142+
StringBuilder beanString3() {
143+
return new StringBuilder("Prod Three");
144+
}
95145
}
96146

97147
@SpringJUnitConfig(FailingNone.class)

0 commit comments

Comments
 (0)