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.
This commit is contained in:
Simon Baslé
2024-03-20 11:48:22 +01:00
parent 711ddd1ac6
commit 2d33aac350
30 changed files with 1714 additions and 820 deletions

View File

@@ -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)

View File

@@ -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.
*
* <p>A set of classes from which to parse {@link OverrideMetadata} must be
* provided to this processor. Each test class is expected to use any
* 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.
*
* <p>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}.
* <p>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.
* <p>If so, put the override metadata in the early tracking map.
* <p>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<String> 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<String> getExistingBeanNames(DefaultListableBeanFactory beanFactory, ResolvableType resolvableType) {
Set<String> 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}.
* <p>Not required when using the Spring TestContext Framework, as registration
* is automatic via the
* {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader}
* mechanism.
* @param registry the bean definition registry
*/
public static void register(BeanDefinitionRegistry registry) {
RuntimeBeanReference registrarReference = new RuntimeBeanReference(BeanOverrideRegistrar.INFRASTRUCTURE_BEAN_NAME);
// Early processor
addInfrastructureBeanDefinition(
registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
constructorArgs.addIndexedArgumentValue(0, registrarReference));
// Main processor
addInfrastructureBeanDefinition(
registry, BeanOverrideBeanFactoryPostProcessor.class, INFRASTRUCTURE_BEAN_NAME, constructorArgs ->
constructorArgs.addIndexedArgumentValue(0, registrarReference));
}
private static void addInfrastructureBeanDefinition(BeanDefinitionRegistry registry,
Class<?> clazz, String beanName, Consumer<ConstructorArgumentValues> constructorArgumentsConsumer) {
if (!registry.containsBeanDefinition(beanName)) {
RootBeanDefinition definition = new RootBeanDefinition(clazz);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues();
constructorArgumentsConsumer.accept(constructorArguments);
registry.registerBeanDefinition(beanName, definition);
}
}
static final class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor,
PriorityOrdered {
private final BeanOverrideRegistrar overrideRegistrar;
private final Map<String, Object> 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());
}
}
}

View File

@@ -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}.
*
* <p>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.
*
* <p>This processor supports two types of {@link BeanOverrideStrategy}:
* <ul>
* <li>Replacing a given bean's definition, immediately preparing a singleton
* instance</li>
* <li>Intercepting the actual bean instance upon creation and wrapping it,
* using the early bean definition mechanism of
* {@link SmartInstantiationAwareBeanPostProcessor}</li>
* </ul>
*
* <p>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<String, OverrideMetadata> earlyOverrideMetadata = new HashMap<>();
private final Map<OverrideMetadata, String> beanNameRegistry = new HashMap<>();
private final Map<Field, String> fieldRegistry = new HashMap<>();
private final Set<OverrideMetadata> 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> 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<OverrideMetadata> 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}.
* <p>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.
* <p>If so, put the override metadata in the early tracking map.
* <p>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<String> 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.
* <p>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<String> getExistingBeanNames(ResolvableType resolvableType) {
Assert.state(this.beanFactory != null, "ConfigurableListableBeanFactory must not be null");
Set<String> 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}.
* <p>Not required when using the Spring TestContext Framework, as registration
* is automatic via the
* {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader}
* mechanism.
* @param registry the bean definition registry
* @param overrideMetadata the initial override metadata set
*/
public static void register(BeanDefinitionRegistry registry, @Nullable Set<OverrideMetadata> 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<OverrideMetadata>()));
ConstructorArgumentValues.ValueHolder constructorArg =
definition.getConstructorArgumentValues().getIndexedArgumentValue(0, Set.class);
@SuppressWarnings({"unchecked", "NullAway"})
Set<OverrideMetadata> existing = (Set<OverrideMetadata>) constructorArg.getValue();
if (overrideMetadata != null && existing != null) {
existing.addAll(overrideMetadata);
}
}
private static BeanDefinition getOrAddInfrastructureBeanDefinition(BeanDefinitionRegistry registry,
Class<?> clazz, String beanName, Consumer<ConstructorArgumentValues> constructorArgumentsConsumer) {
if (!registry.containsBeanDefinition(beanName)) {
RootBeanDefinition definition = new RootBeanDefinition(clazz);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues();
constructorArgumentsConsumer.accept(constructorArguments);
registry.registerBeanDefinition(beanName, definition);
return definition;
}
return registry.getBeanDefinition(beanName);
}
private static final class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor,
PriorityOrdered {
private final BeanOverrideBeanPostProcessor mainProcessor;
private final Map<String, Object> 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());
}
}
}

View File

@@ -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<ContextConfigurationAttributes> configAttributes) {
BeanOverrideParser parser = new BeanOverrideParser();
parseMetadata(testClass, parser);
if (parser.getOverrideMetadata().isEmpty()) {
Set<Class<?>> 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<Class<?>> 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<OverrideMetadata> metadata;
private final Set<Class<?>> 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<OverrideMetadata> metadata) {
this.metadata = metadata;
BeanOverrideContextCustomizer(Set<Class<?>> 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();
}
}

View File

@@ -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<OverrideMetadata> parsedMetadata = new LinkedHashSet<>();
abstract class BeanOverrideParsingUtils {
/**
* Get the set of {@link OverrideMetadata} once {@link #parse(Class)} has been called.
*/
Set<OverrideMetadata> 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.
* <p>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}.
* <p>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<OverrideMetadata> parse(Iterable<Class<?>> classes) {
Set<OverrideMetadata> 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<OverrideMetadata> parse(Class<?> clazz) {
return parse(List.of(clazz));
}
private static void parseField(Field field, Class<?> testClass, Set<OverrideMetadata> 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);
});
}

View File

@@ -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}.
*
* <p>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.
* <p>Processors are generally linked to one or more specific concrete
* annotations (meta-annotated with {@link BeanOverride @BeanOverride}) and
* concrete {@link OverrideMetadata} implementations.
*
* <p>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 &mdash; for example, by using the supplied annotation
* to determine the type.
* <p>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}.
* <p>Specific implementations of metadata can have state to be used during
* override {@linkplain OverrideMetadata#createOverride(String, BeanDefinition,
* Object) instance creation} &mdash; 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);
}

View File

@@ -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<OverrideMetadata, String> beanNameRegistry;
private final Map<String, OverrideMetadata> earlyOverrideMetadata;
private final Set<OverrideMetadata> overrideMetadata;
@Nullable
private ConfigurableBeanFactory beanFactory;
/**
* Construct a new registrar and immediately parse the provided classes.
* @param classesToParse the initial set of classes that have been
* detected to contain bean overriding annotations, to be parsed immediately.
*/
BeanOverrideRegistrar(Set<Class<?>> classesToParse) {
Set<OverrideMetadata> metadata = BeanOverrideParsingUtils.parse(classesToParse);
Assert.state(!metadata.isEmpty(), "Expected metadata to be produced by parser");
this.overrideMetadata = metadata;
this.beanNameRegistry = new HashMap<>();
this.earlyOverrideMetadata = new HashMap<>();
}
/**
* Return this processor's {@link OverrideMetadata} set.
*/
Set<OverrideMetadata> getOverrideMetadata() {
return this.overrideMetadata;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
Assert.isInstanceOf(ConfigurableBeanFactory.class, beanFactory,
"Bean OverrideRegistrar can only be used with a ConfigurableBeanFactory");
this.beanFactory = (ConfigurableBeanFactory) beanFactory;
}
/**
* Check {@link #markWrapEarly(OverrideMetadata, String) early override}
* records and use the {@link OverrideMetadata} to create an override
* instance from the provided bean, if relevant.
*/
final Object wrapIfNecessary(Object bean, String beanName) throws BeansException {
final OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName);
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.
* <p>The resulting complete set of test classes will be parsed as soon as
* the {@link BeanOverrideRegistrar} is constructed.
* @param registry the bean definition registry
* @param detectedTestClasses a partial {@link Set} of {@link Class classes}
* that are expected to contain bean overriding annotations
*/
public static void register(BeanDefinitionRegistry registry, @Nullable Set<Class<?>> detectedTestClasses) {
BeanDefinition definition;
if (!registry.containsBeanDefinition(BeanOverrideRegistrar.INFRASTRUCTURE_BEAN_NAME)) {
definition = new RootBeanDefinition(BeanOverrideRegistrar.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues();
constructorArguments.addIndexedArgumentValue(0, new LinkedHashSet<Class<?>>());
registry.registerBeanDefinition(INFRASTRUCTURE_BEAN_NAME, definition);
}
else {
definition = registry.getBeanDefinition(BeanOverrideRegistrar.INFRASTRUCTURE_BEAN_NAME);
}
ConstructorArgumentValues.ValueHolder constructorArg =
definition.getConstructorArgumentValues().getIndexedArgumentValue(0, Set.class);
@SuppressWarnings({"unchecked", "NullAway"})
Set<Class<?>> existing = (Set<Class<?>>) constructorArg.getValue();
if (detectedTestClasses != null && existing != null) {
existing.addAll(detectedTestClasses);
}
}
void inject(Object target, OverrideMetadata overrideMetadata) {
String beanName = this.beanNameRegistry.get(overrideMetadata);
Assert.state(StringUtils.hasLength(beanName),
() -> "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);
}
}
}

View File

@@ -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
}

View File

@@ -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.
* <p>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<TestContextOverrideMetadata,
BeanOverrideBeanPostProcessor> 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);
}
}
}

View File

@@ -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.
*
* <p><strong>WARNING</strong>: implementations are used as a cache key and
* must implement proper {@code equals} and {@code hashCode}t methods.
*
* <p>Specific implementations of metadata can have state to be used during
* override {@linkplain #createOverride(String, BeanDefinition, Object)
* instance creation} &mdash; 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.
* <p>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();
}

View File

@@ -28,9 +28,11 @@ import org.springframework.test.context.bean.override.BeanOverride;
/**
* Mark a field to override a bean instance in the {@code BeanFactory}.
*
* <p>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.
* <p>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.
* <ul>
* <li>If the {@link #methodName()} is specified, look for a static method with
* that name.</li>
@@ -121,6 +123,9 @@ public @interface TestBean {
/**
* Name of a static factory method to look for in the test class, which will
* be used to instantiate the bean to override.
* <p>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.
* <p>If left unspecified, the name of the factory method will be detected
* based on convention.
* @see #CONVENTION_SUFFIX

View File

@@ -24,11 +24,13 @@ import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
import org.springframework.test.context.bean.override.BeanOverrideStrategy;
import org.springframework.test.context.bean.override.OverrideMetadata;
@@ -37,126 +39,155 @@ import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* {@link BeanOverrideProcessor} implementation primarily made to work with
* fields annotated with {@link TestBean @TestBean}, but can also work with
* arbitrary test bean override annotations provided the annotated field's
* declaring class declares an appropriate test bean factory method according
* to the conventions documented in {@link TestBean}.
* {@link BeanOverrideProcessor} implementation for {@link TestBean @TestBean}
* support. It creates metadata for annotated fields in a given class and
* ensures that a corresponding static factory method exists, according to the
* {@linkplain TestBean documented conventions}.
*
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
*/
public class TestBeanOverrideProcessor implements BeanOverrideProcessor {
class TestBeanOverrideProcessor implements BeanOverrideProcessor {
/**
* Find a test bean factory {@link Method} in the given {@link Class} which
* meets the following criteria.
* Find a test bean factory {@link Method} in the given {@link Class} or one
* of its parent classes, which meets the following criteria.
* <ul>
* <li>The method is static.</li>
* <li>The method does not accept any arguments.</li>
* <li>The method's return type matches the supplied {@code methodReturnType}.</li>
* <li>The method's name is one of the supplied {@code methodNames}.</li>
* </ul>
* <p>If the test class inherits from another class, the class hierarchy is
* searched for factory methods. Matching factory methods are prioritized
* from closest to furthest from the test class in the class hierarchy,
* provided they have the same name. However, if multiple methods are found
* that match distinct candidate names, an exception is thrown.
* @param clazz the class in which to search for the factory method
* @param methodReturnType the return type for the factory method
* @param methodNames a set of supported names for the factory method
* @return the corresponding factory method
* @throws IllegalStateException if a single matching factory method cannot
* be found
* @throws IllegalStateException if a matching factory method cannot
* be found or multiple methods have a match
*/
public static Method findTestBeanFactoryMethod(Class<?> clazz, Class<?> methodReturnType, String... methodNames) {
static Method findTestBeanFactoryMethod(Class<?> clazz, Class<?> methodReturnType, String... methodNames) {
Assert.isTrue(methodNames.length > 0, "At least one candidate method name is required");
Set<String> supportedNames = new LinkedHashSet<>(Arrays.asList(methodNames));
List<Method> methods = Arrays.stream(clazz.getDeclaredMethods())
List<Method> methods = Arrays.stream(ReflectionUtils.getAllDeclaredMethods(clazz))
.filter(method -> Modifier.isStatic(method.getModifiers()) &&
supportedNames.contains(method.getName()) &&
methodReturnType.isAssignableFrom(method.getReturnType()))
.toList();
if (methods.isEmpty() && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
methods = Arrays.stream(ReflectionUtils.getAllDeclaredMethods(clazz.getEnclosingClass()))
.filter(method -> Modifier.isStatic(method.getModifiers()) &&
supportedNames.contains(method.getName()) &&
methodReturnType.isAssignableFrom(method.getReturnType()))
.toList();
}
Assert.state(!methods.isEmpty(), () -> """
Failed to find a static test bean factory method in %s with return type %s \
whose name matches one of the supported candidates %s""".formatted(
clazz.getName(), methodReturnType.getName(), supportedNames));
Assert.state(methods.size() == 1, () -> """
long nameCount = methods.stream().map(Method::getName).distinct().count();
int methodCount = methods.size();
Assert.state(nameCount == 1, () -> """
Found %d competing static test bean factory methods in %s with return type %s \
whose name matches one of the supported candidates %s""".formatted(
methods.size(), clazz.getName(), methodReturnType.getName(), supportedNames));
methodCount, clazz.getName(), methodReturnType.getName(), supportedNames));
return methods.get(0);
}
@Override
public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToOverride) {
// If we can, get an explicit method name right away; fail fast if it doesn't match.
if (overrideAnnotation instanceof TestBean testBeanAnnotation) {
Method overrideMethod = null;
String beanName = null;
if (!testBeanAnnotation.methodName().isBlank()) {
Class<?> declaringClass = field.getDeclaringClass();
overrideMethod = findTestBeanFactoryMethod(declaringClass, field.getType(), testBeanAnnotation.methodName());
}
if (!testBeanAnnotation.name().isBlank()) {
beanName = testBeanAnnotation.name();
}
return new MethodConventionOverrideMetadata(field, overrideMethod, beanName,
overrideAnnotation, typeToOverride);
public TestBeanOverrideMetadata createMetadata(Annotation overrideAnnotation, Class<?> testClass, Field field) {
if (!(overrideAnnotation instanceof TestBean testBeanAnnotation)) {
throw new IllegalStateException(String.format("Invalid annotation passed to %s: expected @TestBean on field %s.%s",
TestBeanOverrideProcessor.class.getSimpleName(), field.getDeclaringClass().getName(),
field.getName()));
}
// Otherwise defer the resolution of the static method until OverrideMetadata#createOverride.
return new MethodConventionOverrideMetadata(field, null, null, overrideAnnotation, typeToOverride);
// If the user specified a method explicitly, search for that.
// Otherwise, search candidate factory methods using the convention suffix
// and the explicit bean name (if any) or field name.
Method explicitOverrideMethod;
if (!testBeanAnnotation.methodName().isBlank()) {
explicitOverrideMethod = findTestBeanFactoryMethod(testClass, field.getType(), testBeanAnnotation.methodName());
}
else {
String beanName = testBeanAnnotation.name();
if (!StringUtils.hasText(beanName)) {
explicitOverrideMethod = findTestBeanFactoryMethod(testClass, field.getType(),
field.getName() + TestBean.CONVENTION_SUFFIX);
}
else {
explicitOverrideMethod = findTestBeanFactoryMethod(testClass, field.getType(),
beanName + TestBean.CONVENTION_SUFFIX,
field.getName() + TestBean.CONVENTION_SUFFIX);
}
}
return new TestBeanOverrideMetadata(field, explicitOverrideMethod, testBeanAnnotation, ResolvableType.forField(field, testClass));
}
static final class MethodConventionOverrideMetadata extends OverrideMetadata {
static final class TestBeanOverrideMetadata extends OverrideMetadata {
@Nullable
private final Method overrideMethod;
@Nullable
private final String beanName;
public MethodConventionOverrideMetadata(Field field, @Nullable Method overrideMethod, @Nullable String beanName,
Annotation overrideAnnotation, ResolvableType typeToOverride) {
public TestBeanOverrideMetadata(Field field, Method overrideMethod, TestBean overrideAnnotation,
ResolvableType typeToOverride) {
super(field, overrideAnnotation, typeToOverride, BeanOverrideStrategy.REPLACE_DEFINITION);
super(field, typeToOverride, BeanOverrideStrategy.REPLACE_DEFINITION);
this.beanName = StringUtils.hasText(overrideAnnotation.name()) ?
overrideAnnotation.name() : field.getName();
this.overrideMethod = overrideMethod;
this.beanName = beanName;
}
@Override
protected String getExpectedBeanName() {
if (StringUtils.hasText(this.beanName)) {
return this.beanName;
}
return super.getExpectedBeanName();
}
@Override
public String getBeanOverrideDescription() {
return "@TestBean";
protected String getBeanName() {
return this.beanName;
}
@Override
protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition,
@Nullable Object existingBeanInstance) {
Method methodToInvoke = this.overrideMethod;
if (methodToInvoke == null) {
methodToInvoke = findTestBeanFactoryMethod(field().getDeclaringClass(), field().getType(),
beanName + TestBean.CONVENTION_SUFFIX,
field().getName() + TestBean.CONVENTION_SUFFIX);
}
try {
ReflectionUtils.makeAccessible(methodToInvoke);
return methodToInvoke.invoke(null);
ReflectionUtils.makeAccessible(this.overrideMethod);
return this.overrideMethod.invoke(null);
}
catch (IllegalAccessException | InvocationTargetException ex) {
throw new IllegalArgumentException("Could not invoke bean overriding method " + methodToInvoke.getName() +
throw new IllegalArgumentException("Could not invoke bean overriding method " + this.overrideMethod.getName() +
"; a static method with no formal parameters is expected", ex);
}
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
TestBeanOverrideMetadata that = (TestBeanOverrideMetadata) o;
return Objects.equals(this.overrideMethod, that.overrideMethod)
&& Objects.equals(this.beanName, that.beanName);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), this.overrideMethod, this.beanName);
}
}
}

View File

@@ -16,11 +16,11 @@
package org.springframework.test.context.bean.override.mockito;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import org.mockito.Answers;
@@ -44,7 +44,7 @@ import static org.mockito.Mockito.mock;
* @author Phillip Webb
* @since 6.2
*/
class MockDefinition extends Definition {
class MockitoBeanMetadata extends MockitoMetadata {
private final Set<Class<?>> extraInterfaces;
@@ -53,33 +53,27 @@ class MockDefinition extends Definition {
private final boolean serializable;
MockDefinition(MockitoBean annotation, Field field, ResolvableType typeToMock) {
this(annotation.name(), annotation.reset(), field, annotation, typeToMock,
MockitoBeanMetadata(MockitoBean annotation, Field field, ResolvableType typeToMock) {
this(annotation.name(), annotation.reset(), field, typeToMock,
annotation.extraInterfaces(), annotation.answers(), annotation.serializable());
}
MockDefinition(String name, MockReset reset, Field field, Annotation annotation, ResolvableType typeToMock,
MockitoBeanMetadata(String name, MockReset reset, Field field, ResolvableType typeToMock,
Class<?>[] extraInterfaces, @Nullable Answers answer, boolean serializable) {
super(name, reset, false, field, annotation, typeToMock, BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION);
super(name, reset, false, field, typeToMock, BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION);
Assert.notNull(typeToMock, "TypeToMock must not be null");
this.extraInterfaces = asClassSet(extraInterfaces);
this.answer = (answer != null) ? answer : Answers.RETURNS_DEFAULTS;
this.serializable = serializable;
}
@Override
public String getBeanOverrideDescription() {
return "@MockitoBean";
}
@Override
protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) {
return createMock(beanName);
}
private Set<Class<?>> asClassSet(Class<?>[] classes) {
private Set<Class<?>> asClassSet(@Nullable Class<?>[] classes) {
Set<Class<?>> classSet = new LinkedHashSet<>();
if (classes != null) {
classSet.addAll(Arrays.asList(classes));
@@ -96,8 +90,8 @@ class MockDefinition extends Definition {
}
/**
* Return the answers mode.
* @return the answers mode; never {@code null}
* Return the {@link Answers}.
* @return the answers mode
*/
Answers getAnswer() {
return this.answer;
@@ -119,9 +113,8 @@ class MockDefinition extends Definition {
if (obj == null || obj.getClass() != getClass()) {
return false;
}
MockDefinition other = (MockDefinition) obj;
MockitoBeanMetadata other = (MockitoBeanMetadata) obj;
boolean result = super.equals(obj);
result = result && ObjectUtils.nullSafeEquals(typeToOverride(), other.typeToOverride());
result = result && ObjectUtils.nullSafeEquals(this.extraInterfaces, other.extraInterfaces);
result = result && ObjectUtils.nullSafeEquals(this.answer, other.answer);
result = result && this.serializable == other.serializable;
@@ -130,30 +123,21 @@ class MockDefinition extends Definition {
@Override
public int hashCode() {
int result = super.hashCode();
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(typeToOverride());
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.extraInterfaces);
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.answer);
result = MULTIPLIER * result + Boolean.hashCode(this.serializable);
return result;
return Objects.hash(super.hashCode(), this.extraInterfaces, this.answer, this.serializable);
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("name", this.name)
.append("typeToMock", typeToOverride())
.append("extraInterfaces", this.extraInterfaces)
.append("answer", this.answer)
.append("serializable", this.serializable)
.append("beanName", getBeanName())
.append("fieldType", getBeanType())
.append("extraInterfaces", getExtraInterfaces())
.append("answer", getAnswer())
.append("serializable", isSerializable())
.append("reset", getReset())
.toString();
}
<T> T createMock() {
return createMock(this.name);
}
@SuppressWarnings("unchecked")
<T> T createMock(String name) {
MockSettings settings = MockReset.withSettings(getReset());
@@ -167,7 +151,7 @@ class MockDefinition extends Definition {
if (this.serializable) {
settings.serializable();
}
return (T) mock(typeToOverride().resolve(), settings);
return (T) mock(getBeanType().resolve(), settings);
}
}

View File

@@ -21,7 +21,6 @@ import java.lang.reflect.Field;
import org.springframework.core.ResolvableType;
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
import org.springframework.test.context.bean.override.OverrideMetadata;
/**
* A {@link BeanOverrideProcessor} for mockito-related annotations
@@ -30,18 +29,19 @@ import org.springframework.test.context.bean.override.OverrideMetadata;
* @author Simon Baslé
* @since 6.2
*/
public class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
@Override
public OverrideMetadata createMetadata(Field field, Annotation overrideAnnotation, ResolvableType typeToMock) {
public MockitoMetadata createMetadata(Annotation overrideAnnotation, Class<?> testClass, Field field) {
if (overrideAnnotation instanceof MockitoBean mockBean) {
return new MockDefinition(mockBean, field, typeToMock);
return new MockitoBeanMetadata(mockBean, field, ResolvableType.forField(field, testClass));
}
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
return new SpyDefinition(spyBean, field, typeToMock);
return new MockitoSpyBeanMetadata(spyBean, field, ResolvableType.forField(field, testClass));
}
throw new IllegalArgumentException("Invalid annotation for MockitoBeanOverrideProcessor: " +
overrideAnnotation.getClass().getName());
throw new IllegalStateException(String.format("Invalid annotation passed to MockitoBeanOverrideProcessor: "
+ "expected @MockitoBean/@MockitoSpyBean on field %s.%s",
field.getDeclaringClass().getName(), field.getName()));
}
}

View File

@@ -16,8 +16,8 @@
package org.springframework.test.context.bean.override.mockito;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Objects;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.SingletonBeanRegistry;
@@ -29,15 +29,12 @@ import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Base class for {@link MockDefinition} and {@link SpyDefinition}.
* Base class for {@link MockitoBeanMetadata} and {@link MockitoSpyBeanMetadata}.
*
* @author Phillip Webb
* @since 6.2
*/
abstract class Definition extends OverrideMetadata {
protected static final int MULTIPLIER = 31;
abstract class MockitoMetadata extends OverrideMetadata {
protected final String name;
@@ -46,10 +43,10 @@ abstract class Definition extends OverrideMetadata {
private final boolean proxyTargetAware;
Definition(String name, @Nullable MockReset reset, boolean proxyTargetAware, Field field,
Annotation annotation, ResolvableType typeToOverride, BeanOverrideStrategy strategy) {
MockitoMetadata(String name, @Nullable MockReset reset, boolean proxyTargetAware, Field field,
ResolvableType typeToOverride, BeanOverrideStrategy strategy) {
super(field, annotation, typeToOverride, strategy);
super(field, typeToOverride, strategy);
this.name = name;
this.reset = (reset != null) ? reset : MockReset.AFTER;
this.proxyTargetAware = proxyTargetAware;
@@ -57,11 +54,11 @@ abstract class Definition extends OverrideMetadata {
@Override
protected String getExpectedBeanName() {
protected String getBeanName() {
if (StringUtils.hasText(this.name)) {
return this.name;
}
return super.getExpectedBeanName();
return super.getBeanName();
}
@Override
@@ -104,8 +101,9 @@ abstract class Definition extends OverrideMetadata {
if (obj == null || !getClass().isAssignableFrom(obj.getClass())) {
return false;
}
Definition other = (Definition) obj;
boolean result = ObjectUtils.nullSafeEquals(this.name, other.name);
MockitoMetadata other = (MockitoMetadata) obj;
boolean result = super.equals(obj);
result = result && ObjectUtils.nullSafeEquals(this.name, other.name);
result = result && ObjectUtils.nullSafeEquals(this.reset, other.reset);
result = result && ObjectUtils.nullSafeEquals(this.proxyTargetAware, other.proxyTargetAware);
return result;
@@ -113,11 +111,7 @@ abstract class Definition extends OverrideMetadata {
@Override
public int hashCode() {
int result = 1;
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.name);
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.reset);
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(this.proxyTargetAware);
return result;
return Objects.hash(super.hashCode(), this.name, this.reset, this.proxyTargetAware);
}
}

View File

@@ -16,9 +16,9 @@
package org.springframework.test.context.bean.override.mockito;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.Objects;
import org.mockito.AdditionalAnswers;
import org.mockito.MockSettings;
@@ -44,26 +44,20 @@ import static org.mockito.Mockito.mock;
* @author Phillip Webb
* @since 6.2
*/
class SpyDefinition extends Definition {
class MockitoSpyBeanMetadata extends MockitoMetadata {
SpyDefinition(MockitoSpyBean spyAnnotation, Field field, ResolvableType typeToSpy) {
this(spyAnnotation.name(), spyAnnotation.reset(), spyAnnotation.proxyTargetAware(), field,
spyAnnotation, typeToSpy);
MockitoSpyBeanMetadata(MockitoSpyBean spyAnnotation, Field field, ResolvableType typeToSpy) {
this(spyAnnotation.name(), spyAnnotation.reset(), spyAnnotation.proxyTargetAware(),
field, typeToSpy);
}
SpyDefinition(String name, MockReset reset, boolean proxyTargetAware, Field field, Annotation annotation,
ResolvableType typeToSpy) {
MockitoSpyBeanMetadata(String name, MockReset reset, boolean proxyTargetAware, Field field, ResolvableType typeToSpy) {
super(name, reset, proxyTargetAware, field, annotation, typeToSpy, BeanOverrideStrategy.WRAP_EARLY_BEAN);
super(name, reset, proxyTargetAware, field, typeToSpy, BeanOverrideStrategy.WRAP_BEAN);
Assert.notNull(typeToSpy, "typeToSpy must not be null");
}
@Override
public String getBeanOverrideDescription() {
return "@MockitoSpyBean";
}
@Override
protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition,
@Nullable Object existingBeanInstance) {
@@ -75,41 +69,35 @@ class SpyDefinition extends Definition {
@Override
public boolean equals(@Nullable Object obj) {
// For SpyBean we want the class to be exactly the same.
if (obj == this) {
return true;
}
// For SpyBean we want the class to be exactly the same.
if (obj == null || obj.getClass() != getClass()) {
return false;
}
SpyDefinition that = (SpyDefinition) obj;
return (super.equals(obj) && ObjectUtils.nullSafeEquals(typeToOverride(), that.typeToOverride()));
MockitoSpyBeanMetadata that = (MockitoSpyBeanMetadata) obj;
return (super.equals(obj) && ObjectUtils.nullSafeEquals(getBeanType(), that.getBeanType()));
}
@Override
public int hashCode() {
int result = super.hashCode();
result = MULTIPLIER * result + ObjectUtils.nullSafeHashCode(typeToOverride());
return result;
return Objects.hash(super.hashCode(), getBeanType());
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("name", this.name)
.append("typeToSpy", typeToOverride())
.append("beanName", getBeanName())
.append("beanType", getBeanType())
.append("reset", getReset())
.toString();
}
<T> T createSpy(Object instance) {
return createSpy(this.name, instance);
}
@SuppressWarnings("unchecked")
<T> T createSpy(String name, Object instance) {
Assert.notNull(instance, "Instance must not be null");
Class<?> resolvedTypeToOverride = typeToOverride().resolve();
Class<?> resolvedTypeToOverride = getBeanType().resolve();
Assert.notNull(resolvedTypeToOverride, "Failed to resolve type to override");
Assert.isInstanceOf(resolvedTypeToOverride, instance);
if (Mockito.mockingDetails(instance).isSpy()) {
@@ -125,7 +113,7 @@ class SpyDefinition extends Definition {
Class<?> toSpy;
if (Proxy.isProxyClass(instance.getClass())) {
settings.defaultAnswer(AdditionalAnswers.delegatesTo(instance));
toSpy = typeToOverride().toClass();
toSpy = getBeanType().toClass();
}
else {
settings.defaultAnswer(Mockito.CALLS_REAL_METHODS);