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
This commit is contained in:
Sam Brannen
2024-09-27 17:36:22 +02:00
parent 4e9b503055
commit d79258ac73
10 changed files with 70 additions and 83 deletions

View File

@@ -40,6 +40,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.ResolvableType;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
@@ -116,14 +117,15 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
private void replaceDefinition(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry,
OverrideMetadata overrideMetadata, boolean enforceExistingDefinition) {
// The following is a "dummy" bean definition which should not be used to
// The following is a "pseudo" bean definition which MUST NOT be used to
// create an actual bean instance.
RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata);
RootBeanDefinition pseudoBeanDefinition = createPseudoBeanDefinition(overrideMetadata);
String beanName = overrideMetadata.getBeanName();
String beanNameIncludingFactory;
BeanDefinition existingBeanDefinition = null;
if (beanName == null) {
beanNameIncludingFactory = getBeanNameForType(beanFactory, registry, overrideMetadata, beanDefinition, enforceExistingDefinition);
beanNameIncludingFactory = getBeanNameForType(
beanFactory, registry, overrideMetadata, pseudoBeanDefinition, enforceExistingDefinition);
beanName = BeanFactoryUtils.transformedBeanName(beanNameIncludingFactory);
if (registry.containsBeanDefinition(beanName)) {
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
@@ -145,25 +147,24 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
// Process existing bean definition.
if (existingBeanDefinition != null) {
copyBeanDefinitionProperties(existingBeanDefinition, beanDefinition);
validateBeanDefinition(beanFactory, beanName);
copyBeanDefinitionProperties(existingBeanDefinition, pseudoBeanDefinition);
registry.removeBeanDefinition(beanName);
}
// At this point, we either removed an existing bean definition above, or
// there was no bean definition to begin with. So, we register the dummy bean
// there was no bean definition to begin with. So, we register the pseudo bean
// definition to ensure that a bean definition exists for the given bean name.
registry.registerBeanDefinition(beanName, beanDefinition);
registry.registerBeanDefinition(beanName, pseudoBeanDefinition);
Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null);
overrideMetadata.track(override, beanFactory);
this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanNameIncludingFactory);
if (beanFactory.isSingleton(beanNameIncludingFactory)) {
// Now we have an instance (the override) that we can register. At this
// stage we don't expect a singleton instance to be present, and this call
// will throw an exception if there is such an instance already.
beanFactory.registerSingleton(beanName, override);
}
// Now we have an instance (the override) that we can register. At this stage, we don't
// expect a singleton instance to be present. If for some reason a singleton instance
// already exists, the following will throw an exception.
beanFactory.registerSingleton(beanName, override);
}
/**
@@ -196,6 +197,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
.formatted(beanName, overrideMetadata.getBeanType()));
}
}
validateBeanDefinition(beanFactory, beanName);
this.overrideRegistrar.markWrapEarly(overrideMetadata, beanName);
this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanName);
}
@@ -276,23 +278,30 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
* definition metadata available in the {@link BeanFactory} — for example,
* for autowiring candidate resolution.
*/
private static RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
private static RootBeanDefinition createPseudoBeanDefinition(OverrideMetadata metadata) {
RootBeanDefinition definition = new RootBeanDefinition(metadata.getBeanType().resolve());
definition.setTargetType(metadata.getBeanType());
definition.setQualifiedElement(metadata.getField());
return definition;
}
/**
* Validate that the {@link BeanDefinition} for the supplied bean name is suitable
* for being replaced by a bean override.
*/
private static void validateBeanDefinition(ConfigurableListableBeanFactory beanFactory, String beanName) {
Assert.state(beanFactory.isSingleton(beanName),
() -> "Unable to override bean '" + beanName + "': only singleton beans can be overridden.");
}
/**
* Copy the following properties of the source {@link BeanDefinition} to the
* target: the {@linkplain BeanDefinition#isPrimary() primary flag}, the
* {@linkplain BeanDefinition#isFallback() fallback flag}, and the
* {@linkplain BeanDefinition#getScope() scope}.
* target: the {@linkplain BeanDefinition#isPrimary() primary flag} and the
* {@linkplain BeanDefinition#isFallback() fallback flag}.
*/
private static void copyBeanDefinitionProperties(BeanDefinition source, RootBeanDefinition target) {
target.setPrimary(source.isPrimary());
target.setFallback(source.isFallback());
target.setScope(source.getScope());
}

View File

@@ -49,6 +49,9 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate
* instance creation} — for example, based on further processing of the
* annotation or the annotated field.
*
* <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.
*
* @author Simon Baslé
* @author Stephane Nicoll
* @author Sam Brannen

View File

@@ -93,6 +93,9 @@ 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.
*
* @author Simon Baslé
* @author Stephane Nicoll
* @author Sam Brannen

View File

@@ -47,6 +47,9 @@ 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 override a non-singleton bean will result in an exception.
*
* @author Simon Baslé
* @since 6.2
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean

View File

@@ -42,6 +42,9 @@ import org.springframework.test.context.bean.override.BeanOverride;
* {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object)
* registered directly}) will not be found.
*
* <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.
*
* @author Simon Baslé
* @since 6.2
* @see org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean