Bean overriding by type uses isAutowireCandidate for matching

This commit uses the bean factory `isAutowiredCandidate` method directly
in `BeanOverrideBeanFactoryPostProcessor` to select a single match among
multiple candidates when matching by type.

The expected consequence, in most cases, is that this will delegate to
a `@Qualifier`-aware `QualifierAnnotationAutowireCandidateResolver`.
In that sense, bean overriding by-type matching is now potentially
taking Qualifier annotations or meta-annotations into account.

It also changes the way existing bean definitions are checked in case
a bean name has been specified: factory beans are now taken into account
when checking the type of an existing definition matches the expected
bean override type.

Closes gh-32822
This commit is contained in:
Simon Baslé
2024-05-17 16:57:57 +02:00
parent b17d1c5124
commit d5c7a5e2db
12 changed files with 319 additions and 43 deletions

View File

@@ -31,6 +31,7 @@ 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.DependencyDescriptor;
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -127,28 +128,34 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata);
String beanName = overrideMetadata.getBeanName();
BeanDefinition existingBeanDefinition = null;
if (beanName == null) {
final String[] candidates = beanFactory.getBeanNamesForType(overrideMetadata.getBeanType());
if (candidates.length != 1) {
Set<String> candidates = getExistingBeanNamesByType(beanFactory, overrideMetadata, true);
if (candidates.size() != 1) {
Field f = overrideMetadata.getField();
throw new IllegalStateException("Unable to select a bean definition to override, " +
candidates.length+ " bean definitions found of type " + overrideMetadata.getBeanType() +
candidates.size() + " bean definitions found of type " + overrideMetadata.getBeanType() +
" (as required by annotated field '" + f.getDeclaringClass().getSimpleName() +
"." + f.getName() + "')");
}
beanName = candidates[0];
beanName = candidates.iterator().next();
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
}
else {
Set<String> candidates = getExistingBeanNamesByType(beanFactory, overrideMetadata, false);
if (candidates.contains(beanName)) {
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
}
else if (enforceExistingDefinition) {
throw new IllegalStateException("Unable to override bean '" + beanName + "'; there is no" +
" bean definition to replace with that name of type " + overrideMetadata.getBeanType());
}
}
BeanDefinition existingBeanDefinition = null;
if (beanFactory.containsBeanDefinition(beanName)) {
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
if (existingBeanDefinition != null) {
copyBeanDefinitionDetails(existingBeanDefinition, beanDefinition);
registry.removeBeanDefinition(beanName);
}
else if (enforceExistingDefinition) {
throw new IllegalStateException("Unable to override bean '" + beanName + "'; there is no" +
" bean definition to replace with that name");
}
registry.registerBeanDefinition(beanName, beanDefinition);
Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null);
@@ -171,33 +178,40 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
* phase.
*/
private void registerWrapBean(ConfigurableListableBeanFactory beanFactory, OverrideMetadata metadata) {
Set<String> existingBeanNames = getExistingBeanNames(beanFactory, metadata.getBeanType());
String beanName = metadata.getBeanName();
if (beanName == null) {
if (existingBeanNames.size() != 1) {
Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, metadata, true);
if (candidateNames.size() != 1) {
Field f = metadata.getField();
throw new IllegalStateException("Unable to select a bean to override by wrapping, " +
existingBeanNames.size() + " bean instances found of type " + metadata.getBeanType() +
candidateNames.size() + " bean instances found of type " + metadata.getBeanType() +
" (as required by annotated field '" + f.getDeclaringClass().getSimpleName() +
"." + f.getName() + "')");
}
beanName = existingBeanNames.iterator().next();
beanName = candidateNames.iterator().next();
}
else if (!existingBeanNames.contains(beanName)) {
throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping; " +
"there is no existing bean instance with that name of type " + metadata.getBeanType());
else {
Set<String> candidates = getExistingBeanNamesByType(beanFactory, metadata, false);
if (!candidates.contains(beanName)) {
throw new IllegalStateException("Unable to override bean '" + beanName + "' by wrapping; there is no" +
" existing bean instance with that name of type " + metadata.getBeanType());
}
}
this.overrideRegistrar.markWrapEarly(metadata, beanName);
this.overrideRegistrar.registerNameForMetadata(metadata, beanName);
}
private RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) {
RootBeanDefinition definition = new RootBeanDefinition();
definition.setTargetType(metadata.getBeanType());
definition.setQualifiedElement(metadata.getField());
return definition;
}
private Set<String> getExistingBeanNames(ConfigurableListableBeanFactory beanFactory, ResolvableType resolvableType) {
private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory, OverrideMetadata metadata,
boolean checkAutowiredCandidate) {
ResolvableType resolvableType = metadata.getBeanType();
Set<String> beans = new LinkedHashSet<>(
Arrays.asList(beanFactory.getBeanNamesForType(resolvableType, true, false)));
Class<?> type = resolvableType.resolve(Object.class);
@@ -209,7 +223,14 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
beans.add(beanName);
}
}
beans.removeIf(ScopedProxyUtils::isScopedTarget);
if (checkAutowiredCandidate) {
DependencyDescriptor descriptor = new DependencyDescriptor(metadata.getField(), true);
beans.removeIf(beanName -> ScopedProxyUtils.isScopedTarget(beanName) ||
!beanFactory.isAutowireCandidate(beanName, descriptor));
}
else {
beans.removeIf(ScopedProxyUtils::isScopedTarget);
}
return beans;
}

View File

@@ -30,8 +30,9 @@ import org.springframework.test.context.bean.override.BeanOverride;
*
* <p>By default, the bean to override is inferred from the type of the
* annotated field. This requires that exactly one matching definition is
* present in the application context. To explicitly specify a bean name to
* replace, set the {@link #value()} or {@link #name()} attribute.
* present in the application context. A {@code @Qualifier} annotation can be
* used to help disambiguate. Alternatively, you can explicitly specify a bean
* name to replace by setting the {@link #value()} or {@link #name()} attribute.
*
* <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

View File

@@ -32,7 +32,8 @@ import org.springframework.test.context.bean.override.BeanOverride;
*
* <p>If no explicit {@link #name()} is specified, a target bean definition is
* selected according to the class of the annotated field, and there must be
* exactly one such candidate definition in the context.
* exactly one such candidate definition in the context. A {@code @Qualifier}
* annotation can be used to help disambiguate.
* If a {@link #name()} is specified, either the definition exists in the
* application context and is replaced, or it doesn't and a new one is added to
* the context.

View File

@@ -32,7 +32,8 @@ import org.springframework.test.context.bean.override.BeanOverride;
*
* <p>If no explicit {@link #name()} is specified, a target bean is selected
* according to the class of the annotated field, and there must be exactly one
* such candidate bean.
* such candidate bean. A {@code @Qualifier} annotation can be used to help
* disambiguate.
* If a {@link #name()} is specified, it is required that a target bean of that
* name has been previously registered in the application context.
*