Polish "Bean Overriding in Tests" support

This commit is contained in:
Sam Brannen
2024-04-16 17:08:15 +02:00
parent ce8758b83c
commit 8727d723f3
14 changed files with 134 additions and 123 deletions

View File

@@ -22,25 +22,23 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Mark an annotation as eligible for Bean Override processing.
* Mark a composed annotation as eligible for Bean Override processing.
*
* <p>Specifying this annotation triggers the defined {@link BeanOverrideProcessor}
* <p>Specifying this annotation triggers the configured {@link BeanOverrideProcessor}
* which must be capable of handling the composed annotation and its attributes.
*
* <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}.
* <p>Since the composed annotation should only be applied to fields, it is
* expected that it has a {@link Target} of {@link ElementType#FIELD FIELD}.
*
* @author Simon Baslé
* @since 6.2
* @see BeanOverrideBeanFactoryPostProcessor
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface BeanOverride {
/**
* The {@link BeanOverrideProcessor} implementation to trigger against
* the composed annotation.
* The {@link BeanOverrideProcessor} implementation to use.
*/
Class<? extends BeanOverrideProcessor> value();

View File

@@ -41,7 +41,7 @@ import org.springframework.util.StringUtils;
/**
* A {@link BeanFactoryPostProcessor} implementation that processes test classes
* and adapt the {@link BeanFactory} for any {@link BeanOverride} it may define.
* and adapts 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
@@ -51,7 +51,7 @@ import org.springframework.util.StringUtils;
*
* <p>The provided classes are fully parsed at creation to build a metadata set.
* This processor implements several {@link BeanOverrideStrategy overriding
* strategy} and chooses the correct one according to each override metadata
* strategies} and chooses the correct one according to each override metadata's
* {@link OverrideMetadata#getStrategy()} method. Additionally, it provides
* support for injecting the overridden bean instances into their corresponding
* annotated {@link Field fields}.
@@ -61,7 +61,6 @@ import org.springframework.util.StringUtils;
*/
class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {
private final BeanOverrideRegistrar overrideRegistrar;
@@ -195,14 +194,13 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
static final class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor,
PriorityOrdered {
private final BeanOverrideRegistrar overrideRegistrar;
private final Map<String, Object> earlyReferences = new ConcurrentHashMap<>(16);
private final Map<String, Object> earlyReferences;
private final BeanOverrideRegistrar overrideRegistrar;
private WrapEarlyBeanPostProcessor(BeanOverrideRegistrar registrar) {
this.overrideRegistrar = registrar;
this.earlyReferences = new ConcurrentHashMap<>(16);
}

View File

@@ -69,6 +69,7 @@ class BeanOverrideContextCustomizer implements ContextCustomizer {
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);

View File

@@ -84,12 +84,11 @@ 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");
MergedAnnotation<?> metaSource = mergedAnnotation.getMetaSource();
Assert.state(metaSource != null, "@BeanOverride annotation must be meta-present");
BeanOverride beanOverride = mergedAnnotation.synthesize();
BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value());
MergedAnnotation<?> metaSource = mergedAnnotation.getMetaSource();
Assert.state(metaSource != null, "Meta-annotation source must not be null");
Annotation composedAnnotation = metaSource.synthesize();
Assert.state(overrideAnnotationFound.compareAndSet(false, true),

View File

@@ -20,11 +20,11 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
/**
* Strategy interface for Bean Override processing, providing an
* Strategy interface for Bean Override processing, providing
* {@link OverrideMetadata} that drives how the target bean is overridden.
*
* <p>At least one composed annotations meta-annotated with
* {@link BeanOverride @BeanOverride}) is a companion of this processor and
* <p>At least one composed annotation that is meta-annotated with
* {@link BeanOverride @BeanOverride} must be a companion of this processor and
* may provide additional user settings that drive how the concrete
* {@link OverrideMetadata} is configured.
*

View File

@@ -41,23 +41,22 @@ import org.springframework.util.StringUtils;
*/
class BeanOverrideRegistrar implements BeanFactoryAware {
private final Map<OverrideMetadata, String> beanNameRegistry;
private final Map<OverrideMetadata, String> beanNameRegistry = new HashMap<>();
private final Map<String, OverrideMetadata> earlyOverrideMetadata;
private final Map<String, OverrideMetadata> earlyOverrideMetadata = new HashMap<>();
private final Set<OverrideMetadata> overrideMetadata;
@Nullable
private ConfigurableBeanFactory beanFactory;
/**
* 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
*/
BeanOverrideRegistrar(Set<Class<?>> classesToParse) {
this.beanNameRegistry = new HashMap<>();
this.earlyOverrideMetadata = new HashMap<>();
this.overrideMetadata = BeanOverrideParsingUtils.parse(classesToParse);
}
@@ -71,7 +70,7 @@ class BeanOverrideRegistrar implements BeanFactoryAware {
}
/**
* Return the detected {@link OverrideMetadata} instances.
* Get the detected {@link OverrideMetadata} instances.
*/
Set<OverrideMetadata> getOverrideMetadata() {
return this.overrideMetadata;
@@ -85,16 +84,16 @@ class BeanOverrideRegistrar implements BeanFactoryAware {
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");
bean = metadata.createOverride(beanName, null, bean);
metadata.track(bean, this.beanFactory);
}
return bean;
}
/**
* Register the provided {@link OverrideMetadata} and associate it with a
* {@code beanName}.
* Register the provided {@link OverrideMetadata} and associate it with the
* supplied {@code beanName}.
*/
void registerNameForMetadata(OverrideMetadata metadata, String beanName) {
this.beanNameRegistry.put(metadata, beanName);
@@ -119,7 +118,7 @@ class BeanOverrideRegistrar implements BeanFactoryAware {
try {
ReflectionUtils.makeAccessible(field);
Object existingValue = ReflectionUtils.getField(field, target);
Assert.state(this.beanFactory != null, "beanFactory must not be null");
Assert.state(this.beanFactory != null, "ConfigurableBeanFactory must not be null");
Object bean = this.beanFactory.getBean(beanName, field.getType());
if (existingValue == bean) {
return;

View File

@@ -35,7 +35,7 @@ public enum BeanOverrideStrategy {
/**
* Replace or create a given bean definition, immediately preparing a
* singleton instance.
* <p>Contrary to {@link #REPLACE_DEFINITION} this create a new bean
* <p>Contrary to {@link #REPLACE_DEFINITION}, this creates a new bean
* definition if the target bean definition does not exist rather than
* failing.
*/
@@ -43,8 +43,9 @@ public enum BeanOverrideStrategy {
/**
* Intercept and process an early bean reference rather than a bean
* definition, allowing variants of bean overriding to wrap the instance.
* For instance, to delegate to actual methods in the context of a mocking "spy".
* definition, allowing variants of bean overriding to wrap the instance
* &mdash; for example, to delegate to actual methods in the context of a
* mocking "spy".
*/
WRAP_BEAN

View File

@@ -20,15 +20,13 @@ 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;
/**
* {@link TestExecutionListener} implementation that enables Bean Override
* support in tests, injecting overridden beans in appropriate fields of the
* test instance.
* {@code TestExecutionListener} that enables Bean Override support in tests,
* injecting overridden beans in appropriate fields of the test instance.
*
* @author Simon Baslé
* @since 6.2
@@ -54,9 +52,9 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList
}
/**
* 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.
* Process the test instance and make sure that fields flagged for bean
* overriding are processed.
* <p>Each field's value will be updated with the overridden bean instance.
*/
protected void injectFields(TestContext testContext) {
postProcessFields(testContext, (testMetadata, overrideRegistrar) -> overrideRegistrar.inject(
@@ -64,9 +62,10 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList
}
/**
* 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.
* Process the test instance and make sure that fields flagged for bean
* overriding are processed.
* <p>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

@@ -26,15 +26,15 @@ import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
/**
* Metadata for Bean Override injection points, also responsible for the
* creation of the overriding instance.
* Metadata for Bean Override injection points, also responsible for creation of
* the overriding instance.
*
* <p><strong>WARNING</strong>: implementations are used as a cache key and
* must implement proper {@code equals} and {@code hashCode}t methods.
* must implement proper {@code equals()} and {@code hashCode()} methods.
*
* <p>Specific implementations of metadata can have state to be used during
* override {@linkplain #createOverride(String, BeanDefinition, Object)
* instance creation} &mdash; for example, from further parsing of the
* instance creation} &mdash; for example, based on further parsing of the
* annotation or the annotated field.
*
* @author Simon Baslé
@@ -58,31 +58,32 @@ public abstract class OverrideMetadata {
}
/**
* Return the bean name to override.
* Get the bean name to override.
* <p>Defaults to the name of the {@link #getField() field}.
*/
protected String getBeanName() {
return this.field.getName();
}
/**
* Return the bean {@link ResolvableType type} to override.
* Get the bean {@linkplain ResolvableType type} to override.
*/
public ResolvableType getBeanType() {
public final ResolvableType getBeanType() {
return this.beanType;
}
/**
* Return the annotated {@link Field}.
* Get the annotated {@link Field}.
*/
public Field getField() {
public final Field getField() {
return this.field;
}
/**
* Return the {@link BeanOverrideStrategy} for this instance, as a hint on
* Get the {@link BeanOverrideStrategy} for this instance, as a hint on
* how and when the override instance should be created.
*/
public BeanOverrideStrategy getStrategy() {
public final BeanOverrideStrategy getStrategy() {
return this.strategy;
}
@@ -91,9 +92,9 @@ public abstract class OverrideMetadata {
* optionally provided with an existing {@link BeanDefinition} and/or an
* original instance, that is a singleton or an early wrapped instance.
* @param beanName the name of the bean being overridden
* @param existingBeanDefinition an existing bean definition for that bean
* name, or {@code null} if not relevant
* @param existingBeanInstance an existing instance for that bean name,
* @param existingBeanDefinition an existing bean definition for the supplied
* bean name, or {@code null} if not relevant
* @param existingBeanInstance an existing instance for the supplied bean name
* for wrapping purposes, or {@code null} if irrelevant
* @return the instance with which to override the bean
*/