Browse Source

Polish

pull/32622/head
Stéphane Nicoll 2 years ago
parent
commit
2e3a923225
  1. 41
      framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc
  2. 16
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java
  3. 79
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java
  4. 105
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java
  5. 37
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java
  6. 15
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java
  7. 22
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java
  8. 77
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java
  9. 18
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideStrategy.java
  10. 22
      spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java
  11. 2
      spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java
  12. 3
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanMetadata.java
  13. 6
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java
  14. 4
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoMetadata.java
  15. 5
      spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanMetadata.java
  16. 4
      spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java

41
framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc

@ -48,7 +48,7 @@ Java:: @@ -48,7 +48,7 @@ Java::
======
NOTE: The method to invoke is searched in the test class and any enclosing class it might
have, as well as its hierarchy. This typically allows nested test class to provide the
have, as well as its hierarchy. This typically allows nested test class to rely on the
method to use in the root test class.
[[spring-testing-annotation-beanoverriding-mockitobean]]
@ -101,33 +101,34 @@ Java:: @@ -101,33 +101,34 @@ Java::
The three annotations introduced above build upon the `@BeanOverride` meta-annotation
and associated infrastructure, which allows to define custom bean overriding variants.
In order to provide an extension, three classes are needed:
To create an extension, the following is needed:
- A concrete `BeanOverrideProcessor` implementation, `P`.
- One or more concrete `OverrideMetadata` implementations created by said processor.
- An annotation meta-annotated with `@BeanOverride(P.class)`.
- An annotation meta-annotated with `@BeanOverride` that defines the
`BeanOverrideProcessor` to use.
- The `BeanOverrideProcessor` implementation itself.
- One or more concrete `OverrideMetadata` implementations provided by the processor.
The Spring TestContext Framework includes infrastructure classes that support bean
overriding: a `BeanFactoryPostProcessor`, a `TestExecutionListener` and a `ContextCustomizerFactory`.
overriding: a `BeanFactoryPostProcessor`, a `TestExecutionListener` and a
`ContextCustomizerFactory`.
The later two are automatically registered via the Spring TestContext Framework
`spring.factories` file, and are responsible for setting up the rest of the infrastructure.
The test classes are parsed looking for any field meta-annotated with `@BeanOverride`,
instantiating the relevant `BeanOverrideProcessor` in order to register an `OverrideMetadata`.
instantiating the relevant `BeanOverrideProcessor` in order to register an
`OverrideMetadata`.
Then the `BeanOverrideBeanFactoryPostProcessor` will use that information to alter the
Context, registering and replacing bean definitions as influenced by each metadata
context, registering and replacing bean definitions as defined by each metadata
`BeanOverrideStrategy`:
- `REPLACE_DEFINITION`: the bean post-processor replaces the bean definition.
If it is not present in the context, an exception is thrown.
- `CREATE_OR_REPLACE_DEFINITION`: same as above but if the bean definition is not present
in the context, one is created
- `WRAP_EARLY_BEAN`: an original instance is obtained and passed to the `OverrideMetadata`
when the override instance is created.
NOTE: The Bean Overriding infrastructure doesn't include any bean resolution step
(unlike e.g. an `@Autowired`-annotated field). As such, the name of the bean to override
MUST be somehow provided to or computed by the `BeanOverrideProcessor`. Typically, the end
user provides the name as part of the custom annotation's attributes, or the annotated
field's name.
- `REPLACE_DEFINITION`: replaces the bean definition. If it is not present in the
context, an exception is thrown.
- `CREATE_OR_REPLACE_DEFINITION`: replaces the bean definition if the bean definition
does not exist, or create one if it is not.
- `WRAP_BEAN`: get the original instance early so that it can be wrapped.
NOTE: The Bean Overriding infrastructure does not include any bean resolution step,
unlike an `@Autowired`-annotated field for instance. As such, the name of the bean to
override must be somehow provided to or computed by the `BeanOverrideProcessor`.
Typically, the user provides the name one way or the other.

16
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java

@ -22,15 +22,13 @@ import java.lang.annotation.RetentionPolicy; @@ -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; @@ -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();

79
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java

@ -22,31 +22,26 @@ import java.util.LinkedHashSet; @@ -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; @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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 {

105
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java

@ -0,0 +1,105 @@ @@ -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();
}
}

37
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java

@ -20,13 +20,10 @@ import java.util.LinkedHashSet; @@ -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 { @@ -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();
}
}
}

15
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java

@ -33,8 +33,8 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate @@ -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 { @@ -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 { @@ -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 { @@ -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");

22
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java

@ -20,17 +20,19 @@ import java.lang.annotation.Annotation; @@ -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 { @@ -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);
}

77
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java

@ -18,7 +18,6 @@ package org.springframework.test.context.bean.override; @@ -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; @@ -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,49 +41,49 @@ import org.springframework.util.StringUtils; @@ -46,49 +41,49 @@ 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<>();
this.overrideMetadata = BeanOverrideParsingUtils.parse(classesToParse);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
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 this processor's {@link OverrideMetadata} set.
* Return the detected {@link OverrideMetadata} instances.
*/
Set<OverrideMetadata> getOverrideMetadata() {
return this.overrideMetadata;
}
@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;
}
/**
* Check {@link #markWrapEarly(OverrideMetadata, String) early override}
* 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 { @@ -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 { @@ -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 { @@ -170,4 +132,5 @@ class BeanOverrideRegistrar implements BeanFactoryAware {
throw new BeanCreationException("Could not inject field '" + field + "'", ex);
}
}
}

18
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideStrategy.java

@ -17,30 +17,34 @@ @@ -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

22
spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java

@ -20,17 +20,15 @@ import java.lang.reflect.Field; @@ -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 @@ -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 @@ -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}.

2
spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java

@ -69,7 +69,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor { @@ -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");

3
spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanMetadata.java

@ -151,7 +151,8 @@ class MockitoBeanMetadata extends MockitoMetadata { @@ -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);
}
}

6
spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java

@ -23,11 +23,13 @@ import org.springframework.core.ResolvableType; @@ -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 {

4
spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoMetadata.java

@ -29,7 +29,7 @@ import org.springframework.util.ObjectUtils; @@ -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 { @@ -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;
}

5
spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanMetadata.java

@ -31,6 +31,7 @@ import org.springframework.core.ResolvableType; @@ -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; @@ -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 { @@ -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");
}

4
spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java

@ -177,8 +177,7 @@ class BeanOverrideBeanFactoryPostProcessorTests { @@ -177,8 +177,7 @@ class BeanOverrideBeanFactoryPostProcessorTests {
private AnnotationConfigApplicationContext createContext(Class<?>... classes) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
BeanOverrideRegistrar.register(context, Set.of(classes));
BeanOverrideBeanFactoryPostProcessor.register(context);
BeanOverrideContextCustomizer.registerInfrastructure(context, Set.of(classes));
return context;
}
@ -193,6 +192,7 @@ class BeanOverrideBeanFactoryPostProcessorTests { @@ -193,6 +192,7 @@ class BeanOverrideBeanFactoryPostProcessorTests {
*/
static final SomeInterface OVERRIDE = new SomeImplementation();
static final ExampleService OVERRIDE_SERVICE = new FailingExampleService();
static class ReplaceBeans {

Loading…
Cancel
Save