Skip to content

Commit 81d89f4

Browse files
committed
Relax singleton enforcement for Bean Overrides in the TestContext framework
In gh-33602, we introduced strict singleton enforcement for bean overrides -- for example, for @⁠MockitoBean, @⁠TestBean, etc. However, the use of BeanFactory#isSingleton(beanName) can result in a BeanCreationException for certain beans, such as a Spring Data JPA FactoryBean for a JpaRepository. In light of that, this commit relaxes the singleton enforcement in BeanOverrideBeanFactoryPostProcessor by only checking the result of BeanDefinition#isSingleton() for existing bean definitions. This commit also updates the Javadoc and reference documentation to reflect the status quo. See gh-33602 Closes gh-33800
1 parent 52e813d commit 81d89f4

File tree

8 files changed

+69
-27
lines changed

8 files changed

+69
-27
lines changed

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

+11-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,17 @@ xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overrid
3939
and the original instance is wrapped in a Mockito spy. This strategy requires that
4040
exactly one candidate bean exists.
4141

42-
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
43-
bean will result in an exception.
42+
[TIP]
43+
====
44+
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
45+
will result in an exception.
46+
47+
When using `@MockitoBean` to mock a bean created by a `FactoryBean`, the `FactoryBean`
48+
will be replaced with a singleton mock of the type of object created by the `FactoryBean`.
49+
50+
When using `@MockitoSpyBean` to create a spy for a `FactoryBean`, a spy will be created
51+
for the object created by the `FactoryBean`, not for the `FactoryBean` itself.
52+
====
4453

4554
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
4655

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

+10-3
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Java::
8282
<2> The result of this static method will be used as the instance and injected into the field.
8383
======
8484

85-
[NOTE]
85+
[TIP]
8686
====
8787
Spring searches for the factory method to invoke in the test class, in the test class
8888
hierarchy, and in the enclosing class hierarchy for a `@Nested` test class.
@@ -92,5 +92,12 @@ fully-qualified method name following the syntax `<fully-qualified class name>#<
9292
– for example, `methodName = "org.example.TestUtils#createCustomService"`.
9393
====
9494

95-
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
96-
bean will result in an exception.
95+
[TIP]
96+
====
97+
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
98+
will result in an exception.
99+
100+
When overriding a bean created by a `FactoryBean`, the `FactoryBean` will be replaced
101+
with a singleton bean corresponding to the value returned from the `@TestBean` factory
102+
method.
103+
====

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

+13-3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ defined by the corresponding `BeanOverrideStrategy`:
5757
`WRAP`::
5858
Retrieves the original bean and wraps it.
5959

60+
[TIP]
61+
====
62+
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
63+
will result in an exception.
64+
65+
When replacing a bean created by a `FactoryBean`, the `FactoryBean` itself will be
66+
replaced with a singleton bean corresponding to bean override instance created by the
67+
applicable `BeanOverrideHandler`.
68+
69+
When wrapping a bean created by a `FactoryBean`, the object created by the `FactoryBean`
70+
will be wrapped, not the `FactoryBean` itself.
71+
====
72+
6073
[NOTE]
6174
====
6275
In contrast to Spring's autowiring mechanism (for example, resolution of an `@Autowired`
@@ -71,6 +84,3 @@ Alternatively, the user can directly provide the bean name in the custom annotat
7184
`BeanOverrideProcessor` implementations may also internally compute a bean name based on
7285
a convention or some other method.
7386
====
74-
75-
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
76-
bean will result in an exception.

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

+11-4
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,7 @@ else if (Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING)) {
197197
// Now we have an instance (the override) that we can manually register as a singleton.
198198
//
199199
// However, we need to remove any existing singleton instance -- for example, a
200-
// manually registered singleton or a singleton that was registered as a side effect
201-
// of the isSingleton() check in validateBeanDefinition().
200+
// manually registered singleton.
202201
//
203202
// As a bonus, by manually registering a singleton during "AOT processing", we allow
204203
// GenericApplicationContext's preDetermineBeanType() method to transparently register
@@ -334,10 +333,18 @@ private static RootBeanDefinition createPseudoBeanDefinition(BeanOverrideHandler
334333
/**
335334
* Validate that the {@link BeanDefinition} for the supplied bean name is suitable
336335
* for being replaced by a bean override.
336+
* <p>If there is no registered {@code BeanDefinition} for the supplied bean name,
337+
* no validation is performed.
337338
*/
338339
private static void validateBeanDefinition(ConfigurableListableBeanFactory beanFactory, String beanName) {
339-
Assert.state(beanFactory.isSingleton(beanName),
340-
() -> "Unable to override bean '" + beanName + "': only singleton beans can be overridden.");
340+
// Due to https://github.com/spring-projects/spring-framework/issues/33800, we do NOT invoke
341+
// beanFactory.isSingleton(beanName), since doing so can result in a BeanCreationException for
342+
// certain beans -- for example, a Spring Data FactoryBean for a JpaRepository.
343+
if (beanFactory.containsBeanDefinition(beanName)) {
344+
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
345+
Assert.state(beanDefinition.isSingleton(),
346+
() -> "Unable to override bean '" + beanName + "': only singleton beans can be overridden.");
347+
}
341348
}
342349

343350
private static void destroySingleton(ConfigurableListableBeanFactory beanFactory, String beanName) {

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@
9898
* }</code></pre>
9999
*
100100
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
101-
* Any attempt to override a non-singleton bean will result in an exception.
101+
* Any attempt to override a non-singleton bean will result in an exception. When
102+
* overriding a bean created by a {@link org.springframework.beans.factory.FactoryBean
103+
* FactoryBean}, the {@code FactoryBean} will be replaced with a singleton bean
104+
* corresponding to the value returned from the {@code @TestBean} factory method.
102105
*
103106
* @author Simon Baslé
104107
* @author Stephane Nicoll

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@
5252
* registered directly}) will not be found, and a mocked bean will be added to
5353
* the context alongside the existing dependency.
5454
*
55-
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
56-
* Any attempt to mock a non-singleton bean will result in an exception.
55+
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be mocked.
56+
* Any attempt to mock a non-singleton bean will result in an exception. When
57+
* mocking a bean created by a {@link org.springframework.beans.factory.FactoryBean
58+
* FactoryBean}, the {@code FactoryBean} will be replaced with a singleton mock
59+
* of the type of object created by the {@code FactoryBean}.
5760
*
5861
* @author Simon Baslé
5962
* @author Sam Brannen

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,11 @@
4343
* {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object)
4444
* registered directly} as resolvable dependencies.
4545
*
46-
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be spied.
47-
* Any attempt to create a spy for a non-singleton bean will result in an exception.
46+
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be spied. Any attempt
47+
* to create a spy for a non-singleton bean will result in an exception. When
48+
* creating a spy for a {@link org.springframework.beans.factory.FactoryBean FactoryBean},
49+
* a spy will be created for the object created by the {@code FactoryBean}, not
50+
* for the {@code FactoryBean} itself.
4851
*
4952
* @author Simon Baslé
5053
* @author Sam Brannen

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

+10-10
Original file line numberDiff line numberDiff line change
@@ -237,16 +237,16 @@ void replaceBeanByNameWithMatchingBeanDefinitionForClassBasedSingletonFactoryBea
237237
assertThat(context.getBean(beanName)).isEqualTo("overridden");
238238
}
239239

240-
@Test
241-
void replaceBeanByNameWithMatchingBeanDefinitionForClassBasedNonSingletonFactoryBeanFails() {
240+
@Test // gh-33800
241+
void replaceBeanByNameWithMatchingBeanDefinitionForClassBasedNonSingletonFactoryBean() {
242242
String beanName = "descriptionBean";
243243
AnnotationConfigApplicationContext context = createContext(CaseByName.class);
244244
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(NonSingletonStringFactoryBean.class);
245245
context.registerBeanDefinition(beanName, factoryBeanDefinition);
246246

247-
assertThatIllegalStateException()
248-
.isThrownBy(context::refresh)
249-
.withMessage("Unable to override bean 'descriptionBean': only singleton beans can be overridden.");
247+
assertThatNoException().isThrownBy(context::refresh);
248+
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
249+
assertThat(context.getBean(beanName)).isEqualTo("overridden");
250250
}
251251

252252
@Test
@@ -261,16 +261,16 @@ void replaceBeanByNameWithMatchingBeanDefinitionForInterfaceBasedSingletonFactor
261261
assertThat(context.getBean(beanName, MessageService.class).getMessage()).isEqualTo("overridden");
262262
}
263263

264-
@Test
265-
void replaceBeanByNameWithMatchingBeanDefinitionForInterfaceBasedNonSingletonFactoryBeanFails() {
264+
@Test // gh-33800
265+
void replaceBeanByNameWithMatchingBeanDefinitionForInterfaceBasedNonSingletonFactoryBean() {
266266
String beanName = "messageServiceBean";
267267
AnnotationConfigApplicationContext context = createContext(MessageServiceTestCase.class);
268268
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(NonSingletonMessageServiceFactoryBean.class);
269269
context.registerBeanDefinition(beanName, factoryBeanDefinition);
270270

271-
assertThatIllegalStateException()
272-
.isThrownBy(context::refresh)
273-
.withMessage("Unable to override bean 'messageServiceBean': only singleton beans can be overridden.");
271+
assertThatNoException().isThrownBy(context::refresh);
272+
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
273+
assertThat(context.getBean(beanName, MessageService.class).getMessage()).isEqualTo("overridden");
274274
}
275275

276276
@Test

0 commit comments

Comments
 (0)