Skip to content

Commit 2d33aac

Browse files
committed
Improve Bean Overriding support, testing and documentation
This commit improves on the bean overriding feature in several ways: the API is simplified and polished (metadata and processor contracts, etc...). The commit also reworks infrastructure classes (context customizer, test execution listener, BeanOverrideBeanFactoryPostProcessor, etc...). Parsing of annotations is now fully stateless. In order to avoid OverrideMetadata in bean definition and to make a first step towards AOT support, the BeanOverrideBeanFactoryPostProcessor now delegates to a BeanOverrideRegistrar to track classes to parse, the metadata-related state as well as for the field injection methods for tests. Lastly, this commit increases the test coverage for the provided annotations and adds integration tests and fixes a few `@TestBean` issues.
1 parent 711ddd1 commit 2d33aac

30 files changed

+1710
-816
lines changed

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

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ Java::
4747
<2> The result of this static method will be used as the instance and injected into the field
4848
======
4949

50+
NOTE: The method to invoke is searched in the test class and any enclosing class it might
51+
have, as well as its hierarchy. This typically allows nested test class to provide the
52+
method to use in the root test class.
5053

5154
[[spring-testing-annotation-beanoverriding-mockitobean]]
5255
== `@MockitoBean` and `@MockitoSpyBean`
@@ -100,32 +103,31 @@ and associated infrastructure, which allows to define custom bean overriding var
100103

101104
In order to provide an extension, three classes are needed:
102105

103-
- A concrete `BeanOverrideProcessor<P>`.
104-
- A concrete `OverrideMetadata` created by said processor.
106+
- A concrete `BeanOverrideProcessor` implementation, `P`.
107+
- One or more concrete `OverrideMetadata` implementations created by said processor.
105108
- An annotation meta-annotated with `@BeanOverride(P.class)`.
106109

107110
The Spring TestContext Framework includes infrastructure classes that support bean
108-
overriding: a `BeanPostProcessor`, a `TestExecutionListener` and a `ContextCustomizerFactory`.
109-
These are automatically registered via the Spring TestContext Framework `spring.factories`
110-
file.
111+
overriding: a `BeanFactoryPostProcessor`, a `TestExecutionListener` and a `ContextCustomizerFactory`.
112+
The later two are automatically registered via the Spring TestContext Framework
113+
`spring.factories` file, and are responsible for setting up the rest of the infrastructure.
111114

112115
The test classes are parsed looking for any field meta-annotated with `@BeanOverride`,
113116
instantiating the relevant `BeanOverrideProcessor` in order to register an `OverrideMetadata`.
114117

115-
Then the `BeanOverrideBeanPostProcessor` will use that information to alter the Context,
116-
registering and replacing bean definitions as influenced by each metadata
118+
Then the `BeanOverrideBeanFactoryPostProcessor` will use that information to alter the
119+
Context, registering and replacing bean definitions as influenced by each metadata
117120
`BeanOverrideStrategy`:
118121

119122
- `REPLACE_DEFINITION`: the bean post-processor replaces the bean definition.
120123
If it is not present in the context, an exception is thrown.
121124
- `CREATE_OR_REPLACE_DEFINITION`: same as above but if the bean definition is not present
122125
in the context, one is created
123-
- `WRAP_EARLY_BEAN`: an original instance is obtained via
124-
`SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String)` and
125-
provided to the processor during `OverrideMetadata` creation.
126-
127-
NOTE: The Bean Overriding infrastructure works best with singleton beans. It also doesn't
128-
include any bean resolution (unlike e.g. an `@Autowired`-annotated field). As such, the
129-
name of the bean to override MUST be somehow provided to or computed by the
130-
`BeanOverrideProcessor`. Typically, the end user provides the name as part of the custom
131-
annotation's attributes, or the annotated field's name.
126+
- `WRAP_EARLY_BEAN`: an original instance is obtained and passed to the `OverrideMetadata`
127+
when the override instance is created.
128+
129+
NOTE: The Bean Overriding infrastructure doesn't include any bean resolution step
130+
(unlike e.g. an `@Autowired`-annotated field). As such, the name of the bean to override
131+
MUST be somehow provided to or computed by the `BeanOverrideProcessor`. Typically, the end
132+
user provides the name as part of the custom annotation's attributes, or the annotated
133+
field's name.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
*
3535
* @author Simon Baslé
3636
* @since 6.2
37-
* @see BeanOverrideBeanPostProcessor
37+
* @see BeanOverrideBeanFactoryPostProcessor
3838
*/
3939
@Retention(RetentionPolicy.RUNTIME)
4040
@Target(ElementType.ANNOTATION_TYPE)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.bean.override;
18+
19+
import java.lang.reflect.Field;
20+
import java.util.Arrays;
21+
import java.util.LinkedHashSet;
22+
import java.util.Map;
23+
import java.util.Set;
24+
import java.util.concurrent.ConcurrentHashMap;
25+
import java.util.function.Consumer;
26+
27+
import org.springframework.aop.scope.ScopedProxyUtils;
28+
import org.springframework.beans.BeansException;
29+
import org.springframework.beans.factory.BeanFactoryUtils;
30+
import org.springframework.beans.factory.FactoryBean;
31+
import org.springframework.beans.factory.config.BeanDefinition;
32+
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
33+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
34+
import org.springframework.beans.factory.config.ConstructorArgumentValues;
35+
import org.springframework.beans.factory.config.RuntimeBeanReference;
36+
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
37+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
38+
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
39+
import org.springframework.beans.factory.support.RootBeanDefinition;
40+
import org.springframework.core.Ordered;
41+
import org.springframework.core.PriorityOrdered;
42+
import org.springframework.core.ResolvableType;
43+
import org.springframework.util.Assert;
44+
import org.springframework.util.StringUtils;
45+
46+
/**
47+
* A {@link BeanFactoryPostProcessor} implementation that processes test classes
48+
* and adapt the {@link BeanDefinitionRegistry} for any {@link BeanOverride} it
49+
* may define.
50+
*
51+
* <p>A set of classes from which to parse {@link OverrideMetadata} must be
52+
* provided to this processor. Each test class is expected to use any
53+
* annotation meta-annotated with {@link BeanOverride @BeanOverride} to mark
54+
* beans to override. The {@link BeanOverrideParsingUtils#hasBeanOverride(Class)}
55+
* method can be used to check if a class matches the above criteria.
56+
*
57+
* <p>The provided classes are fully parsed at creation to build a metadata set.
58+
* This processor implements several {@link BeanOverrideStrategy overriding
59+
* strategy} and chooses the correct one according to each override metadata
60+
* {@link OverrideMetadata#getStrategy()} method. Additionally, it provides
61+
* support for injecting the overridden bean instances into their corresponding
62+
* annotated {@link Field fields}.
63+
*
64+
* @author Simon Baslé
65+
* @since 6.2
66+
*/
67+
class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {
68+
69+
private static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideBeanFactoryPostProcessor.class.getName();
70+
71+
private static final String EARLY_INFRASTRUCTURE_BEAN_NAME =
72+
BeanOverrideBeanFactoryPostProcessor.WrapEarlyBeanPostProcessor.class.getName();
73+
74+
private final BeanOverrideRegistrar overrideRegistrar;
75+
76+
77+
/**
78+
* Create a new {@code BeanOverrideBeanFactoryPostProcessor} instance with
79+
* the given {@link BeanOverrideRegistrar}, which contains a set of parsed
80+
* {@link OverrideMetadata}.
81+
* @param overrideRegistrar the {@link BeanOverrideRegistrar} used to track
82+
* metadata
83+
*/
84+
public BeanOverrideBeanFactoryPostProcessor(BeanOverrideRegistrar overrideRegistrar) {
85+
this.overrideRegistrar = overrideRegistrar;
86+
}
87+
88+
89+
@Override
90+
public int getOrder() {
91+
return Ordered.LOWEST_PRECEDENCE - 10;
92+
}
93+
94+
95+
@Override
96+
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
97+
Assert.isInstanceOf(DefaultListableBeanFactory.class, beanFactory,
98+
"Bean overriding annotations can only be used on a DefaultListableBeanFactory");
99+
postProcessWithRegistry((DefaultListableBeanFactory) beanFactory);
100+
}
101+
102+
private void postProcessWithRegistry(DefaultListableBeanFactory registry) {
103+
for (OverrideMetadata metadata : this.overrideRegistrar.getOverrideMetadata()) {
104+
registerBeanOverride(registry, metadata);
105+
}
106+
}
107+
108+
/**
109+
* Copy certain details of a {@link BeanDefinition} to the definition created by
110+
* this processor for a given {@link OverrideMetadata}.
111+
* <p>The default implementation copies the {@linkplain BeanDefinition#isPrimary()
112+
* primary flag} and the {@linkplain BeanDefinition#getScope() scope}.
113+
*/
114+
protected void copyBeanDefinitionDetails(BeanDefinition from, RootBeanDefinition to) {
115+
to.setPrimary(from.isPrimary());
116+
to.setScope(from.getScope());
117+
}
118+
119+
private void registerBeanOverride(DefaultListableBeanFactory beanFactory, OverrideMetadata overrideMetadata) {
120+
switch (overrideMetadata.getStrategy()) {
121+
case REPLACE_DEFINITION -> registerReplaceDefinition(beanFactory, overrideMetadata, true);
122+
case REPLACE_OR_CREATE_DEFINITION -> registerReplaceDefinition(beanFactory, overrideMetadata, false);
123+
case WRAP_BEAN -> registerWrapBean(beanFactory, overrideMetadata);
124+
}
125+
}
126+
127+
private void registerReplaceDefinition(DefaultListableBeanFactory beanFactory, OverrideMetadata overrideMetadata, boolean enforceExistingDefinition) {
128+
129+
RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata);
130+
String beanName = overrideMetadata.getBeanName();
131+
132+
BeanDefinition existingBeanDefinition = null;
133+
if (beanFactory.containsBeanDefinition(beanName)) {
134+
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
135+
copyBeanDefinitionDetails(existingBeanDefinition, beanDefinition);
136+
beanFactory.removeBeanDefinition(beanName);
137+
}
138+
else if (enforceExistingDefinition) {
139+
throw new IllegalStateException("Unable to override bean '" + beanName + "'; there is no" +
140+
" bean definition to replace with that name");
141+
}
142+
beanFactory.registerBeanDefinition(beanName, beanDefinition);
143+
144+
Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null);
145+
if (beanFactory.isSingleton(beanName)) {
146+
// Now we have an instance (the override) that we can register.
147+
// At this stage we don't expect a singleton instance to be present,
148+
// and this call will throw if there is such an instance already.
149+
beanFactory.registerSingleton(beanName, override);
150+
}
151+
152+
overrideMetadata.track(override, beanFactory);
153+
this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanName);
154+
}
155+
156+
/**
157+
* Check that the expected bean name is registered and matches the type to override.
158+
* <p>If so, put the override metadata in the early tracking map.
159+
* <p>The map will later be checked to see if a given bean should be wrapped
160+
* upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)}
161+
* phase.
162+
*/
163+
private void registerWrapBean(DefaultListableBeanFactory beanFactory, OverrideMetadata metadata) {
164+
Set<String> existingBeanNames = getExistingBeanNames(beanFactory, metadata.getBeanType());
165+
String beanName = metadata.getBeanName();
166+
if (!existingBeanNames.contains(beanName)) {
167+
throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping," +
168+
" no existing bean instance by this name of type " + metadata.getBeanType());
169+
}
170+
this.overrideRegistrar.markWrapEarly(metadata, beanName);
171+
this.overrideRegistrar.registerNameForMetadata(metadata, beanName);
172+
}
173+
174+
private RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
175+
RootBeanDefinition definition = new RootBeanDefinition();
176+
definition.setTargetType(metadata.getBeanType());
177+
return definition;
178+
}
179+
180+
private Set<String> getExistingBeanNames(DefaultListableBeanFactory beanFactory, ResolvableType resolvableType) {
181+
Set<String> beans = new LinkedHashSet<>(
182+
Arrays.asList(beanFactory.getBeanNamesForType(resolvableType, true, false)));
183+
Class<?> type = resolvableType.resolve(Object.class);
184+
for (String beanName : beanFactory.getBeanNamesForType(FactoryBean.class, true, false)) {
185+
beanName = BeanFactoryUtils.transformedBeanName(beanName);
186+
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
187+
Object attribute = beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE);
188+
if (resolvableType.equals(attribute) || type.equals(attribute)) {
189+
beans.add(beanName);
190+
}
191+
}
192+
beans.removeIf(ScopedProxyUtils::isScopedTarget);
193+
return beans;
194+
}
195+
196+
/**
197+
* Register a {@link BeanOverrideBeanFactoryPostProcessor} with a {@link BeanDefinitionRegistry}.
198+
* <p>Not required when using the Spring TestContext Framework, as registration
199+
* is automatic via the
200+
* {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader}
201+
* mechanism.
202+
* @param registry the bean definition registry
203+
*/
204+
public static void register(BeanDefinitionRegistry registry) {
205+
RuntimeBeanReference registrarReference = new RuntimeBeanReference(BeanOverrideRegistrar.INFRASTRUCTURE_BEAN_NAME);
206+
// Early processor
207+
addInfrastructureBeanDefinition(
208+
registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
209+
constructorArgs.addIndexedArgumentValue(0, registrarReference));
210+
211+
// Main processor
212+
addInfrastructureBeanDefinition(
213+
registry, BeanOverrideBeanFactoryPostProcessor.class, INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
214+
constructorArgs.addIndexedArgumentValue(0, registrarReference));
215+
}
216+
217+
private static void addInfrastructureBeanDefinition(BeanDefinitionRegistry registry,
218+
Class<?> clazz, String beanName, Consumer<ConstructorArgumentValues> constructorArgumentsConsumer) {
219+
220+
if (!registry.containsBeanDefinition(beanName)) {
221+
RootBeanDefinition definition = new RootBeanDefinition(clazz);
222+
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
223+
ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues();
224+
constructorArgumentsConsumer.accept(constructorArguments);
225+
registry.registerBeanDefinition(beanName, definition);
226+
}
227+
}
228+
229+
230+
static final class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor,
231+
PriorityOrdered {
232+
233+
private final BeanOverrideRegistrar overrideRegistrar;
234+
235+
private final Map<String, Object> earlyReferences;
236+
237+
238+
private WrapEarlyBeanPostProcessor(BeanOverrideRegistrar registrar) {
239+
this.overrideRegistrar = registrar;
240+
this.earlyReferences = new ConcurrentHashMap<>(16);
241+
}
242+
243+
244+
@Override
245+
public int getOrder() {
246+
return Ordered.HIGHEST_PRECEDENCE;
247+
}
248+
249+
@Override
250+
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
251+
if (bean instanceof FactoryBean) {
252+
return bean;
253+
}
254+
this.earlyReferences.put(getCacheKey(bean, beanName), bean);
255+
return this.overrideRegistrar.wrapIfNecessary(bean, beanName);
256+
}
257+
258+
@Override
259+
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
260+
if (bean instanceof FactoryBean) {
261+
return bean;
262+
}
263+
if (this.earlyReferences.remove(getCacheKey(bean, beanName)) != bean) {
264+
return this.overrideRegistrar.wrapIfNecessary(bean, beanName);
265+
}
266+
return bean;
267+
}
268+
269+
private String getCacheKey(Object bean, String beanName) {
270+
return (StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName());
271+
}
272+
273+
}
274+
275+
}

0 commit comments

Comments
 (0)