From 2d33aac35038ef9f6773ff60f542975a051e7e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 20 Mar 2024 11:48:22 +0100 Subject: [PATCH] Improve Bean Overriding support, testing and documentation This commit improves on the bean overriding feature in several ways: the API is simplified and polished (metadata and processor contracts, etc...). The commit also reworks infrastructure classes (context customizer, test execution listener, BeanOverrideBeanFactoryPostProcessor, etc...). Parsing of annotations is now fully stateless. In order to avoid OverrideMetadata in bean definition and to make a first step towards AOT support, the BeanOverrideBeanFactoryPostProcessor now delegates to a BeanOverrideRegistrar to track classes to parse, the metadata-related state as well as for the field injection methods for tests. Lastly, this commit increases the test coverage for the provided annotations and adds integration tests and fixes a few `@TestBean` issues. --- .../annotation-beanoverriding.adoc | 32 +- .../context/bean/override/BeanOverride.java | 2 +- .../BeanOverrideBeanFactoryPostProcessor.java | 275 +++++++++++++ .../BeanOverrideBeanPostProcessor.java | 382 ------------------ .../BeanOverrideContextCustomizerFactory.java | 46 +-- ...ser.java => BeanOverrideParsingUtils.java} | 83 ++-- .../bean/override/BeanOverrideProcessor.java | 44 +- .../bean/override/BeanOverrideRegistrar.java | 173 ++++++++ .../bean/override/BeanOverrideStrategy.java | 11 +- .../BeanOverrideTestExecutionListener.java | 29 +- .../bean/override/OverrideMetadata.java | 80 ++-- .../bean/override/convention/TestBean.java | 11 +- .../convention/TestBeanOverrideProcessor.java | 145 ++++--- ...finition.java => MockitoBeanMetadata.java} | 50 +-- .../mockito/MockitoBeanOverrideProcessor.java | 14 +- .../{Definition.java => MockitoMetadata.java} | 30 +- ...ition.java => MockitoSpyBeanMetadata.java} | 42 +- ...verrideBeanFactoryPostProcessorTests.java} | 57 ++- ...ava => BeanOverrideParsingUtilsTests.java} | 38 +- .../bean/override/OverrideMetadataTests.java | 16 +- .../AbstractTestBeanIntegrationTestCase.java | 97 +++++ .../TestBeanInheritanceIntegrationTests.java | 133 ++++++ .../convention/TestBeanIntegrationTests.java | 265 ++++++++++++ .../TestBeanOverrideProcessorTests.java | 45 ++- .../ExampleBeanOverrideAnnotation.java | 2 + .../example/ExampleBeanOverrideProcessor.java | 17 +- .../example/TestOverrideMetadata.java | 46 ++- .../mockito/MockitoBeanIntegrationTests.java | 131 ++++++ .../MockitoBeanOverrideProcessorTests.java | 78 ++++ .../MockitoSpyBeanIntegrationTests.java | 160 ++++++++ 30 files changed, 1714 insertions(+), 820 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java delete mode 100644 spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanPostProcessor.java rename spring-test/src/main/java/org/springframework/test/context/bean/override/{BeanOverrideParser.java => BeanOverrideParsingUtils.java} (51%) create mode 100644 spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java rename spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/{MockDefinition.java => MockitoBeanMetadata.java} (71%) rename spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/{Definition.java => MockitoMetadata.java} (75%) rename spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/{SpyDefinition.java => MockitoSpyBeanMetadata.java} (77%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/{BeanOverrideBeanPostProcessorTests.java => BeanOverrideBeanFactoryPostProcessorTests.java} (81%) rename spring-test/src/test/java/org/springframework/test/context/bean/override/{BeanOverrideParserTests.java => BeanOverrideParsingUtilsTests.java} (69%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/convention/AbstractTestBeanIntegrationTestCase.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanInheritanceIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoSpyBeanIntegrationTests.java 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. *