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 f2f7655f1f..2952797003 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 @@ -47,6 +47,9 @@ Java:: <2> The result of this static method will be used as the instance and injected into the field ====== +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 +method to use in the root test class. [[spring-testing-annotation-beanoverriding-mockitobean]] == `@MockitoBean` and `@MockitoSpyBean` @@ -100,32 +103,31 @@ and associated infrastructure, which allows to define custom bean overriding var In order to provide an extension, three classes are needed: - - A concrete `BeanOverrideProcessor

`. - - A concrete `OverrideMetadata` created by said processor. + - A concrete `BeanOverrideProcessor` implementation, `P`. + - One or more concrete `OverrideMetadata` implementations created by said processor. - An annotation meta-annotated with `@BeanOverride(P.class)`. The Spring TestContext Framework includes infrastructure classes that support bean -overriding: a `BeanPostProcessor`, a `TestExecutionListener` and a `ContextCustomizerFactory`. -These are automatically registered via the Spring TestContext Framework `spring.factories` -file. +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`. -Then the `BeanOverrideBeanPostProcessor` will use that information to alter the Context, -registering and replacing bean definitions as influenced by each metadata +Then the `BeanOverrideBeanFactoryPostProcessor` will use that information to alter the +Context, registering and replacing bean definitions as influenced 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 via -`SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String)` and -provided to the processor during `OverrideMetadata` creation. + - `WRAP_EARLY_BEAN`: an original instance is obtained and passed to the `OverrideMetadata` +when the override instance is created. -NOTE: The Bean Overriding infrastructure works best with singleton beans. It also doesn't -include any bean resolution (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 +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 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 d93499a44d..2bfd17a7fe 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 @@ -34,7 +34,7 @@ import java.lang.annotation.Target; * * @author Simon Baslé * @since 6.2 - * @see BeanOverrideBeanPostProcessor + * @see BeanOverrideBeanFactoryPostProcessor */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) 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 new file mode 100644 index 0000000000..60f914df6f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java @@ -0,0 +1,275 @@ +/* + * 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.lang.reflect.Field; +import java.util.Arrays; +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.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. + * + *

A set of classes from which to parse {@link OverrideMetadata} must be + * provided to this processor. Each test class is expected to use any + * annotation meta-annotated with {@link BeanOverride @BeanOverride} to mark + * beans to override. The {@link BeanOverrideParsingUtils#hasBeanOverride(Class)} + * method can be used to check if a class matches the above criteria. + * + *

The provided classes are fully parsed at creation to build a metadata set. + * This processor implements several {@link BeanOverrideStrategy overriding + * strategy} and chooses the correct one according to each override metadata + * {@link OverrideMetadata#getStrategy()} method. Additionally, it provides + * support for injecting the overridden bean instances into their corresponding + * annotated {@link Field fields}. + * + * @author Simon Baslé + * @since 6.2 + */ +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; + + + /** + * Create a new {@code BeanOverrideBeanFactoryPostProcessor} instance with + * the given {@link BeanOverrideRegistrar}, which contains a set of parsed + * {@link OverrideMetadata}. + * @param overrideRegistrar the {@link BeanOverrideRegistrar} used to track + * metadata + */ + public BeanOverrideBeanFactoryPostProcessor(BeanOverrideRegistrar overrideRegistrar) { + this.overrideRegistrar = overrideRegistrar; + } + + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10; + } + + + @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); + } + + private void postProcessWithRegistry(DefaultListableBeanFactory registry) { + for (OverrideMetadata metadata : this.overrideRegistrar.getOverrideMetadata()) { + registerBeanOverride(registry, metadata); + } + } + + /** + * Copy certain details of a {@link BeanDefinition} to the definition created by + * this processor for a given {@link OverrideMetadata}. + *

The default implementation copies the {@linkplain BeanDefinition#isPrimary() + * primary flag} and the {@linkplain BeanDefinition#getScope() scope}. + */ + protected void copyBeanDefinitionDetails(BeanDefinition from, RootBeanDefinition to) { + to.setPrimary(from.isPrimary()); + to.setScope(from.getScope()); + } + + private void registerBeanOverride(DefaultListableBeanFactory beanFactory, OverrideMetadata overrideMetadata) { + switch (overrideMetadata.getStrategy()) { + case REPLACE_DEFINITION -> registerReplaceDefinition(beanFactory, overrideMetadata, true); + case REPLACE_OR_CREATE_DEFINITION -> registerReplaceDefinition(beanFactory, overrideMetadata, false); + case WRAP_BEAN -> registerWrapBean(beanFactory, overrideMetadata); + } + } + + private void registerReplaceDefinition(DefaultListableBeanFactory beanFactory, OverrideMetadata overrideMetadata, boolean enforceExistingDefinition) { + + RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata); + String beanName = overrideMetadata.getBeanName(); + + BeanDefinition existingBeanDefinition = null; + if (beanFactory.containsBeanDefinition(beanName)) { + existingBeanDefinition = beanFactory.getBeanDefinition(beanName); + copyBeanDefinitionDetails(existingBeanDefinition, beanDefinition); + beanFactory.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); + + Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null); + if (beanFactory.isSingleton(beanName)) { + // Now we have an instance (the override) that we can register. + // At this stage we don't expect a singleton instance to be present, + // and this call will throw if there is such an instance already. + beanFactory.registerSingleton(beanName, override); + } + + overrideMetadata.track(override, beanFactory); + this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanName); + } + + /** + * Check that the expected bean name is registered and matches the type to override. + *

If so, put the override metadata in the early tracking map. + *

The map will later be checked to see if a given bean should be wrapped + * upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)} + * phase. + */ + private void registerWrapBean(DefaultListableBeanFactory beanFactory, OverrideMetadata metadata) { + Set existingBeanNames = getExistingBeanNames(beanFactory, metadata.getBeanType()); + String beanName = metadata.getBeanName(); + if (!existingBeanNames.contains(beanName)) { + throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping," + + " no existing bean instance by this name of type " + metadata.getBeanType()); + } + this.overrideRegistrar.markWrapEarly(metadata, beanName); + this.overrideRegistrar.registerNameForMetadata(metadata, beanName); + } + + private RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) { + RootBeanDefinition definition = new RootBeanDefinition(); + definition.setTargetType(metadata.getBeanType()); + return definition; + } + + private Set getExistingBeanNames(DefaultListableBeanFactory beanFactory, ResolvableType resolvableType) { + Set beans = new LinkedHashSet<>( + Arrays.asList(beanFactory.getBeanNamesForType(resolvableType, true, false))); + Class type = resolvableType.resolve(Object.class); + for (String beanName : beanFactory.getBeanNamesForType(FactoryBean.class, true, false)) { + beanName = BeanFactoryUtils.transformedBeanName(beanName); + BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); + Object attribute = beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE); + if (resolvableType.equals(attribute) || type.equals(attribute)) { + beans.add(beanName); + } + } + beans.removeIf(ScopedProxyUtils::isScopedTarget); + 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 { + + private final BeanOverrideRegistrar overrideRegistrar; + + private final Map earlyReferences; + + + private WrapEarlyBeanPostProcessor(BeanOverrideRegistrar registrar) { + this.overrideRegistrar = registrar; + this.earlyReferences = new ConcurrentHashMap<>(16); + } + + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + + @Override + public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { + if (bean instanceof FactoryBean) { + return bean; + } + this.earlyReferences.put(getCacheKey(bean, beanName), bean); + return this.overrideRegistrar.wrapIfNecessary(bean, beanName); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof FactoryBean) { + return bean; + } + if (this.earlyReferences.remove(getCacheKey(bean, beanName)) != bean) { + return this.overrideRegistrar.wrapIfNecessary(bean, beanName); + } + return bean; + } + + private String getCacheKey(Object bean, String beanName) { + return (StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName()); + } + + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java deleted file mode 100644 index fdb004afc3..0000000000 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java +++ /dev/null @@ -1,382 +0,0 @@ -/* - * 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.lang.reflect.Field; -import java.util.Arrays; -import java.util.HashMap; -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.PropertyValues; -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -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.InstantiationAwareBeanPostProcessor; -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.RootBeanDefinition; -import org.springframework.context.ApplicationContext; -import org.springframework.core.Ordered; -import org.springframework.core.PriorityOrdered; -import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; - -/** - * A {@link BeanFactoryPostProcessor} used to register and inject overriding - * bean metadata with the {@link ApplicationContext}. - * - *

A set of {@link OverrideMetadata} must be provided to this processor. A - * {@link BeanOverrideParser} can typically be used to parse this metadata from - * test classes that use any annotation meta-annotated with - * {@link BeanOverride @BeanOverride} to mark override sites. - * - *

This processor supports two types of {@link BeanOverrideStrategy}: - *

- * - *

This processor also provides support for injecting the overridden bean - * instances into their corresponding annotated {@link Field fields}. - * - * @author Simon Baslé - * @since 6.2 - */ -public class BeanOverrideBeanPostProcessor implements InstantiationAwareBeanPostProcessor, - BeanFactoryAware, BeanFactoryPostProcessor, Ordered { - - private static final String INFRASTRUCTURE_BEAN_NAME = BeanOverrideBeanPostProcessor.class.getName(); - - private static final String EARLY_INFRASTRUCTURE_BEAN_NAME = - BeanOverrideBeanPostProcessor.WrapEarlyBeanPostProcessor.class.getName(); - - - private final Map earlyOverrideMetadata = new HashMap<>(); - - private final Map beanNameRegistry = new HashMap<>(); - - private final Map fieldRegistry = new HashMap<>(); - - private final Set overrideMetadata; - - @Nullable - private ConfigurableListableBeanFactory beanFactory; - - - /** - * Create a new {@code BeanOverrideBeanPostProcessor} instance with the - * given {@link OverrideMetadata} set. - * @param overrideMetadata the initial override metadata - */ - public BeanOverrideBeanPostProcessor(Set overrideMetadata) { - this.overrideMetadata = overrideMetadata; - } - - - @Override - public int getOrder() { - return Ordered.LOWEST_PRECEDENCE - 10; - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory, - "Bean overriding can only be used with a ConfigurableListableBeanFactory"); - this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; - } - - /** - * Return this processor's {@link OverrideMetadata} set. - */ - protected Set getOverrideMetadata() { - return this.overrideMetadata; - } - - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - Assert.state(this.beanFactory == beanFactory, "Unexpected BeanFactory to post-process"); - Assert.isInstanceOf(BeanDefinitionRegistry.class, beanFactory, - "Bean overriding annotations can only be used on bean factories that implement " - + "BeanDefinitionRegistry"); - postProcessWithRegistry((BeanDefinitionRegistry) beanFactory); - } - - private void postProcessWithRegistry(BeanDefinitionRegistry registry) { - // Note that a tracker bean is registered down the line only if there is some overrideMetadata parsed. - for (OverrideMetadata metadata : getOverrideMetadata()) { - registerBeanOverride(registry, metadata); - } - } - - /** - * Copy certain details of a {@link BeanDefinition} to the definition created by - * this processor for a given {@link OverrideMetadata}. - *

The default implementation copies the {@linkplain BeanDefinition#isPrimary() - * primary flag} and the {@linkplain BeanDefinition#getScope() scope}. - */ - protected void copyBeanDefinitionDetails(BeanDefinition from, RootBeanDefinition to) { - to.setPrimary(from.isPrimary()); - to.setScope(from.getScope()); - } - - private void registerBeanOverride(BeanDefinitionRegistry registry, OverrideMetadata overrideMetadata) { - switch (overrideMetadata.getBeanOverrideStrategy()) { - case REPLACE_DEFINITION -> registerReplaceDefinition(registry, overrideMetadata, true); - case REPLACE_OR_CREATE_DEFINITION -> registerReplaceDefinition(registry, overrideMetadata, false); - case WRAP_EARLY_BEAN -> registerWrapEarly(overrideMetadata); - } - } - - private void registerReplaceDefinition(BeanDefinitionRegistry registry, OverrideMetadata overrideMetadata, - boolean enforceExistingDefinition) { - - RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata); - String beanName = overrideMetadata.getExpectedBeanName(); - - BeanDefinition existingBeanDefinition = null; - if (registry.containsBeanDefinition(beanName)) { - existingBeanDefinition = registry.getBeanDefinition(beanName); - copyBeanDefinitionDetails(existingBeanDefinition, beanDefinition); - registry.removeBeanDefinition(beanName); - } - else if (enforceExistingDefinition) { - throw new IllegalStateException("Unable to override " + overrideMetadata.getBeanOverrideDescription() + - " bean; expected a bean definition to replace with name '" + beanName + "'"); - } - registry.registerBeanDefinition(beanName, beanDefinition); - - Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null); - Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null"); - if (this.beanFactory.isSingleton(beanName)) { - // Now we have an instance (the override) that we can register. - // At this stage we don't expect a singleton instance to be present, - // and this call will throw if there is such an instance already. - this.beanFactory.registerSingleton(beanName, override); - } - - overrideMetadata.track(override, this.beanFactory); - this.beanNameRegistry.put(overrideMetadata, beanName); - this.fieldRegistry.put(overrideMetadata.field(), beanName); - } - - /** - * Check that the expected bean name is registered and matches the type to override. - *

If so, put the override metadata in the early tracking map. - *

The map will later be checked to see if a given bean should be wrapped - * upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)} - * phase. - */ - private void registerWrapEarly(OverrideMetadata metadata) { - Set existingBeanNames = getExistingBeanNames(metadata.typeToOverride()); - String beanName = metadata.getExpectedBeanName(); - if (!existingBeanNames.contains(beanName)) { - throw new IllegalStateException("Unable to override wrap-early bean named '" + beanName + "', not found among " + - existingBeanNames); - } - this.earlyOverrideMetadata.put(beanName, metadata); - this.beanNameRegistry.put(metadata, beanName); - this.fieldRegistry.put(metadata.field(), beanName); - } - - /** - * Check early override records and use the {@link OverrideMetadata} to - * create an override instance from the provided bean, if relevant. - *

Called during the {@link SmartInstantiationAwareBeanPostProcessor} - * phases. - * @see WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String) - * @see WrapEarlyBeanPostProcessor#postProcessAfterInitialization(Object, String) - */ - protected final Object wrapIfNecessary(Object bean, String beanName) throws BeansException { - final OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName); - if (metadata != null && metadata.getBeanOverrideStrategy() == BeanOverrideStrategy.WRAP_EARLY_BEAN) { - bean = metadata.createOverride(beanName, null, bean); - Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null"); - metadata.track(bean, this.beanFactory); - } - return bean; - } - - private RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) { - RootBeanDefinition definition = new RootBeanDefinition(metadata.typeToOverride().resolve()); - definition.setTargetType(metadata.typeToOverride()); - return definition; - } - - private Set getExistingBeanNames(ResolvableType resolvableType) { - Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null"); - Set beans = new LinkedHashSet<>( - Arrays.asList(this.beanFactory.getBeanNamesForType(resolvableType, true, false))); - Class type = resolvableType.resolve(Object.class); - for (String beanName : this.beanFactory.getBeanNamesForType(FactoryBean.class, true, false)) { - beanName = BeanFactoryUtils.transformedBeanName(beanName); - BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition(beanName); - Object attribute = beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE); - if (resolvableType.equals(attribute) || type.equals(attribute)) { - beans.add(beanName); - } - } - beans.removeIf(ScopedProxyUtils::isScopedTarget); - return beans; - } - - @Override - public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) - throws BeansException { - ReflectionUtils.doWithFields(bean.getClass(), field -> postProcessField(bean, field)); - return pvs; - } - - private void postProcessField(Object bean, Field field) { - String beanName = this.fieldRegistry.get(field); - if (StringUtils.hasText(beanName)) { - inject(field, bean, beanName); - } - } - - void inject(Field field, Object target, OverrideMetadata overrideMetadata) { - String beanName = this.beanNameRegistry.get(overrideMetadata); - Assert.state(StringUtils.hasLength(beanName), - () -> "No bean found for OverrideMetadata: " + overrideMetadata); - inject(field, target, beanName); - } - - private void inject(Field field, Object target, String beanName) { - try { - ReflectionUtils.makeAccessible(field); - Object existingValue = ReflectionUtils.getField(field, target); - Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null"); - Object bean = this.beanFactory.getBean(beanName, field.getType()); - if (existingValue == bean) { - return; - } - Assert.state(existingValue == null, () -> "The existing value '" + existingValue + - "' of field '" + field + "' is not the same as the new value '" + bean + "'"); - ReflectionUtils.setField(field, target, bean); - } - catch (Throwable ex) { - throw new BeanCreationException("Could not inject field '" + field + "'", ex); - } - } - - /** - * Register a {@link BeanOverrideBeanPostProcessor} 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 - * @param overrideMetadata the initial override metadata set - */ - public static void register(BeanDefinitionRegistry registry, @Nullable Set overrideMetadata) { - // Early processor - getOrAddInfrastructureBeanDefinition( - registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, constructorArgs -> - constructorArgs.addIndexedArgumentValue(0, new RuntimeBeanReference(INFRASTRUCTURE_BEAN_NAME))); - - // Main processor - BeanDefinition definition = getOrAddInfrastructureBeanDefinition( - registry, BeanOverrideBeanPostProcessor.class, INFRASTRUCTURE_BEAN_NAME, constructorArgs -> - constructorArgs.addIndexedArgumentValue(0, new LinkedHashSet())); - ConstructorArgumentValues.ValueHolder constructorArg = - definition.getConstructorArgumentValues().getIndexedArgumentValue(0, Set.class); - @SuppressWarnings({"unchecked", "NullAway"}) - Set existing = (Set) constructorArg.getValue(); - if (overrideMetadata != null && existing != null) { - existing.addAll(overrideMetadata); - } - } - - private static BeanDefinition getOrAddInfrastructureBeanDefinition(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); - return definition; - } - return registry.getBeanDefinition(beanName); - } - - - private static final class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, - PriorityOrdered { - - private final BeanOverrideBeanPostProcessor mainProcessor; - - private final Map earlyReferences; - - - private WrapEarlyBeanPostProcessor(BeanOverrideBeanPostProcessor mainProcessor) { - this.mainProcessor = mainProcessor; - this.earlyReferences = new ConcurrentHashMap<>(16); - } - - - @Override - public int getOrder() { - return Ordered.HIGHEST_PRECEDENCE; - } - - @Override - public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { - if (bean instanceof FactoryBean) { - return bean; - } - this.earlyReferences.put(getCacheKey(bean, beanName), bean); - return this.mainProcessor.wrapIfNecessary(bean, beanName); - } - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof FactoryBean) { - return bean; - } - if (this.earlyReferences.remove(getCacheKey(bean, beanName)) != bean) { - return this.mainProcessor.wrapIfNecessary(bean, beanName); - } - return bean; - } - - private String getCacheKey(Object bean, String beanName) { - return (StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName()); - } - - } - -} 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 d2371de48d..6cddebf650 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 @@ -16,6 +16,7 @@ package org.springframework.test.context.bean.override; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -29,56 +30,51 @@ import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContextAnnotationUtils; /** - * {@link ContextCustomizerFactory} which provides support for Bean Overriding - * in tests. + * {@link ContextCustomizerFactory} implementation that provides support for + * Bean Overriding. * * @author Simon Baslé * @since 6.2 + * @see BeanOverride */ -public class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory { +class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory { @Override @Nullable public ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { - BeanOverrideParser parser = new BeanOverrideParser(); - parseMetadata(testClass, parser); - if (parser.getOverrideMetadata().isEmpty()) { + Set> detectedClasses = new LinkedHashSet<>(); + findClassesWithBeanOverride(testClass, detectedClasses); + if (detectedClasses.isEmpty()) { return null; } - return new BeanOverrideContextCustomizer(parser.getOverrideMetadata()); + return new BeanOverrideContextCustomizer(detectedClasses); } - private void parseMetadata(Class testClass, BeanOverrideParser parser) { - parser.parse(testClass); + private void findClassesWithBeanOverride(Class testClass, Set> detectedClasses) { + if (BeanOverrideParsingUtils.hasBeanOverride(testClass)) { + detectedClasses.add(testClass); + } if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) { - parseMetadata(testClass.getEnclosingClass(), parser); + findClassesWithBeanOverride(testClass.getEnclosingClass(), detectedClasses); } } - /** - * {@link ContextCustomizer} for Bean Overriding in tests. - */ private static final class BeanOverrideContextCustomizer implements ContextCustomizer { - private final Set metadata; + private final Set> detectedClasses; - /** - * Construct a context customizer given some pre-existing override - * metadata. - * @param metadata a set of concrete {@link OverrideMetadata} provided - * by the underlying {@link BeanOverrideParser} - */ - BeanOverrideContextCustomizer(Set metadata) { - this.metadata = metadata; + BeanOverrideContextCustomizer(Set> detectedClasses) { + this.detectedClasses = detectedClasses; } @Override public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { if (context instanceof BeanDefinitionRegistry registry) { - BeanOverrideBeanPostProcessor.register(registry, this.metadata); + BeanOverrideRegistrar.register(registry, this.detectedClasses); + BeanOverrideBeanFactoryPostProcessor.register(registry); } } @@ -91,12 +87,12 @@ public class BeanOverrideContextCustomizerFactory implements ContextCustomizerFa return false; } BeanOverrideContextCustomizer other = (BeanOverrideContextCustomizer) obj; - return this.metadata.equals(other.metadata); + return this.detectedClasses.equals(other.detectedClasses); } @Override public int hashCode() { - return this.metadata.hashCode(); + return this.detectedClasses.hashCode(); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java similarity index 51% rename from spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java rename to spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java index e8c4f15177..c6ee7e7166 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParser.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideParsingUtils.java @@ -18,13 +18,12 @@ package org.springframework.test.context.bean.override; import java.lang.annotation.Annotation; import java.lang.reflect.Field; -import java.util.Collections; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import org.springframework.beans.BeanUtils; -import org.springframework.core.ResolvableType; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.util.Assert; @@ -33,63 +32,58 @@ import org.springframework.util.ReflectionUtils; import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.DIRECT; /** - * A parser that discovers annotations meta-annotated with {@link BeanOverride @BeanOverride} - * on fields of a given class and creates {@link OverrideMetadata} accordingly. + * Internal parsing utilities to discover the presence of + * {@link BeanOverride @BeanOverride} on fields, and create the relevant + * {@link OverrideMetadata} accordingly. * * @author Simon Baslé * @author Sam Brannen * @since 6.2 */ -class BeanOverrideParser { - - private final Set parsedMetadata = new LinkedHashSet<>(); - +abstract class BeanOverrideParsingUtils { /** - * Get the set of {@link OverrideMetadata} once {@link #parse(Class)} has been called. - */ - Set getOverrideMetadata() { - return Collections.unmodifiableSet(this.parsedMetadata); - } - - /** - * Discover fields of the provided class that are meta-annotated with - * {@link BeanOverride @BeanOverride}, then instantiate the corresponding - * {@link BeanOverrideProcessor} and use it to create {@link OverrideMetadata} - * for each field. - *

Each call to {@code parse} adds the parsed metadata to the parser's - * override metadata {@link #getOverrideMetadata() set}. - * @param testClass the test class in which to inspect fields - */ - void parse(Class testClass) { - ReflectionUtils.doWithFields(testClass, field -> parseField(field, testClass)); - } - - /** - * Check if any field of the provided {@code testClass} is meta-annotated + * Check if at least one field of the given {@code clazz} is meta-annotated * with {@link BeanOverride @BeanOverride}. - *

This is similar to the initial discovery of fields in {@link #parse(Class)} - * without the heavier steps of instantiating processors and creating - * {@link OverrideMetadata}. Consequently, this method leaves the current - * state of the parser's override metadata {@link #getOverrideMetadata() set} - * unchanged. - * @param testClass the class which fields to inspect - * @return true if there is a bean override annotation present, false otherwise - * @see #parse(Class) + * @param clazz the class which fields to inspect + * @return {@code true} if there is a bean override annotation present, + * {@code false} otherwise */ - boolean hasBeanOverride(Class testClass) { + static boolean hasBeanOverride(Class clazz) { AtomicBoolean hasBeanOverride = new AtomicBoolean(); - ReflectionUtils.doWithFields(testClass, field -> { + ReflectionUtils.doWithFields(clazz, field -> { if (hasBeanOverride.get()) { return; } boolean present = MergedAnnotations.from(field, DIRECT).isPresent(BeanOverride.class); hasBeanOverride.compareAndSet(false, present); }); - return hasBeanOverride.get(); + if (hasBeanOverride.get()) { + return true; + } + return false; } - private void parseField(Field field, Class source) { + /** + * Parse the specified classes for the presence of fields annotated with + * {@link BeanOverride @BeanOverride}, and create an {@link OverrideMetadata} + * for each. + * @param classes the classes to parse + */ + static Set parse(Iterable> classes) { + Set result = new LinkedHashSet<>(); + classes.forEach(c -> ReflectionUtils.doWithFields(c, field -> parseField(field, c, result))); + return result; + } + + /** + * Convenience method to {@link #parse(Iterable) parse} a single test class. + */ + static Set parse(Class clazz) { + return parse(List.of(clazz)); + } + + private static void parseField(Field field, Class testClass, Set metadataSet) { AtomicBoolean overrideAnnotationFound = new AtomicBoolean(); MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> { @@ -100,14 +94,11 @@ class BeanOverrideParser { MergedAnnotation metaSource = mergedAnnotation.getMetaSource(); Assert.state(metaSource != null, "Meta-annotation source must not be null"); Annotation composedAnnotation = metaSource.synthesize(); - ResolvableType typeToOverride = processor.getOrDeduceType(field, composedAnnotation, source); Assert.state(overrideAnnotationFound.compareAndSet(false, true), () -> "Multiple @BeanOverride annotations found on field: " + field); - OverrideMetadata metadata = processor.createMetadata(field, composedAnnotation, typeToOverride); - boolean isNewDefinition = this.parsedMetadata.add(metadata); - Assert.state(isNewDefinition, () -> "Duplicate " + metadata.getBeanOverrideDescription() + - " OverrideMetadata: " + metadata); + OverrideMetadata metadata = processor.createMetadata(composedAnnotation, testClass, field); + metadataSet.add(metadata); }); } 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 8546d405b2..afc3836826 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 @@ -18,18 +18,14 @@ package org.springframework.test.context.bean.override; import java.lang.annotation.Annotation; import java.lang.reflect.Field; -import java.lang.reflect.TypeVariable; - -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.core.ResolvableType; /** - * Strategy interface for Bean Override processing. + * Strategy interface for Bean Override processing and creation of + * {@link OverrideMetadata}. * - *

Processors are generally linked to one or more specific concrete annotations - * (meta-annotated with {@link BeanOverride @BeanOverride}) and specify different - * steps in the process of parsing these annotations, ultimately creating - * {@link OverrideMetadata} which will be used to instantiate the overrides. + *

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

Implementations are required to have a no-argument constructor and be * stateless. @@ -41,31 +37,13 @@ import org.springframework.core.ResolvableType; public interface BeanOverrideProcessor { /** - * Determine the {@link ResolvableType} for which an {@link OverrideMetadata} - * instance will be created — for example, by using the supplied annotation - * to determine the type. - *

The default implementation deduces the field's corresponding - * {@link ResolvableType}, additionally tracking the source class if the - * field's type is a {@link TypeVariable}. - */ - default ResolvableType getOrDeduceType(Field field, Annotation annotation, Class source) { - return (field.getGenericType() instanceof TypeVariable ? - ResolvableType.forField(field, source) : ResolvableType.forField(field)); - } - - /** - * Create an {@link OverrideMetadata} instance for the given annotated field - * and target {@link #getOrDeduceType(Field, Annotation, Class) type}. - *

Specific implementations of metadata can have state to be used during - * override {@linkplain OverrideMetadata#createOverride(String, BeanDefinition, - * Object) instance creation} — for example, from further parsing of the - * annotation or the annotated field. - * @param field the annotated field + * Create an {@link OverrideMetadata} instance for the given annotated field. * @param overrideAnnotation the field annotation - * @param typeToOverride the target type + * @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 field the annotated field * @return a new {@link OverrideMetadata} instance - * @see #getOrDeduceType(Field, Annotation, Class) */ - OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride); - + 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 new file mode 100644 index 0000000000..eb98ffe27a --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java @@ -0,0 +1,173 @@ +/* + * 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.lang.reflect.Field; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +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; +import org.springframework.util.StringUtils; + +/** + * An internal class used to track {@link OverrideMetadata}-related state after + * the bean factory has been processed and to provide field injection utilities + * for test execution listeners. + * + * @author Simon Baslé + * @since 6.2 + */ +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. + * @param classesToParse the initial set of classes that have been + * detected to contain bean overriding annotations, to be parsed immediately. + */ + 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<>(); + } + + /** + * Return this processor's {@link OverrideMetadata} set. + */ + 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); + if (metadata != null && metadata.getStrategy() == BeanOverrideStrategy.WRAP_BEAN) { + bean = metadata.createOverride(beanName, null, bean); + Assert.state(this.beanFactory != null, "ConfigurableBeanFactory must not be null"); + metadata.track(bean, this.beanFactory); + } + return bean; + } + + /** + * Register the provided {@link OverrideMetadata} and associated it with a + * {@code beanName}. + */ + void registerNameForMetadata(OverrideMetadata metadata, String beanName) { + this.beanNameRegistry.put(metadata, beanName); + } + + /** + * 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) { + 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), + () -> "No bean found for OverrideMetadata: " + overrideMetadata); + inject(overrideMetadata.getField(), target, beanName); + } + + private void inject(Field field, Object target, String beanName) { + try { + ReflectionUtils.makeAccessible(field); + Object existingValue = ReflectionUtils.getField(field, target); + Assert.state(this.beanFactory != null, "beanFactory must not be null"); + Object bean = this.beanFactory.getBean(beanName, field.getType()); + if (existingValue == bean) { + return; + } + Assert.state(existingValue == null, () -> "The existing value '" + existingValue + + "' of field '" + field + "' is not the same as the new value '" + bean + "'"); + ReflectionUtils.setField(field, target, bean); + } + catch (Throwable ex) { + 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 62e398a576..e1df52a993 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,8 +17,7 @@ package org.springframework.test.context.bean.override; /** - * Strategies for bean override instantiation, implemented in - * {@link BeanOverrideBeanPostProcessor}. + * Strategies for bean override instantiation. * * @author Simon Baslé * @since 6.2 @@ -39,10 +38,10 @@ public enum BeanOverrideStrategy { REPLACE_OR_CREATE_DEFINITION, /** - * Intercept and wrap the actual bean instance upon creation, during the {@linkplain - * org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference(Object, String) - * early bean reference} phase. + * 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"). */ - WRAP_EARLY_BEAN + 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 41f5068fef..44b28a08e3 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 @@ -56,17 +56,17 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList } /** - * Using a registered {@link BeanOverrideBeanPostProcessor}, find metadata + * Using a registered {@link BeanOverrideBeanFactoryPostProcessor}, find metadata * associated with the current test class and ensure fields are injected * with the overridden bean instance. */ protected void injectFields(TestContext testContext) { - postProcessFields(testContext, (testMetadata, postProcessor) -> postProcessor.inject( - testMetadata.overrideMetadata.field(), testMetadata.testInstance, testMetadata.overrideMetadata)); + postProcessFields(testContext, (testMetadata, overrideRegistrar) -> overrideRegistrar.inject( + testMetadata.testInstance, testMetadata.overrideMetadata)); } /** - * Using a registered {@link BeanOverrideBeanPostProcessor}, find metadata + * 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. *

This method does nothing if the @@ -79,31 +79,28 @@ public class BeanOverrideTestExecutionListener extends AbstractTestExecutionList postProcessFields(testContext, (testMetadata, postProcessor) -> { Object testInstance = testMetadata.testInstance; - Field field = testMetadata.overrideMetadata.field(); + Field field = testMetadata.overrideMetadata.getField(); ReflectionUtils.makeAccessible(field); ReflectionUtils.setField(field, testInstance, null); - postProcessor.inject(field, testInstance, testMetadata.overrideMetadata); + postProcessor.inject(testInstance, testMetadata.overrideMetadata); }); } } private void postProcessFields(TestContext testContext, BiConsumer consumer) { + BeanOverrideRegistrar> consumer) { Class testClass = testContext.getTestClass(); Object testInstance = testContext.getTestInstance(); - BeanOverrideParser parser = new BeanOverrideParser(); - // Avoid full parsing, but validate that this particular class has some bean override field(s). - if (parser.hasBeanOverride(testClass)) { - BeanOverrideBeanPostProcessor postProcessor = - testContext.getApplicationContext().getBean(BeanOverrideBeanPostProcessor.class); - // The class should have already been parsed by the context customizer. - for (OverrideMetadata metadata : postProcessor.getOverrideMetadata()) { - if (!metadata.field().getDeclaringClass().equals(testClass)) { + if (BeanOverrideParsingUtils.hasBeanOverride(testClass)) { + BeanOverrideRegistrar registrar = + testContext.getApplicationContext().getBean(BeanOverrideRegistrar.class); + for (OverrideMetadata metadata : registrar.getOverrideMetadata()) { + if (!metadata.getField().getDeclaringClass().isAssignableFrom(testClass)) { continue; } - consumer.accept(new TestContextOverrideMetadata(testInstance, metadata), postProcessor); + consumer.accept(new TestContextOverrideMetadata(testInstance, metadata), registrar); } } } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java index 94a5b664ec..cc9bdbcf85 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/OverrideMetadata.java @@ -16,7 +16,6 @@ package org.springframework.test.context.bean.override; -import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.Objects; @@ -27,7 +26,16 @@ import org.springframework.core.style.ToStringCreator; import org.springframework.lang.Nullable; /** - * Metadata for Bean Overrides. + * Metadata for Bean Override injection points, also responsible for the + * creation of the overriding instance. + * + *

WARNING: implementations are used as a cache key and + * must implement proper {@code equals} and {@code hashCode}t methods. + * + *

Specific implementations of metadata can have state to be used during + * override {@linkplain #createOverride(String, BeanDefinition, Object) + * instance creation} — for example, from further parsing of the + * annotation or the annotated field. * * @author Simon Baslé * @since 6.2 @@ -36,66 +44,45 @@ public abstract class OverrideMetadata { private final Field field; - private final Annotation overrideAnnotation; - - private final ResolvableType typeToOverride; + private final ResolvableType beanType; private final BeanOverrideStrategy strategy; - protected OverrideMetadata(Field field, Annotation overrideAnnotation, - ResolvableType typeToOverride, BeanOverrideStrategy strategy) { + protected OverrideMetadata(Field field, ResolvableType beanType, + BeanOverrideStrategy strategy) { this.field = field; - this.overrideAnnotation = overrideAnnotation; - this.typeToOverride = typeToOverride; + this.beanType = beanType; this.strategy = strategy; } - /** - * Return a short, human-readable description of the kind of override this - * instance handles. + * Return the bean name to override. */ - public abstract String getBeanOverrideDescription(); - - /** - * Return the expected bean name to override. - *

Typically, this is either explicitly set in a concrete annotation or - * inferred from the annotated field's name. - * @return the expected bean name - */ - protected String getExpectedBeanName() { + protected String getBeanName() { return this.field.getName(); } - /** - * Return the annotated {@link Field}. - */ - public Field field() { - return this.field; - } - - /** - * Return the concrete override annotation, that is the one meta-annotated - * with {@link BeanOverride @BeanOverride}. - */ - public Annotation overrideAnnotation() { - return this.overrideAnnotation; - } - /** * Return the bean {@link ResolvableType type} to override. */ - public ResolvableType typeToOverride() { - return this.typeToOverride; + public ResolvableType getBeanType() { + return this.beanType; + } + + /** + * Return the annotated {@link Field}. + */ + public Field getField() { + return this.field; } /** * Return the {@link BeanOverrideStrategy} for this instance, as a hint on * how and when the override instance should be created. */ - public final BeanOverrideStrategy getBeanOverrideStrategy() { + public BeanOverrideStrategy getStrategy() { return this.strategy; } @@ -133,25 +120,22 @@ public abstract class OverrideMetadata { return false; } OverrideMetadata that = (OverrideMetadata) obj; - return Objects.equals(this.field, that.field) && - Objects.equals(this.overrideAnnotation, that.overrideAnnotation) && - Objects.equals(this.strategy, that.strategy) && - Objects.equals(typeToOverride(), that.typeToOverride()); + return Objects.equals(this.strategy, that.strategy) && + Objects.equals(this.field, that.field) && + Objects.equals(this.beanType, that.beanType); } @Override public int hashCode() { - return Objects.hash(this.field, this.overrideAnnotation, this.strategy, typeToOverride()); + return Objects.hash(this.strategy, this.field, this.beanType); } @Override public String toString() { return new ToStringCreator(this) - .append("category", getBeanOverrideDescription()) - .append("field", this.field) - .append("overrideAnnotation", this.overrideAnnotation) .append("strategy", this.strategy) - .append("typeToOverride", typeToOverride()) + .append("field", this.field) + .append("beanType", this.beanType) .toString(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java index 4a712c60e9..e3fba8d0ad 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java @@ -28,9 +28,11 @@ import org.springframework.test.context.bean.override.BeanOverride; /** * Mark a field to override a bean instance in the {@code BeanFactory}. * - *

The instance is created from a zero-argument static factory method in the test - * class whose return type is compatible with the annotated field. The method - * is deduced as follows. + *

The instance is created from a zero-argument static factory method in the + * test class whose return type is compatible with the annotated field. In the + * case of a nested test, any enclosing class it might have is also considered. + * Similarly, in case the test class inherits from a base class the whole class + * hierarchy is considered. The method is deduced as follows. *