This commit is contained in:
Stéphane Nicoll 2024-03-09 17:27:44 +01:00
parent 6b64e9992a
commit 8172d7adfe
7 changed files with 99 additions and 62 deletions

View File

@ -100,12 +100,12 @@ class BeanOverrideParser {
MergedAnnotations.from(field, MergedAnnotations.SearchStrategy.DIRECT)
.stream(BeanOverride.class)
.map(bo -> {
var a = bo.getMetaSource();
MergedAnnotation<?> a = bo.getMetaSource();
Assert.notNull(a, "BeanOverride annotation must be meta-present");
return new AnnotationPair(a.synthesize(), bo);
})
.forEach(pair -> {
var metaAnnotation = pair.metaAnnotation().synthesize();
BeanOverride metaAnnotation = pair.metaAnnotation().synthesize();
final BeanOverrideProcessor processor = getProcessorInstance(metaAnnotation.value());
if (processor == null) {
return;

View File

@ -26,7 +26,8 @@ import org.springframework.core.annotation.MergedAnnotation;
/**
* An interface for Bean Overriding concrete processing.
* Processors are generally linked to one or more specific concrete annotations
*
* <p>Processors are generally linked to one or more specific concrete annotations
* (meta-annotated with {@link BeanOverride}) and specify different steps in the
* process of parsing these annotations, ultimately creating
* {@link OverrideMetadata} which will be used to instantiate the overrides.
@ -57,8 +58,8 @@ public interface BeanOverrideProcessor {
* {@link #getOrDeduceType(Field, Annotation, Class) type}.
* Specific implementations of metadata can have state to be used during
* override {@link OverrideMetadata#createOverride(String, BeanDefinition,
* Object) instance creation} (e.g. from further parsing the annotation or
* the annotated field).
* Object) instance creation}, that is from further parsing the annotation or
* the annotated field.
* @param field the annotated field
* @param overrideAnnotation the field annotation
* @param typeToOverride the target type

View File

@ -26,20 +26,23 @@ package org.springframework.test.bean.override;
public enum BeanOverrideStrategy {
/**
* Replace a given bean's definition, immediately preparing a singleton
* Replace a given bean definition, immediately preparing a singleton
* instance. Enforces the original bean definition to exist.
*/
REPLACE_DEFINITION,
/**
* Replace a given bean's definition, immediately preparing a singleton
* Replace a given bean definition, immediately preparing a singleton
* instance. If the original bean definition does not exist, create the
* override definition instead of failing.
*/
REPLACE_OR_CREATE_DEFINITION,
/**
* Intercept and wrap the actual bean instance upon creation, during
* {@link org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String)
* early bean definition}.
*/
WRAP_EARLY_BEAN;
WRAP_EARLY_BEAN
}

View File

@ -26,8 +26,8 @@ import org.springframework.test.context.support.DependencyInjectionTestExecution
import org.springframework.util.ReflectionUtils;
/**
* A {@link TestExecutionListener} that enables Bean Override support in
* tests, injecting overridden beans in appropriate fields.
* A {@link TestExecutionListener} implementation that enables Bean Override
* support in tests, injecting overridden beans in appropriate fields.
*
* <p>Some flavors of Bean Override might additionally require the use of
* additional listeners, which should be mentioned in the annotation(s) javadoc.

View File

@ -34,11 +34,14 @@ import org.springframework.lang.Nullable;
public abstract class OverrideMetadata {
private final Field field;
private final Annotation overrideAnnotation;
private final ResolvableType typeToOverride;
private final BeanOverrideStrategy strategy;
public OverrideMetadata(Field field, Annotation overrideAnnotation,
protected OverrideMetadata(Field field, Annotation overrideAnnotation,
ResolvableType typeToOverride, BeanOverrideStrategy strategy) {
this.field = field;
this.overrideAnnotation = overrideAnnotation;
@ -47,50 +50,46 @@ public abstract class OverrideMetadata {
}
/**
* Define a short human-readable description of the kind of override this
* OverrideMetadata is about. This is especially useful for
* {@link BeanOverrideProcessor} that produce several subtypes of metadata
* (e.g. "mock" vs "spy").
* Return a short human-readable description of the kind of override this
* instance handles.
*/
public abstract String getBeanOverrideDescription();
/**
* Provide the expected bean name to override. Typically, this is either
* Return the expected bean name to override. Typically, this is either
* explicitly set in the concrete annotations or defined by the annotated
* field's name.
* @return the expected bean name, not null
* @return the expected bean name
*/
protected String getExpectedBeanName() {
return this.field.getName();
}
/**
* The field annotated with a {@link BeanOverride}-compatible annotation.
* @return the annotated field
* Return the annotated {@link Field}.
*/
public Field field() {
return this.field;
}
/**
* The concrete override annotation, i.e. the one meta-annotated with
* {@link BeanOverride}.
* Return the concrete override annotation, that is the one meta-annotated
* with {@link BeanOverride}.
*/
public Annotation overrideAnnotation() {
return this.overrideAnnotation;
}
/**
* The type to override, as a {@link ResolvableType}.
* Return the bean {@link ResolvableType type} to override.
*/
public ResolvableType typeToOverride() {
return this.typeToOverride;
}
/**
* Define the broad {@link BeanOverrideStrategy} for this
* {@link OverrideMetadata}, as a hint on how and when the override instance
* should be created.
* Return the {@link BeanOverrideStrategy} for this instance, as a hint on
* how and when the override instance should be created.
*/
public final BeanOverrideStrategy getBeanOverrideStrategy() {
return this.strategy;
@ -99,7 +98,7 @@ public abstract class OverrideMetadata {
/**
* Create an override instance from this {@link OverrideMetadata},
* optionally provided with an existing {@link BeanDefinition} and/or an
* original instance (i.e. a singleton or an early wrapped instance).
* 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
@ -129,7 +128,7 @@ public abstract class OverrideMetadata {
if (obj == null || !getClass().isAssignableFrom(obj.getClass())) {
return false;
}
var that = (OverrideMetadata) obj;
OverrideMetadata that = (OverrideMetadata) obj;
return Objects.equals(this.field, that.field) &&
Objects.equals(this.overrideAnnotation, that.overrideAnnotation) &&
Objects.equals(this.strategy, that.strategy) &&

View File

@ -25,29 +25,63 @@ import java.lang.annotation.Target;
import org.springframework.test.bean.override.BeanOverride;
/**
* Mark a field to represent a "method" bean override of the bean of the same
* name and inject the field with the overriding instance.
* Mark a field to override a bean instance in the {@code BeanFactory}.
*
* <p>The instance is created from a static method in the declaring class which
* return type is compatible with the annotated field and which name follows the
* convention:
* <p>The instance is created from a no-arg static method in the declaring
* class whose return type is compatible with the annotated field. The method
* is deduced as follows:
* <ul>
* <li>if the annotation's {@link #methodName()} is specified,
* look for that one.</li>
* <li>if not, look for exactly one method named with the
* {@link #CONVENTION_SUFFIX} suffix and either:</li>
* <ul>
* <li>starting with the annotated field name</li>
* <li>starting with the bean name</li>
* </ul>
* <li>if the {@link #methodName()} is specified, look for a static method with
* that name.</li>
* <li>if not, look for exactly one static method named with a suffix equal to
* {@value #CONVENTION_SUFFIX} and either starting with the annotated field
* name, or starting with the bean name.</li>
* </ul>
*
* <p>The annotated field's name is interpreted to be the name of the original
* bean to override, unless the annotation's {@link #name()} is specified.
* <p>Consider the following example:
*
* <pre><code>
* class CustomerServiceTests {
*
* &#064;TestBean
* private CustomerRepository repository;
*
* // Tests
*
* private static CustomerRepository repositoryTestOverride() {
* return new TestCustomerRepository();
* }
* }</code></pre>
*
* <p>In the example above, the {@code repository} bean is replaced by the
* instance generated by the {@code repositoryTestOverride} method. Not only
* the overridden instance is injected in the {@code repository} field, but it
* is also replaced in the {@code BeanFactory} so that other injection points
* for that bean use the override.
*
* <p>To make things more explicit, the method name can be set, as shown in the
* following example:
*
* <pre><code>
* class CustomerServiceTests {
*
* &#064;TestBean(methodName = "createTestCustomerRepository")
* private CustomerRepository repository;
*
* // Tests
*
* private static CustomerRepository createTestCustomerRepository() {
* return new TestCustomerRepository();
* }
* }</code></pre>
*
* <p>By default, the name of the bean is inferred from the name of the annotated
* field. To use a different bean name, set the {@link #name()} property.
*
* @see TestBeanOverrideProcessor
* @author Simon Baslé
* @author Stephane Nicoll
* @since 6.2
* @see TestBeanOverrideProcessor
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@ -56,23 +90,23 @@ import org.springframework.test.bean.override.BeanOverride;
public @interface TestBean {
/**
* The method suffix expected as a convention in static methods which
* provides an override instance.
* Required suffix for a method that overrides a bean instance that is
* detected by convention.
*/
String CONVENTION_SUFFIX = "TestOverride";
/**
* The name of a static method to look for in the Configuration, which will
* be used to instantiate the override bean and inject the annotated field.
* <p> Default is {@code ""} (the empty String), which is translated into
* the annotated field's name concatenated with the
* {@link #CONVENTION_SUFFIX}.
* Name of a static method to look for in the test, which will be used to
* instantiate the bean to override.
* <p>Default to {@code ""} (the empty String), which detects the method
* to us by convention.
*/
String methodName() default "";
/**
* The name of the original bean to override, or {@code ""} (the empty
* String) to deduce the name from the annotated field.
* Name of the bean to override.
* <p>Default to {@code ""} (the empty String) to use the name of the
* annotated field.
*/
String name() default "";
}

View File

@ -25,7 +25,6 @@ import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.ResolvableType;
@ -37,8 +36,8 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Simple {@link BeanOverrideProcessor} primarily made to work with the
* {@link TestBean} annotation but can work with arbitrary override annotations
* {@link BeanOverrideProcessor} implementation primarily made to work with
* {@link TestBean @TestBean}, but can work with arbitrary override annotations
* provided the annotated class has a relevant method according to the
* convention documented in {@link TestBean}.
*
@ -48,19 +47,20 @@ import org.springframework.util.StringUtils;
public class TestBeanOverrideProcessor implements BeanOverrideProcessor {
/**
* Ensures the {@code enclosingClass} has a static, no-arguments method with
* the provided {@code expectedMethodReturnType} and exactly one of the
* Ensure the given {@code enclosingClass} has a static, no-arguments method
* with the given {@code expectedMethodReturnType} and exactly one of the
* {@code expectedMethodNames}.
*/
public static Method ensureMethod(Class<?> enclosingClass, Class<?> expectedMethodReturnType,
String... expectedMethodNames) {
Assert.isTrue(expectedMethodNames.length > 0, "At least one expectedMethodName is required");
Set<String> expectedNames = new LinkedHashSet<>(Arrays.asList(expectedMethodNames));
final List<Method> found = Arrays.stream(enclosingClass.getDeclaredMethods())
.filter(m -> Modifier.isStatic(m.getModifiers()))
.filter(m -> expectedNames.contains(m.getName()) && expectedMethodReturnType
.isAssignableFrom(m.getReturnType()))
.collect(Collectors.toList());
.filter(method -> Modifier.isStatic(method.getModifiers()))
.filter(method -> expectedNames.contains(method.getName())
&& expectedMethodReturnType.isAssignableFrom(method.getReturnType()))
.toList();
Assert.state(found.size() == 1, () -> "Found " + found.size() + " static methods " +
"instead of exactly one, matching a name in " + expectedNames + " with return type " +
@ -87,7 +87,7 @@ public class TestBeanOverrideProcessor implements BeanOverrideProcessor {
}
// otherwise defer the resolution of the static method until OverrideMetadata#createOverride
return new MethodConventionOverrideMetadata(field, null, null, overrideAnnotation,
typeToOverride);
typeToOverride);
}
static final class MethodConventionOverrideMetadata extends OverrideMetadata {