diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc index 29527970032..3639b88fb06 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-beanoverriding.adoc @@ -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:: 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. \ No newline at end of file + - `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. \ No newline at end of file diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java index 2bfd17a7fed..a92c19c9da6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java @@ -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. * - *

This meta-annotation specifies a {@link BeanOverrideProcessor} class which - * must be capable of handling the composed annotation that is meta-annotated - * with {@code @BeanOverride}. + *

Specifying this annotation triggers the defined {@link BeanOverrideProcessor} + * which must be capable of handling the composed annotation and its attributes. * - *

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}. + *

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 value(); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java index 60f914df6ff..dc52c828b32 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java @@ -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. * *

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 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 getExistingBeanNames(DefaultListableBeanFactory beanFactory, ResolvableType resolvableType) { + private Set getExistingBeanNames(ConfigurableListableBeanFactory beanFactory, ResolvableType resolvableType) { Set 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}. - *

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 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 { diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java new file mode 100644 index 00000000000..4c1d9440f16 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java @@ -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> detectedClasses; + + BeanOverrideContextCustomizer(Set> detectedClasses) { + this.detectedClasses = detectedClasses; + } + + static void registerInfrastructure(BeanDefinitionRegistry registry, Set> 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 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(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java index 6cddebf650c..73b072a62be 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactory.java @@ -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> detectedClasses; - - BeanOverrideContextCustomizer(Set> 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(); - } - } - } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java index c6ee7e71661..e9156cecbc4 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java +++ b/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 /** * 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 parse(Iterable> 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 parse(Class clazz) { return parse(List.of(clazz)); @@ -85,7 +83,6 @@ abstract class BeanOverrideParsingUtils { private static void parseField(Field field, Class testClass, Set metadataSet) { AtomicBoolean overrideAnnotationFound = new AtomicBoolean(); - MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> { Assert.state(mergedAnnotation.isMetaPresent(), "@BeanOverride annotation must be meta-present"); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java index afc3836826e..1262c11c4b4 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideProcessor.java @@ -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. * - *

Processors are generally linked to one or more specific concrete - * annotations (meta-annotated with {@link BeanOverride @BeanOverride}) and - * concrete {@link OverrideMetadata} implementations. + *

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. * *

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); } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java index eb98ffe27a3..6c4e2cf8acc 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java @@ -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,49 +41,49 @@ import org.springframework.util.StringUtils; */ class BeanOverrideRegistrar implements BeanFactoryAware { - static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideRegistrar.class.getName(); - private final Map beanNameRegistry; + private final Map earlyOverrideMetadata; + private final Set 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> classesToParse) { - Set 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 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 { } /** - * 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. - *

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> 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>()); - 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> existing = (Set>) 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); } } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideStrategy.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideStrategy.java index e1df52a9935..315be3174c0 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideStrategy.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideStrategy.java @@ -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. - *

Requires that the original bean definition exists. + *

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. - *

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. + *

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 diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java index 44b28a08e3c..18c53f2338d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideTestExecutionListener.java @@ -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. - * - *

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. *

This method does nothing if the * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE} * attribute is not present in the {@code TestContext}. diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java index a02f879ec68..b8b156ba282 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessor.java @@ -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"); diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanMetadata.java index 8465889ba19..b1f99958002 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanMetadata.java @@ -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); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java index 83a2a6c3cb3..12dc919b51e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessor.java @@ -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 { diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoMetadata.java index 26311b6433f..e92d1cd219f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoMetadata.java @@ -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; } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanMetadata.java index 78454cd8090..1a8b27e7f5c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanMetadata.java @@ -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"); } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java index 8754cc95d5b..79713a42de2 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java @@ -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 { */ static final SomeInterface OVERRIDE = new SomeImplementation(); + static final ExampleService OVERRIDE_SERVICE = new FailingExampleService(); static class ReplaceBeans {