Skip to content

Commit d79258a

Browse files
committed
Reject non-singletons in Test Bean Override support
Prior to this commit, a non-singleton FactoryBean was silently replaced by a singleton bean. In addition, bean definitions for prototype-scoped and custom-scoped beans were replaced by singleton bean definitions that were incapable of creating the desired bean instance. For example, if the bean type of the original bean definition was a concrete class, an attempt was made to invoke the default constructor which either succeeded with undesirable results or failed with an exception if the bean type did not have a default constructor. If the bean type of the original bean definition was an interface or a FactoryBean that claimed to create a bean of a certain interface type, an attempt was made to instantiate the interface which always failed with a BeanCreationException. To address the aforementioned issues, this commit reworks the logic in BeanOverrideBeanFactoryPostProcessor so that an exception is thrown whenever an attempt is made to override a non-singleton bean. Closes gh-33602
1 parent 4e9b503 commit d79258a

File tree

10 files changed

+70
-83
lines changed

10 files changed

+70
-83
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overrid
3636
and the original instance is wrapped in a Mockito spy. This strategy requires that
3737
exactly one candidate bean definition exists.
3838

39+
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
40+
bean will result in an exception.
41+
3942
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
4043

4144
[tabs]

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

+3
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,6 @@ Alternatively, a factory method in an external class can be referenced via its
8787
fully-qualified method name following the syntax `<fully-qualified class name>#<method name>`
8888
– for example, `methodName = "org.example.TestUtils#createCustomService"`.
8989
====
90+
91+
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
92+
bean will result in an exception.

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

+3
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,6 @@ Alternatively, the user can directly provide the bean name in the custom annotat
6969
Some `BeanOverrideProcessor` implementations could also internally compute a bean name
7070
based on a convention or another advanced method.
7171
====
72+
73+
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
74+
bean will result in an exception.

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

+26-17
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.core.Ordered;
4141
import org.springframework.core.PriorityOrdered;
4242
import org.springframework.core.ResolvableType;
43+
import org.springframework.util.Assert;
4344
import org.springframework.util.StringUtils;
4445

4546
/**
@@ -116,14 +117,15 @@ private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, B
116117
private void replaceDefinition(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry,
117118
OverrideMetadata overrideMetadata, boolean enforceExistingDefinition) {
118119

119-
// The following is a "dummy" bean definition which should not be used to
120+
// The following is a "pseudo" bean definition which MUST NOT be used to
120121
// create an actual bean instance.
121-
RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata);
122+
RootBeanDefinition pseudoBeanDefinition = createPseudoBeanDefinition(overrideMetadata);
122123
String beanName = overrideMetadata.getBeanName();
123124
String beanNameIncludingFactory;
124125
BeanDefinition existingBeanDefinition = null;
125126
if (beanName == null) {
126-
beanNameIncludingFactory = getBeanNameForType(beanFactory, registry, overrideMetadata, beanDefinition, enforceExistingDefinition);
127+
beanNameIncludingFactory = getBeanNameForType(
128+
beanFactory, registry, overrideMetadata, pseudoBeanDefinition, enforceExistingDefinition);
127129
beanName = BeanFactoryUtils.transformedBeanName(beanNameIncludingFactory);
128130
if (registry.containsBeanDefinition(beanName)) {
129131
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
@@ -145,25 +147,24 @@ else if (enforceExistingDefinition) {
145147

146148
// Process existing bean definition.
147149
if (existingBeanDefinition != null) {
148-
copyBeanDefinitionProperties(existingBeanDefinition, beanDefinition);
150+
validateBeanDefinition(beanFactory, beanName);
151+
copyBeanDefinitionProperties(existingBeanDefinition, pseudoBeanDefinition);
149152
registry.removeBeanDefinition(beanName);
150153
}
151154

152155
// At this point, we either removed an existing bean definition above, or
153-
// there was no bean definition to begin with. So, we register the dummy bean
156+
// there was no bean definition to begin with. So, we register the pseudo bean
154157
// definition to ensure that a bean definition exists for the given bean name.
155-
registry.registerBeanDefinition(beanName, beanDefinition);
158+
registry.registerBeanDefinition(beanName, pseudoBeanDefinition);
156159

157160
Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null);
158161
overrideMetadata.track(override, beanFactory);
159162
this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanNameIncludingFactory);
160163

161-
if (beanFactory.isSingleton(beanNameIncludingFactory)) {
162-
// Now we have an instance (the override) that we can register. At this
163-
// stage we don't expect a singleton instance to be present, and this call
164-
// will throw an exception if there is such an instance already.
165-
beanFactory.registerSingleton(beanName, override);
166-
}
164+
// Now we have an instance (the override) that we can register. At this stage, we don't
165+
// expect a singleton instance to be present. If for some reason a singleton instance
166+
// already exists, the following will throw an exception.
167+
beanFactory.registerSingleton(beanName, override);
167168
}
168169

169170
/**
@@ -196,6 +197,7 @@ private void wrapBean(ConfigurableListableBeanFactory beanFactory, OverrideMetad
196197
.formatted(beanName, overrideMetadata.getBeanType()));
197198
}
198199
}
200+
validateBeanDefinition(beanFactory, beanName);
199201
this.overrideRegistrar.markWrapEarly(overrideMetadata, beanName);
200202
this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanName);
201203
}
@@ -276,23 +278,30 @@ private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory b
276278
* definition metadata available in the {@link BeanFactory} &mdash; for example,
277279
* for autowiring candidate resolution.
278280
*/
279-
private static RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
281+
private static RootBeanDefinition createPseudoBeanDefinition(OverrideMetadata metadata) {
280282
RootBeanDefinition definition = new RootBeanDefinition(metadata.getBeanType().resolve());
281283
definition.setTargetType(metadata.getBeanType());
282284
definition.setQualifiedElement(metadata.getField());
283285
return definition;
284286
}
285287

288+
/**
289+
* Validate that the {@link BeanDefinition} for the supplied bean name is suitable
290+
* for being replaced by a bean override.
291+
*/
292+
private static void validateBeanDefinition(ConfigurableListableBeanFactory beanFactory, String beanName) {
293+
Assert.state(beanFactory.isSingleton(beanName),
294+
() -> "Unable to override bean '" + beanName + "': only singleton beans can be overridden.");
295+
}
296+
286297
/**
287298
* Copy the following properties of the source {@link BeanDefinition} to the
288-
* target: the {@linkplain BeanDefinition#isPrimary() primary flag}, the
289-
* {@linkplain BeanDefinition#isFallback() fallback flag}, and the
290-
* {@linkplain BeanDefinition#getScope() scope}.
299+
* target: the {@linkplain BeanDefinition#isPrimary() primary flag} and the
300+
* {@linkplain BeanDefinition#isFallback() fallback flag}.
291301
*/
292302
private static void copyBeanDefinitionProperties(BeanDefinition source, RootBeanDefinition target) {
293303
target.setPrimary(source.isPrimary());
294304
target.setFallback(source.isFallback());
295-
target.setScope(source.getScope());
296305
}
297306

298307

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

+3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
* instance creation} &mdash; for example, based on further processing of the
5050
* annotation or the annotated field.
5151
*
52+
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
53+
* Any attempt to override a non-singleton bean will result in an exception.
54+
*
5255
* @author Simon Baslé
5356
* @author Stephane Nicoll
5457
* @author Sam Brannen

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

+3
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@
9393
* }
9494
* }</code></pre>
9595
*
96+
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
97+
* Any attempt to override a non-singleton bean will result in an exception.
98+
*
9699
* @author Simon Baslé
97100
* @author Stephane Nicoll
98101
* @author Sam Brannen

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

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
* registered directly}) will not be found, and a mocked bean will be added to
4848
* the context alongside the existing dependency.
4949
*
50+
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
51+
* Any attempt to override a non-singleton bean will result in an exception.
52+
*
5053
* @author Simon Baslé
5154
* @since 6.2
5255
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean

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

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
* {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object)
4343
* registered directly}) will not be found.
4444
*
45+
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
46+
* Any attempt to override a non-singleton bean will result in an exception.
47+
*
4548
* @author Simon Baslé
4649
* @since 6.2
4750
* @see org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean

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

+23-59
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import org.junit.jupiter.api.Test;
2626

2727
import org.springframework.beans.BeanWrapper;
28-
import org.springframework.beans.factory.BeanCreationException;
2928
import org.springframework.beans.factory.FactoryBean;
3029
import org.springframework.beans.factory.annotation.Qualifier;
3130
import org.springframework.beans.factory.config.BeanDefinition;
@@ -43,7 +42,6 @@
4342
import org.springframework.util.Assert;
4443

4544
import static org.assertj.core.api.Assertions.assertThat;
46-
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
4745
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
4846
import static org.assertj.core.api.Assertions.assertThatNoException;
4947
import static org.mockito.Mockito.mock;
@@ -228,17 +226,15 @@ void replaceBeanByNameWithMatchingBeanDefinitionForClassBasedSingletonFactoryBea
228226
}
229227

230228
@Test
231-
void replaceBeanByNameWithMatchingBeanDefinitionForClassBasedNonSingletonFactoryBean() {
229+
void replaceBeanByNameWithMatchingBeanDefinitionForClassBasedNonSingletonFactoryBeanFails() {
232230
String beanName = "descriptionBean";
233231
AnnotationConfigApplicationContext context = createContext(CaseByName.class);
234232
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(NonSingletonStringFactoryBean.class);
235233
context.registerBeanDefinition(beanName, factoryBeanDefinition);
236234

237-
assertThatNoException().isThrownBy(context::refresh);
238-
// Even though the FactoryBean signals it does not manage a singleton,
239-
// the Bean Override support currently replaces it with a singleton.
240-
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
241-
assertThat(context.getBean(beanName)).isEqualTo("overridden");
235+
assertThatIllegalStateException()
236+
.isThrownBy(context::refresh)
237+
.withMessage("Unable to override bean 'descriptionBean': only singleton beans can be overridden.");
242238
}
243239

244240
@Test
@@ -254,44 +250,33 @@ void replaceBeanByNameWithMatchingBeanDefinitionForInterfaceBasedSingletonFactor
254250
}
255251

256252
@Test
257-
void replaceBeanByNameWithMatchingBeanDefinitionForInterfaceBasedNonSingletonFactoryBean() {
253+
void replaceBeanByNameWithMatchingBeanDefinitionForInterfaceBasedNonSingletonFactoryBeanFails() {
258254
String beanName = "messageServiceBean";
259255
AnnotationConfigApplicationContext context = createContext(MessageServiceTestCase.class);
260256
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(NonSingletonMessageServiceFactoryBean.class);
261257
context.registerBeanDefinition(beanName, factoryBeanDefinition);
262258

263-
assertThatNoException().isThrownBy(context::refresh);
264-
// Even though the FactoryBean signals it does not manage a singleton,
265-
// the Bean Override support currently replaces it with a singleton.
266-
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
267-
assertThat(context.getBean(beanName, MessageService.class).getMessage()).isEqualTo("overridden");
259+
assertThatIllegalStateException()
260+
.isThrownBy(context::refresh)
261+
.withMessage("Unable to override bean 'messageServiceBean': only singleton beans can be overridden.");
268262
}
269263

270264
@Test
271-
void replaceBeanByNameWithMatchingBeanDefinitionWithPrototypeScope() {
265+
void replaceBeanByNameWithMatchingBeanDefinitionWithPrototypeScopeFails() {
272266
String beanName = "descriptionBean";
273267

274268
AnnotationConfigApplicationContext context = createContext(CaseByName.class);
275269
RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL");
276270
definition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
277271
context.registerBeanDefinition(beanName, definition);
278272

279-
assertThatNoException().isThrownBy(context::refresh);
280-
// The Bean Override support currently creates a "dummy" BeanDefinition that
281-
// retains the prototype scope of the original BeanDefinition.
282-
assertThat(context.isSingleton(beanName)).as("isSingleton").isFalse();
283-
assertThat(context.isPrototype(beanName)).as("isPrototype").isTrue();
284-
// Since the "dummy" BeanDefinition has prototype scope, a manual singleton
285-
// is not registered, and the "dummy" BeanDefinition is used to create a
286-
// new java.lang.String using the default constructor, which results in an
287-
// empty string instead of "overridden". In other words, the bean is not
288-
// actually overridden as expected, and no exception is thrown which
289-
// silently masks the issue.
290-
assertThat(context.getBean(beanName)).isEqualTo("");
273+
assertThatIllegalStateException()
274+
.isThrownBy(context::refresh)
275+
.withMessage("Unable to override bean 'descriptionBean': only singleton beans can be overridden.");
291276
}
292277

293278
@Test
294-
void replaceBeanByNameWithMatchingBeanDefinitionWithCustomScope() {
279+
void replaceBeanByNameWithMatchingBeanDefinitionWithCustomScopeFails() {
295280
String beanName = "descriptionBean";
296281
String scope = "customScope";
297282

@@ -302,49 +287,28 @@ void replaceBeanByNameWithMatchingBeanDefinitionWithCustomScope() {
302287
definition.setScope(scope);
303288
context.registerBeanDefinition(beanName, definition);
304289

305-
assertThatNoException().isThrownBy(context::refresh);
306-
// The Bean Override support currently creates a "dummy" BeanDefinition that
307-
// retains the custom scope of the original BeanDefinition.
308-
assertThat(context.isSingleton(beanName)).as("isSingleton").isFalse();
309-
assertThat(context.isPrototype(beanName)).as("isPrototype").isFalse();
310-
assertThat(beanFactory.getBeanDefinition(beanName).getScope()).isEqualTo(scope);
311-
// Since the "dummy" BeanDefinition has a custom scope, a manual singleton
312-
// is not registered, and the "dummy" BeanDefinition is used to create a
313-
// new java.lang.String using the default constructor, which results in an
314-
// empty string instead of "overridden". In other words, the bean is not
315-
// actually overridden as expected, and no exception is thrown which
316-
// silently masks the issue.
317-
assertThat(context.getBean(beanName)).isEqualTo("");
290+
assertThatIllegalStateException()
291+
.isThrownBy(context::refresh)
292+
.withMessage("Unable to override bean 'descriptionBean': only singleton beans can be overridden.");
318293
}
319294

320295
@Test
321-
void replaceBeanByNameWithMatchingBeanDefinitionForPrototypeScopedFactoryBean() {
296+
void replaceBeanByNameWithMatchingBeanDefinitionForPrototypeScopedFactoryBeanFails() {
322297
String beanName = "messageServiceBean";
323298
AnnotationConfigApplicationContext context = createContext(MessageServiceTestCase.class);
324299
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(SingletonMessageServiceFactoryBean.class);
325300
factoryBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
326301
context.registerBeanDefinition(beanName, factoryBeanDefinition);
327302

328-
assertThatNoException().isThrownBy(context::refresh);
329-
// The Bean Override support currently creates a "dummy" BeanDefinition that
330-
// retains the prototype scope of the original BeanDefinition.
331-
assertThat(context.isSingleton(beanName)).as("isSingleton").isFalse();
332-
assertThat(context.isPrototype(beanName)).as("isPrototype").isTrue();
333-
// Since the "dummy" BeanDefinition has prototype scope, a manual singleton
334-
// is not registered, and the "dummy" BeanDefinition is used to create a
335-
// new MessageService using the default constructor, which results in an
336-
// error since MessageService is an interface.
337-
assertThatExceptionOfType(BeanCreationException.class)
338-
.isThrownBy(() -> context.getBean(beanName))
339-
.withMessageContaining("Specified class is an interface");
303+
assertThatIllegalStateException()
304+
.isThrownBy(context::refresh)
305+
.withMessage("Unable to override bean 'messageServiceBean': only singleton beans can be overridden.");
340306
}
341307

342308
@Test
343-
void replaceBeanByNameWithMatchingBeanDefinitionRetainsPrimaryFallbackAndScopeProperties() {
309+
void replaceBeanByNameWithMatchingBeanDefinitionRetainsPrimaryAndFallbackFlags() {
344310
AnnotationConfigApplicationContext context = createContext(CaseByName.class);
345-
context.getBeanFactory().registerScope("customScope", new SimpleThreadScope());
346311
RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL");
347-
definition.setScope("customScope");
348312
definition.setPrimary(true);
349313
definition.setFallback(true);
350314
context.registerBeanDefinition("descriptionBean", definition);
@@ -354,8 +318,8 @@ void replaceBeanByNameWithMatchingBeanDefinitionRetainsPrimaryFallbackAndScopePr
354318
.isNotSameAs(definition)
355319
.matches(BeanDefinition::isPrimary, "isPrimary")
356320
.matches(BeanDefinition::isFallback, "isFallback")
357-
.satisfies(d -> assertThat(d.getScope()).isEqualTo("customScope"))
358-
.matches(Predicate.not(BeanDefinition::isSingleton), "!isSingleton")
321+
.satisfies(d -> assertThat(d.getScope()).isEqualTo(""))
322+
.matches(BeanDefinition::isSingleton, "isSingleton")
359323
.matches(Predicate.not(BeanDefinition::isPrototype), "!isPrototype");
360324
}
361325

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

-7
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,11 @@ public TestBean getObject() {
9090
public Class<?> getObjectType() {
9191
return TestBean.class;
9292
}
93-
94-
@Override
95-
public boolean isSingleton() {
96-
return false;
97-
}
98-
9993
}
10094

10195
public interface TestBean {
10296

10397
String hello();
104-
10598
}
10699

107100
}

0 commit comments

Comments
 (0)