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
This commit is contained in:
Sam Brannen 2024-10-28 11:42:56 +01:00
parent 52e813d0ad
commit 81d89f478a
8 changed files with 69 additions and 27 deletions

View File

@ -39,8 +39,17 @@ xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overrid
and the original instance is wrapped in a Mockito spy. This strategy requires that
exactly one candidate bean exists.
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
bean will result in an exception.
[TIP]
====
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
will result in an exception.
When using `@MockitoBean` to mock a bean created by a `FactoryBean`, the `FactoryBean`
will be replaced with a singleton mock of the type of object created by the `FactoryBean`.
When using `@MockitoSpyBean` to create a spy for a `FactoryBean`, a spy will be created
for the object created by the `FactoryBean`, not for the `FactoryBean` itself.
====
The following example shows how to use the default behavior of the `@MockitoBean` annotation:

View File

@ -82,7 +82,7 @@ Java::
<2> The result of this static method will be used as the instance and injected into the field.
======
[NOTE]
[TIP]
====
Spring searches for the factory method to invoke in the test class, in the test class
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>#<
for example, `methodName = "org.example.TestUtils#createCustomService"`.
====
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
bean will result in an exception.
[TIP]
====
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
will result in an exception.
When overriding a bean created by a `FactoryBean`, the `FactoryBean` will be replaced
with a singleton bean corresponding to the value returned from the `@TestBean` factory
method.
====

View File

@ -57,6 +57,19 @@ defined by the corresponding `BeanOverrideStrategy`:
`WRAP`::
Retrieves the original bean and wraps it.
[TIP]
====
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
will result in an exception.
When replacing a bean created by a `FactoryBean`, the `FactoryBean` itself will be
replaced with a singleton bean corresponding to bean override instance created by the
applicable `BeanOverrideHandler`.
When wrapping a bean created by a `FactoryBean`, the object created by the `FactoryBean`
will be wrapped, not the `FactoryBean` itself.
====
[NOTE]
====
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
`BeanOverrideProcessor` implementations may also internally compute a bean name based on
a convention or some other method.
====
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
bean will result in an exception.

View File

@ -197,8 +197,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
// Now we have an instance (the override) that we can manually register as a singleton.
//
// However, we need to remove any existing singleton instance -- for example, a
// manually registered singleton or a singleton that was registered as a side effect
// of the isSingleton() check in validateBeanDefinition().
// manually registered singleton.
//
// As a bonus, by manually registering a singleton during "AOT processing", we allow
// GenericApplicationContext's preDetermineBeanType() method to transparently register
@ -334,10 +333,18 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
/**
* Validate that the {@link BeanDefinition} for the supplied bean name is suitable
* for being replaced by a bean override.
* <p>If there is no registered {@code BeanDefinition} for the supplied bean name,
* no validation is performed.
*/
private static void validateBeanDefinition(ConfigurableListableBeanFactory beanFactory, String beanName) {
Assert.state(beanFactory.isSingleton(beanName),
() -> "Unable to override bean '" + beanName + "': only singleton beans can be overridden.");
// Due to https://github.com/spring-projects/spring-framework/issues/33800, we do NOT invoke
// beanFactory.isSingleton(beanName), since doing so can result in a BeanCreationException for
// certain beans -- for example, a Spring Data FactoryBean for a JpaRepository.
if (beanFactory.containsBeanDefinition(beanName)) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
Assert.state(beanDefinition.isSingleton(),
() -> "Unable to override bean '" + beanName + "': only singleton beans can be overridden.");
}
}
private static void destroySingleton(ConfigurableListableBeanFactory beanFactory, String beanName) {

View File

@ -98,7 +98,10 @@ import org.springframework.test.context.bean.override.BeanOverride;
* }</code></pre>
*
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
* Any attempt to override a non-singleton bean will result in an exception.
* Any attempt to override a non-singleton bean will result in an exception. When
* overriding a bean created by a {@link org.springframework.beans.factory.FactoryBean
* FactoryBean}, the {@code FactoryBean} will be replaced with a singleton bean
* corresponding to the value returned from the {@code @TestBean} factory method.
*
* @author Simon Baslé
* @author Stephane Nicoll

View File

@ -52,8 +52,11 @@ import org.springframework.test.context.bean.override.BeanOverride;
* registered directly}) will not be found, and a mocked bean will be added to
* the context alongside the existing dependency.
*
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
* Any attempt to mock a non-singleton bean will result in an exception.
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be mocked.
* Any attempt to mock a non-singleton bean will result in an exception. When
* mocking a bean created by a {@link org.springframework.beans.factory.FactoryBean
* FactoryBean}, the {@code FactoryBean} will be replaced with a singleton mock
* of the type of object created by the {@code FactoryBean}.
*
* @author Simon Baslé
* @author Sam Brannen

View File

@ -43,8 +43,11 @@ import org.springframework.test.context.bean.override.BeanOverride;
* {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object)
* registered directly} as resolvable dependencies.
*
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be spied.
* Any attempt to create a spy for a non-singleton bean will result in an exception.
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be spied. Any attempt
* to create a spy for a non-singleton bean will result in an exception. When
* creating a spy for a {@link org.springframework.beans.factory.FactoryBean FactoryBean},
* a spy will be created for the object created by the {@code FactoryBean}, not
* for the {@code FactoryBean} itself.
*
* @author Simon Baslé
* @author Sam Brannen

View File

@ -237,16 +237,16 @@ class BeanOverrideBeanFactoryPostProcessorTests {
assertThat(context.getBean(beanName)).isEqualTo("overridden");
}
@Test
void replaceBeanByNameWithMatchingBeanDefinitionForClassBasedNonSingletonFactoryBeanFails() {
@Test // gh-33800
void replaceBeanByNameWithMatchingBeanDefinitionForClassBasedNonSingletonFactoryBean() {
String beanName = "descriptionBean";
AnnotationConfigApplicationContext context = createContext(CaseByName.class);
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(NonSingletonStringFactoryBean.class);
context.registerBeanDefinition(beanName, factoryBeanDefinition);
assertThatIllegalStateException()
.isThrownBy(context::refresh)
.withMessage("Unable to override bean 'descriptionBean': only singleton beans can be overridden.");
assertThatNoException().isThrownBy(context::refresh);
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
assertThat(context.getBean(beanName)).isEqualTo("overridden");
}
@Test
@ -261,16 +261,16 @@ class BeanOverrideBeanFactoryPostProcessorTests {
assertThat(context.getBean(beanName, MessageService.class).getMessage()).isEqualTo("overridden");
}
@Test
void replaceBeanByNameWithMatchingBeanDefinitionForInterfaceBasedNonSingletonFactoryBeanFails() {
@Test // gh-33800
void replaceBeanByNameWithMatchingBeanDefinitionForInterfaceBasedNonSingletonFactoryBean() {
String beanName = "messageServiceBean";
AnnotationConfigApplicationContext context = createContext(MessageServiceTestCase.class);
RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(NonSingletonMessageServiceFactoryBean.class);
context.registerBeanDefinition(beanName, factoryBeanDefinition);
assertThatIllegalStateException()
.isThrownBy(context::refresh)
.withMessage("Unable to override bean 'messageServiceBean': only singleton beans can be overridden.");
assertThatNoException().isThrownBy(context::refresh);
assertThat(context.isSingleton(beanName)).as("isSingleton").isTrue();
assertThat(context.getBean(beanName, MessageService.class).getMessage()).isEqualTo("overridden");
}
@Test