This commit is contained in:
Stéphane Nicoll
2024-04-11 08:45:26 +02:00
parent 77c6f160fe
commit 2e3a923225
16 changed files with 229 additions and 229 deletions

View File

@@ -22,15 +22,13 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Mark an annotation as eligible for Bean Override parsing.
* Mark an annotation as eligible for Bean Override processing.
*
* <p>This meta-annotation specifies a {@link BeanOverrideProcessor} class which
* must be capable of handling the composed annotation that is meta-annotated
* with {@code @BeanOverride}.
* <p>Specifying this annotation triggers the defined {@link BeanOverrideProcessor}
* which must be capable of handling the composed annotation and its attributes.
*
* <p>The composed annotation that is meta-annotated with {@code @BeanOverride}
* must have a {@code RetentionPolicy} of {@link RetentionPolicy#RUNTIME RUNTIME}
* and a {@code Target} of {@link ElementType#FIELD FIELD}.
* <p>The composed annotation is meant to be detected on fields only so it is
* expected that it has a {@code Target} of {@link ElementType#FIELD FIELD}.
*
* @author Simon Baslé
* @since 6.2
@@ -41,8 +39,8 @@ import java.lang.annotation.Target;
public @interface BeanOverride {
/**
* A {@link BeanOverrideProcessor} implementation class by which the composed
* annotation should be processed.
* The {@link BeanOverrideProcessor} implementation to trigger against
* the composed annotation.
*/
Class<? extends BeanOverrideProcessor> value();

View File

@@ -22,31 +22,26 @@ import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
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;
/**
* A {@link BeanFactoryPostProcessor} implementation that processes test classes
* and adapt the {@link BeanDefinitionRegistry} for any {@link BeanOverride} it
* may define.
* and adapt the {@link BeanFactory} for any {@link BeanOverride} it may define.
*
* <p>A set of classes from which to parse {@link OverrideMetadata} must be
* provided to this processor. Each test class is expected to use any
@@ -66,10 +61,6 @@ import org.springframework.util.StringUtils;
*/
class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {
private static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideBeanFactoryPostProcessor.class.getName();
private static final String EARLY_INFRASTRUCTURE_BEAN_NAME =
BeanOverrideBeanFactoryPostProcessor.WrapEarlyBeanPostProcessor.class.getName();
private final BeanOverrideRegistrar overrideRegistrar;
@@ -94,14 +85,16 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
Assert.isInstanceOf(DefaultListableBeanFactory.class, beanFactory,
"Bean overriding annotations can only be used on a DefaultListableBeanFactory");
postProcessWithRegistry((DefaultListableBeanFactory) beanFactory);
if (!(beanFactory instanceof BeanDefinitionRegistry registry)) {
throw new IllegalStateException("Cannot process bean override with a BeanFactory " +
"that doesn't implement BeanDefinitionRegistry: " + beanFactory.getClass());
}
postProcessWithRegistry(beanFactory, registry);
}
private void postProcessWithRegistry(DefaultListableBeanFactory registry) {
private void postProcessWithRegistry(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry) {
for (OverrideMetadata metadata : this.overrideRegistrar.getOverrideMetadata()) {
registerBeanOverride(registry, metadata);
registerBeanOverride(beanFactory, registry, metadata);
}
}
@@ -116,15 +109,20 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
to.setScope(from.getScope());
}
private void registerBeanOverride(DefaultListableBeanFactory beanFactory, OverrideMetadata overrideMetadata) {
private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry,
OverrideMetadata overrideMetadata) {
switch (overrideMetadata.getStrategy()) {
case REPLACE_DEFINITION -> registerReplaceDefinition(beanFactory, overrideMetadata, true);
case REPLACE_OR_CREATE_DEFINITION -> registerReplaceDefinition(beanFactory, overrideMetadata, false);
case REPLACE_DEFINITION ->
registerReplaceDefinition(beanFactory, registry, overrideMetadata, true);
case REPLACE_OR_CREATE_DEFINITION ->
registerReplaceDefinition(beanFactory, registry, overrideMetadata, false);
case WRAP_BEAN -> registerWrapBean(beanFactory, overrideMetadata);
}
}
private void registerReplaceDefinition(DefaultListableBeanFactory beanFactory, OverrideMetadata overrideMetadata, boolean enforceExistingDefinition) {
private void registerReplaceDefinition(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry,
OverrideMetadata overrideMetadata, boolean enforceExistingDefinition) {
RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata);
String beanName = overrideMetadata.getBeanName();
@@ -133,13 +131,13 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
if (beanFactory.containsBeanDefinition(beanName)) {
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
copyBeanDefinitionDetails(existingBeanDefinition, beanDefinition);
beanFactory.removeBeanDefinition(beanName);
registry.removeBeanDefinition(beanName);
}
else if (enforceExistingDefinition) {
throw new IllegalStateException("Unable to override bean '" + beanName + "'; there is no" +
" bean definition to replace with that name");
}
beanFactory.registerBeanDefinition(beanName, beanDefinition);
registry.registerBeanDefinition(beanName, beanDefinition);
Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null);
if (beanFactory.isSingleton(beanName)) {
@@ -160,7 +158,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
* upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)}
* phase.
*/
private void registerWrapBean(DefaultListableBeanFactory beanFactory, OverrideMetadata metadata) {
private void registerWrapBean(ConfigurableListableBeanFactory beanFactory, OverrideMetadata metadata) {
Set<String> existingBeanNames = getExistingBeanNames(beanFactory, metadata.getBeanType());
String beanName = metadata.getBeanName();
if (!existingBeanNames.contains(beanName)) {
@@ -177,7 +175,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
return definition;
}
private Set<String> getExistingBeanNames(DefaultListableBeanFactory beanFactory, ResolvableType resolvableType) {
private Set<String> getExistingBeanNames(ConfigurableListableBeanFactory beanFactory, ResolvableType resolvableType) {
Set<String> beans = new LinkedHashSet<>(
Arrays.asList(beanFactory.getBeanNamesForType(resolvableType, true, false)));
Class<?> type = resolvableType.resolve(Object.class);
@@ -193,39 +191,6 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
return beans;
}
/**
* Register a {@link BeanOverrideBeanFactoryPostProcessor} with a {@link BeanDefinitionRegistry}.
* <p>Not required when using the Spring TestContext Framework, as registration
* is automatic via the
* {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader}
* mechanism.
* @param registry the bean definition registry
*/
public static void register(BeanDefinitionRegistry registry) {
RuntimeBeanReference registrarReference = new RuntimeBeanReference(BeanOverrideRegistrar.INFRASTRUCTURE_BEAN_NAME);
// Early processor
addInfrastructureBeanDefinition(
registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
constructorArgs.addIndexedArgumentValue(0, registrarReference));
// Main processor
addInfrastructureBeanDefinition(
registry, BeanOverrideBeanFactoryPostProcessor.class, INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
constructorArgs.addIndexedArgumentValue(0, registrarReference));
}
private static void addInfrastructureBeanDefinition(BeanDefinitionRegistry registry,
Class<?> clazz, String beanName, Consumer<ConstructorArgumentValues> constructorArgumentsConsumer) {
if (!registry.containsBeanDefinition(beanName)) {
RootBeanDefinition definition = new RootBeanDefinition(clazz);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues();
constructorArgumentsConsumer.accept(constructorArguments);
registry.registerBeanDefinition(beanName, definition);
}
}
static final class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor,
PriorityOrdered {

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override;
import java.util.Set;
import java.util.function.Consumer;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.bean.override.BeanOverrideBeanFactoryPostProcessor.WrapEarlyBeanPostProcessor;
/**
* {@link ContextCustomizer} implementation that registers the necessary
* infrastructure to support {@linkplain BeanOverride bean overriding}.
*
* @author Simon Baslé
* @author Stephane Nicoll
* @since 6.2
*/
class BeanOverrideContextCustomizer implements ContextCustomizer {
private static final String REGISTRAR_BEAN_NAME =
"org.springframework.test.context.bean.override.internalBeanOverrideRegistrar";
private static final String INFRASTRUCTURE_BEAN_NAME =
"org.springframework.test.context.bean.override.internalBeanOverridePostProcessor";
private static final String EARLY_INFRASTRUCTURE_BEAN_NAME =
"org.springframework.test.context.bean.override.internalWrapEarlyBeanPostProcessor";
private final Set<Class<?>> detectedClasses;
BeanOverrideContextCustomizer(Set<Class<?>> detectedClasses) {
this.detectedClasses = detectedClasses;
}
static void registerInfrastructure(BeanDefinitionRegistry registry, Set<Class<?>> detectedClasses) {
addInfrastructureBeanDefinition(registry, BeanOverrideRegistrar.class, REGISTRAR_BEAN_NAME,
constructorArgs -> constructorArgs.addIndexedArgumentValue(0, detectedClasses));
RuntimeBeanReference registrarReference = new RuntimeBeanReference(REGISTRAR_BEAN_NAME);
addInfrastructureBeanDefinition(
registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME,
constructorArgs -> constructorArgs.addIndexedArgumentValue(0, registrarReference));
addInfrastructureBeanDefinition(
registry, BeanOverrideBeanFactoryPostProcessor.class, INFRASTRUCTURE_BEAN_NAME,
constructorArgs -> constructorArgs.addIndexedArgumentValue(0, registrarReference));
}
private static void addInfrastructureBeanDefinition(BeanDefinitionRegistry registry,
Class<?> clazz, String beanName, Consumer<ConstructorArgumentValues> constructorArgumentsConsumer) {
if (!registry.containsBeanDefinition(beanName)) {
RootBeanDefinition definition = new RootBeanDefinition(clazz);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues();
constructorArgumentsConsumer.accept(constructorArguments);
registry.registerBeanDefinition(beanName, definition);
}
}
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
if (context instanceof BeanDefinitionRegistry registry) {
registerInfrastructure(registry, this.detectedClasses);
}
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || obj.getClass() != getClass()) {
return false;
}
BeanOverrideContextCustomizer other = (BeanOverrideContextCustomizer) obj;
return this.detectedClasses.equals(other.detectedClasses);
}
@Override
public int hashCode() {
return this.detectedClasses.hashCode();
}
}

View File

@@ -20,13 +20,10 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestContextAnnotationUtils;
/**
@@ -62,38 +59,4 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
}
}
private static final class BeanOverrideContextCustomizer implements ContextCustomizer {
private final Set<Class<?>> detectedClasses;
BeanOverrideContextCustomizer(Set<Class<?>> detectedClasses) {
this.detectedClasses = detectedClasses;
}
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
if (context instanceof BeanDefinitionRegistry registry) {
BeanOverrideRegistrar.register(registry, this.detectedClasses);
BeanOverrideBeanFactoryPostProcessor.register(registry);
}
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || obj.getClass() != getClass()) {
return false;
}
BeanOverrideContextCustomizer other = (BeanOverrideContextCustomizer) obj;
return this.detectedClasses.equals(other.detectedClasses);
}
@Override
public int hashCode() {
return this.detectedClasses.hashCode();
}
}
}

View File

@@ -33,8 +33,8 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate
/**
* Internal parsing utilities to discover the presence of
* {@link BeanOverride @BeanOverride} on fields, and create the relevant
* {@link OverrideMetadata} accordingly.
* {@link BeanOverride @BeanOverride} and create the relevant
* {@link OverrideMetadata} if necessary.
*
* @author Simon Baslé
* @author Sam Brannen
@@ -58,16 +58,13 @@ abstract class BeanOverrideParsingUtils {
boolean present = MergedAnnotations.from(field, DIRECT).isPresent(BeanOverride.class);
hasBeanOverride.compareAndSet(false, present);
});
if (hasBeanOverride.get()) {
return true;
}
return false;
return hasBeanOverride.get();
}
/**
* Parse the specified classes for the presence of fields annotated with
* {@link BeanOverride @BeanOverride}, and create an {@link OverrideMetadata}
* for each.
* for each identified field.
* @param classes the classes to parse
*/
static Set<OverrideMetadata> parse(Iterable<Class<?>> classes) {
@@ -77,7 +74,8 @@ abstract class BeanOverrideParsingUtils {
}
/**
* Convenience method to {@link #parse(Iterable) parse} a single test class.
* Convenience method to parse a single test class.
* @see #parse(Iterable)
*/
static Set<OverrideMetadata> parse(Class<?> clazz) {
return parse(List.of(clazz));
@@ -85,7 +83,6 @@ abstract class BeanOverrideParsingUtils {
private static void parseField(Field field, Class<?> testClass, Set<OverrideMetadata> metadataSet) {
AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> {
Assert.state(mergedAnnotation.isMetaPresent(), "@BeanOverride annotation must be meta-present");

View File

@@ -20,17 +20,19 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
/**
* Strategy interface for Bean Override processing and creation of
* {@link OverrideMetadata}.
* Strategy interface for Bean Override processing, providing an
* {@link OverrideMetadata} that drives how the target bean is overridden.
*
* <p>Processors are generally linked to one or more specific concrete
* annotations (meta-annotated with {@link BeanOverride @BeanOverride}) and
* concrete {@link OverrideMetadata} implementations.
* <p>At least one composed annotations meta-annotated with
* {@link BeanOverride @BeanOverride}) is a companion of this processor and
* may provide additional user settings that drive how the concrete
* {@link OverrideMetadata} is configured.
*
* <p>Implementations are required to have a no-argument constructor and be
* stateless.
*
* @author Simon Baslé
* @author Stephane Nicoll
* @since 6.2
*/
@FunctionalInterface
@@ -38,12 +40,12 @@ public interface BeanOverrideProcessor {
/**
* Create an {@link OverrideMetadata} instance for the given annotated field.
* @param overrideAnnotation the field annotation
* @param testClass the test class being processed, which can be different
* from the {@code field.getDeclaringClass()} in case the field is inherited
* from a superclass
* @param overrideAnnotation the composed annotation that defines the
* {@link BeanOverride @BeanOverride} that triggers this processor
* @param testClass the test class being processed
* @param field the annotated field
* @return a new {@link OverrideMetadata} instance
* @return the {@link OverrideMetadata} instance that should handle the
* given field
*/
OverrideMetadata createMetadata(Annotation overrideAnnotation, Class<?> testClass, Field field);
}

View File

@@ -18,7 +18,6 @@ package org.springframework.test.context.bean.override;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
@@ -26,11 +25,7 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
@@ -46,40 +41,40 @@ import org.springframework.util.StringUtils;
*/
class BeanOverrideRegistrar implements BeanFactoryAware {
static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideRegistrar.class.getName();
private final Map<OverrideMetadata, String> beanNameRegistry;
private final Map<String, OverrideMetadata> earlyOverrideMetadata;
private final Set<OverrideMetadata> overrideMetadata;
@Nullable
private ConfigurableBeanFactory beanFactory;
/**
* Construct a new registrar and immediately parse the provided classes.
* Create a new registrar and immediately parse the provided classes.
* @param classesToParse the initial set of classes that have been
* detected to contain bean overriding annotations, to be parsed immediately.
* detected to contain bean overriding annotations
*/
BeanOverrideRegistrar(Set<Class<?>> classesToParse) {
Set<OverrideMetadata> metadata = BeanOverrideParsingUtils.parse(classesToParse);
Assert.state(!metadata.isEmpty(), "Expected metadata to be produced by parser");
this.overrideMetadata = metadata;
this.beanNameRegistry = new HashMap<>();
this.earlyOverrideMetadata = new HashMap<>();
}
/**
* Return this processor's {@link OverrideMetadata} set.
*/
Set<OverrideMetadata> getOverrideMetadata() {
return this.overrideMetadata;
this.overrideMetadata = BeanOverrideParsingUtils.parse(classesToParse);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
Assert.isInstanceOf(ConfigurableBeanFactory.class, beanFactory,
"Bean OverrideRegistrar can only be used with a ConfigurableBeanFactory");
this.beanFactory = (ConfigurableBeanFactory) beanFactory;
if (!(beanFactory instanceof ConfigurableBeanFactory cbf)) {
throw new IllegalStateException("Cannot process bean override with a BeanFactory " +
"that doesn't implement ConfigurableBeanFactory: " + beanFactory.getClass());
}
this.beanFactory = cbf;
}
/**
* Return the detected {@link OverrideMetadata} instances.
*/
Set<OverrideMetadata> getOverrideMetadata() {
return this.overrideMetadata;
}
/**
@@ -87,8 +82,8 @@ class BeanOverrideRegistrar implements BeanFactoryAware {
* records and use the {@link OverrideMetadata} to create an override
* instance from the provided bean, if relevant.
*/
final Object wrapIfNecessary(Object bean, String beanName) throws BeansException {
final OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName);
Object wrapIfNecessary(Object bean, String beanName) throws BeansException {
OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName);
if (metadata != null && metadata.getStrategy() == BeanOverrideStrategy.WRAP_BEAN) {
bean = metadata.createOverride(beanName, null, bean);
Assert.state(this.beanFactory != null, "ConfigurableBeanFactory must not be null");
@@ -98,7 +93,7 @@ class BeanOverrideRegistrar implements BeanFactoryAware {
}
/**
* Register the provided {@link OverrideMetadata} and associated it with a
* Register the provided {@link OverrideMetadata} and associate it with a
* {@code beanName}.
*/
void registerNameForMetadata(OverrideMetadata metadata, String beanName) {
@@ -109,43 +104,10 @@ class BeanOverrideRegistrar implements BeanFactoryAware {
* Mark the provided {@link OverrideMetadata} and {@code beanName} as "wrap
* early", allowing for later bean override using {@link #wrapIfNecessary(Object, String)}.
*/
public void markWrapEarly(OverrideMetadata metadata, String beanName) {
void markWrapEarly(OverrideMetadata metadata, String beanName) {
this.earlyOverrideMetadata.put(beanName, metadata);
}
/**
* Register a bean definition for a {@link BeanOverrideRegistrar} if it does
* not yet exist. Additionally, each call adds the provided
* {@code detectedTestClasses} to the set that will be used as constructor
* argument.
* <p>The resulting complete set of test classes will be parsed as soon as
* the {@link BeanOverrideRegistrar} is constructed.
* @param registry the bean definition registry
* @param detectedTestClasses a partial {@link Set} of {@link Class classes}
* that are expected to contain bean overriding annotations
*/
public static void register(BeanDefinitionRegistry registry, @Nullable Set<Class<?>> detectedTestClasses) {
BeanDefinition definition;
if (!registry.containsBeanDefinition(BeanOverrideRegistrar.INFRASTRUCTURE_BEAN_NAME)) {
definition = new RootBeanDefinition(BeanOverrideRegistrar.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues();
constructorArguments.addIndexedArgumentValue(0, new LinkedHashSet<Class<?>>());
registry.registerBeanDefinition(INFRASTRUCTURE_BEAN_NAME, definition);
}
else {
definition = registry.getBeanDefinition(BeanOverrideRegistrar.INFRASTRUCTURE_BEAN_NAME);
}
ConstructorArgumentValues.ValueHolder constructorArg =
definition.getConstructorArgumentValues().getIndexedArgumentValue(0, Set.class);
@SuppressWarnings({"unchecked", "NullAway"})
Set<Class<?>> existing = (Set<Class<?>>) constructorArg.getValue();
if (detectedTestClasses != null && existing != null) {
existing.addAll(detectedTestClasses);
}
}
void inject(Object target, OverrideMetadata overrideMetadata) {
String beanName = this.beanNameRegistry.get(overrideMetadata);
Assert.state(StringUtils.hasLength(beanName),
@@ -170,4 +132,5 @@ class BeanOverrideRegistrar implements BeanFactoryAware {
throw new BeanCreationException("Could not inject field '" + field + "'", ex);
}
}
}

View File

@@ -17,30 +17,34 @@
package org.springframework.test.context.bean.override;
/**
* Strategies for bean override instantiation.
* Strategies for bean override processing.
*
* @author Simon Baslé
* @author Stephane Nicoll
* @since 6.2
*/
public enum BeanOverrideStrategy {
/**
* Replace a given bean definition, immediately preparing a singleton instance.
* <p>Requires that the original bean definition exists.
* <p>Fails if the original bean definition exists. To create a new bean
* definition in such a case, use {@link #REPLACE_OR_CREATE_DEFINITION}.
*/
REPLACE_DEFINITION,
/**
* Replace a given bean definition, immediately preparing a singleton instance.
* <p>If the original bean definition does not exist, an override definition
* will be created instead of failing.
* Replace or create a given bean definition, immediately preparing a
* singleton instance.
* <p>Contrary to {@link #REPLACE_DEFINITION} this create a new bean
* definition if the target bean definition does not exist rather than
* failing.
*/
REPLACE_OR_CREATE_DEFINITION,
/**
* Intercept and process an early bean reference rather than a bean
* definition, allowing variants of bean overriding to wrap the instance
* (e.g. to delegate to actual methods in the context of a mocking "spy").
* definition, allowing variants of bean overriding to wrap the instance.
* For instance, to delegate to actual methods in the context of a mocking "spy".
*/
WRAP_BEAN

View File

@@ -20,17 +20,15 @@ import java.lang.reflect.Field;
import java.util.function.BiConsumer;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.util.ReflectionUtils;
/**
* {@code TestExecutionListener} that enables Bean Override support in tests,
* injecting overridden beans in appropriate fields of the test instance.
*
* <p>Some Bean Override implementations might additionally require the use of
* additional listeners, which should be mentioned in the javadoc for the
* corresponding annotations.
* {@link TestExecutionListener} implementation that enables Bean Override
* support in tests, injecting overridden beans in appropriate fields of the
* test instance.
*
* @author Simon Baslé
* @since 6.2
@@ -56,9 +54,9 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList
}
/**
* Using a registered {@link BeanOverrideBeanFactoryPostProcessor}, find metadata
* associated with the current test class and ensure fields are injected
* with the overridden bean instance.
* Process the test instance and make sure that flagged fields for bean
* overriding are processed. Each field get is value updated with the
* overridden bean instance.
*/
protected void injectFields(TestContext testContext) {
postProcessFields(testContext, (testMetadata, overrideRegistrar) -> overrideRegistrar.inject(
@@ -66,9 +64,9 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList
}
/**
* Using a registered {@link BeanOverrideBeanFactoryPostProcessor}, find metadata
* associated with the current test class and ensure fields are nulled out
* and then re-injected with the overridden bean instance.
* Process the test instance and make sure that flagged fields for bean
* overriding are processed. If a fresh instance is required, the field
* is nulled out and then re-injected with the overridden bean instance.
* <p>This method does nothing if the
* {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
* attribute is not present in the {@code TestContext}.

View File

@@ -69,7 +69,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
* @param methodNames a set of supported names for the factory method
* @return the corresponding factory method
* @throws IllegalStateException if a matching factory method cannot
* be found or multiple methods have a match
* be found or multiple methods match
*/
static Method findTestBeanFactoryMethod(Class<?> clazz, Class<?> methodReturnType, String... methodNames) {
Assert.isTrue(methodNames.length > 0, "At least one candidate method name is required");

View File

@@ -151,7 +151,8 @@ class MockitoBeanMetadata extends MockitoMetadata {
if (this.serializable) {
settings.serializable();
}
return (T) mock(getBeanType().resolve(), settings);
Class<?> targetType = getBeanType().resolve();
return (T) mock(targetType, settings);
}
}

View File

@@ -23,11 +23,13 @@ import org.springframework.core.ResolvableType;
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
/**
* A {@link BeanOverrideProcessor} for mockito-related annotations
* ({@link MockitoBean} and {@link MockitoSpyBean}).
* {@link BeanOverrideProcessor} implementation for Mockito support. Both mocking
* and spying are supported.
*
* @author Simon Baslé
* @since 6.2
* @see MockitoBean
* @see MockitoSpyBean
*/
class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {

View File

@@ -29,7 +29,7 @@ import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Base class for {@link MockitoBeanMetadata} and {@link MockitoSpyBeanMetadata}.
* Base class for Mockito override metadata.
*
* @author Phillip Webb
* @since 6.2
@@ -94,7 +94,7 @@ abstract class MockitoMetadata extends OverrideMetadata {
}
@Override
public boolean equals(Object obj) {
public boolean equals(@Nullable Object obj) {
if (obj == this) {
return true;
}

View File

@@ -31,6 +31,7 @@ import org.springframework.core.ResolvableType;
import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
import org.springframework.test.context.bean.override.BeanOverrideStrategy;
import org.springframework.test.context.bean.override.OverrideMetadata;
import org.springframework.test.util.AopTestUtils;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -39,9 +40,10 @@ import org.springframework.util.StringUtils;
import static org.mockito.Mockito.mock;
/**
* A complete definition that can be used to create a Mockito spy.
* {@link OverrideMetadata} for Mockito {@code spy} support.
*
* @author Phillip Webb
* @author Simon Baslé
* @since 6.2
*/
class MockitoSpyBeanMetadata extends MockitoMetadata {
@@ -52,7 +54,6 @@ class MockitoSpyBeanMetadata extends MockitoMetadata {
}
MockitoSpyBeanMetadata(String name, MockReset reset, boolean proxyTargetAware, Field field, ResolvableType typeToSpy) {
super(name, reset, proxyTargetAware, field, typeToSpy, BeanOverrideStrategy.WRAP_BEAN);
Assert.notNull(typeToSpy, "typeToSpy must not be null");
}