Support Bean Overrides for non-singletons

Prior to this commit, the BeanOverrideBeanFactoryPostProcessor rejected
any attempt to override a non-singleton bean; however, due to interest
from the community, we have decided to provide support for overriding
non-singleton beans via the Bean Override mechanism — for example, when
using @⁠MockitoBean, @⁠MockitoSpyBean, and @⁠TestBean.

With this commit, we now support Bean Overrides for non-singletons: for
standard JVM runtimes as well as AOT processing and AOT runtimes. This
commit also documents that non-singletons will effectively be converted
to singletons when overridden and logs a warning similar to the
following.

WARN: BeanOverrideBeanFactoryPostProcessor - Converting 'prototype' scoped bean definition 'myBean' to a singleton.

See gh-33602
See gh-32933
See gh-33800
Closes gh-35574
This commit is contained in:
Sam Brannen
2025-10-06 15:25:47 +02:00
parent ff9a349271
commit 30db2e4fb5
15 changed files with 250 additions and 53 deletions
@@ -22,6 +22,8 @@ import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.aop.scope.ScopedProxyUtils;
@@ -67,6 +69,8 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
private static final String PSEUDO_BEAN_NAME_PLACEHOLDER = "<<< PSEUDO BEAN NAME PLACEHOLDER >>>";
private static final Log logger = LogFactory.getLog(BeanOverrideBeanFactoryPostProcessor.class);
private static final BeanNameGenerator beanNameGenerator = DefaultBeanNameGenerator.INSTANCE;
private final Set<BeanOverrideHandler> beanOverrideHandlers;
@@ -182,10 +186,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
}
if (existingBeanDefinition != null) {
// Validate the existing bean definition.
// Process the existing bean definition.
//
// Applies during "JVM runtime", "AOT processing", and "AOT runtime".
validateBeanDefinition(beanFactory, beanName);
convertToSingletonIfNecessary(existingBeanDefinition, beanName);
}
else if (Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING)) {
// There was no existing bean definition, but during "AOT processing" we
@@ -289,7 +293,8 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
}
}
validateBeanDefinition(beanFactory, beanName);
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
convertToSingletonIfNecessary(beanDefinition, beanName);
this.beanOverrideRegistry.registerBeanOverrideHandler(handler, beanName);
}
@@ -470,19 +475,20 @@ 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.
* Convert the supplied {@link BeanDefinition} for the supplied bean name to
* a singleton, if necessary.
* @since 7.0
*/
private static void validateBeanDefinition(ConfigurableListableBeanFactory beanFactory, String beanName) {
private static void convertToSingletonIfNecessary(BeanDefinition beanDefinition, String beanName) {
// 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.");
if (!beanDefinition.isSingleton()) {
if (logger.isWarnEnabled()) {
logger.warn("Converting '%s' scoped bean definition '%s' to a singleton."
.formatted(beanDefinition.getScope(), beanName));
}
beanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
}
}
@@ -64,13 +64,29 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate
* creation} &mdash; for example, based on further processing of the annotation,
* the annotated field, or the annotated class.
*
* <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.
* <h3>Singleton Semantics</h3>
*
* <p>When replacing a non-singleton bean, the non-singleton bean will be replaced
* with a singleton bean corresponding to bean override instance created by the
* handler, and the corresponding bean definition will be converted to a singleton.
* Consequently, if a handler overrides a prototype or custom scoped bean, the
* overridden bean will be treated as a singleton.
*
* <p>When replacing a bean created by a
* {@link org.springframework.beans.factory.FactoryBean FactoryBean}, the
* {@code FactoryBean} itself will be replaced with a singleton bean corresponding
* to bean override instance created by the handler.
*
* <p>When wrapping a bean created by a
* {@link org.springframework.beans.factory.FactoryBean FactoryBean}, the object
* created by the {@code FactoryBean} will be wrapped, not the {@code FactoryBean}
* itself.
*
* @author Simon Baslé
* @author Stephane Nicoll
* @author Sam Brannen
* @since 6.2
* @see BeanOverrideStrategy
*/
public abstract class BeanOverrideHandler {
@@ -109,11 +109,14 @@ import org.springframework.test.context.bean.override.BeanOverride;
* See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}
* for further details and examples.
*
* <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. 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.
* <p><strong>NOTE</strong>: When overriding a non-singleton bean, the non-singleton
* bean will be replaced with a singleton bean corresponding to the value returned
* from the {@code @TestBean} factory method, and the corresponding bean definition
* will be converted to a singleton. Consequently, if you override a prototype or
* scoped bean, it will be treated as a singleton. Similarly, 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.
*
* <p>There are no restrictions on the visibility of {@code @TestBean} fields or
* factory methods. Such fields and methods can therefore be {@code public},
@@ -84,11 +84,13 @@ import org.springframework.test.context.bean.override.BeanOverride;
* See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}
* for further details and examples.
*
* <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}.
* <p><strong>NOTE</strong>: When mocking a non-singleton bean, the non-singleton
* bean will be replaced with a singleton mock, and the corresponding bean definition
* will be converted to a singleton. Consequently, if you mock a prototype or scoped
* bean, the mock will be treated as a singleton. Similarly, 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}.
*
* <p>There are no restrictions on the visibility of a {@code @MockitoBean} field.
* Such fields can therefore be {@code public}, {@code protected}, package-private
@@ -86,11 +86,13 @@ import org.springframework.test.context.bean.override.BeanOverride;
* See the Javadoc for {@link org.springframework.test.context.ContextHierarchy @ContextHierarchy}
* for further details and examples.
*
* <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.
* <p><strong>NOTE</strong>: When creating a spy for a non-singleton bean, the
* corresponding bean definition will be converted to a singleton. Consequently,
* if you create a spy for a prototype or scoped bean, the spy will be treated as
* a singleton. Similarly, 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.
*
* <p>There are no restrictions on the visibility of a {@code @MockitoSpyBean} field.
* Such fields can therefore be {@code public}, {@code protected}, package-private