Detect illegal bean definition override during classpath scanning

Closes gh-25952
This commit is contained in:
Juergen Hoeller 2023-08-06 14:03:29 +02:00
parent c596ff5c38
commit eaf54b54c3
6 changed files with 67 additions and 14 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -97,6 +97,19 @@ public interface BeanDefinitionRegistry extends AliasRegistry {
*/ */
int getBeanDefinitionCount(); int getBeanDefinitionCount();
/**
* Determine whether the bean definition for the given name is overridable,
* i.e. whether {@link #registerBeanDefinition} would successfully return
* against an existing definition of the same name.
* <p>The default implementation returns {@code true}.
* @param beanName the name to check
* @return whether the definition for the given bean name is overridable
* @since 6.1
*/
default boolean isBeanDefinitionOverridable(String beanName) {
return true;
}
/** /**
* Determine whether the given bean name is already in use within this registry, * Determine whether the given bean name is already in use within this registry,
* i.e. whether there is a local bean or alias registered under this name. * i.e. whether there is a local bean or alias registered under this name.

View File

@ -1011,7 +1011,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName); BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
if (existingDefinition != null) { if (existingDefinition != null) {
if (!isAllowBeanDefinitionOverriding()) { if (!isBeanDefinitionOverridable(beanName)) {
throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition); throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
} }
else if (existingDefinition.getRole() < beanDefinition.getRole()) { else if (existingDefinition.getRole() < beanDefinition.getRole()) {
@ -1040,8 +1040,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
} }
else { else {
if (isAlias(beanName)) { if (isAlias(beanName)) {
if (!isAllowBeanDefinitionOverriding()) { String aliasedName = canonicalName(beanName);
String aliasedName = canonicalName(beanName); if (!isBeanDefinitionOverridable(aliasedName)) {
if (containsBeanDefinition(aliasedName)) { // alias for existing bean definition if (containsBeanDefinition(aliasedName)) { // alias for existing bean definition
throw new BeanDefinitionOverrideException( throw new BeanDefinitionOverrideException(
beanName, beanDefinition, getBeanDefinition(aliasedName)); beanName, beanDefinition, getBeanDefinition(aliasedName));
@ -1150,8 +1150,19 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
} }
} }
/**
* This implementation returns {@code true} if bean definition overriding
* is generally allowed.
* @see #setAllowBeanDefinitionOverriding
*/
@Override
public boolean isBeanDefinitionOverridable(String beanName) {
return isAllowBeanDefinitionOverriding();
}
/** /**
* Only allows alias overriding if bean definition overriding is allowed. * Only allows alias overriding if bean definition overriding is allowed.
* @see #setAllowBeanDefinitionOverriding
*/ */
@Override @Override
protected boolean allowAliasOverriding() { protected boolean allowAliasOverriding() {
@ -1164,7 +1175,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
@Override @Override
protected void checkForAliasCircle(String name, String alias) { protected void checkForAliasCircle(String name, String alias) {
super.checkForAliasCircle(name, alias); super.checkForAliasCircle(name, alias);
if (!isAllowBeanDefinitionOverriding() && containsBeanDefinition(alias)) { if (!isBeanDefinitionOverridable(alias) && containsBeanDefinition(alias)) {
throw new IllegalStateException("Cannot register alias '" + alias + throw new IllegalStateException("Cannot register alias '" + alias +
"' for name '" + name + "': Alias would override bean definition '" + alias + "'"); "' for name '" + name + "': Alias would override bean definition '" + alias + "'");
} }

View File

@ -329,21 +329,31 @@ public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateCo
* @return {@code true} if the bean can be registered as-is; * @return {@code true} if the bean can be registered as-is;
* {@code false} if it should be skipped because there is an * {@code false} if it should be skipped because there is an
* existing, compatible bean definition for the specified name * existing, compatible bean definition for the specified name
* @throws ConflictingBeanDefinitionException if an existing, incompatible * @throws IllegalStateException if an existing, incompatible bean definition
* bean definition has been found for the specified name * has been found for the specified name
*/ */
protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException { protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
if (!this.registry.containsBeanDefinition(beanName)) { if (!this.registry.containsBeanDefinition(beanName)) {
return true; return true;
} }
BeanDefinition existingDef = this.registry.getBeanDefinition(beanName); BeanDefinition existingDef = this.registry.getBeanDefinition(beanName);
BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition(); BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();
if (originatingDef != null) { if (originatingDef != null) {
existingDef = originatingDef; existingDef = originatingDef;
} }
// Explicitly registered overriding bean?
if (!(existingDef instanceof ScannedGenericBeanDefinition) &&
this.registry.isBeanDefinitionOverridable(beanName)) {
return false;
}
// Scanned same file or equivalent class twice?
if (isCompatible(beanDefinition, existingDef)) { if (isCompatible(beanDefinition, existingDef)) {
return false; return false;
} }
throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName + throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName +
"' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " + "' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " +
"non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]"); "non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]");
@ -354,16 +364,15 @@ public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateCo
* the given existing bean definition. * the given existing bean definition.
* <p>The default implementation considers them as compatible when the existing * <p>The default implementation considers them as compatible when the existing
* bean definition comes from the same source or from a non-scanning source. * bean definition comes from the same source or from a non-scanning source.
* @param newDefinition the new bean definition, originated from scanning * @param newDef the new bean definition, originated from scanning
* @param existingDefinition the existing bean definition, potentially an * @param existingDef the existing bean definition, potentially an
* explicitly defined one or a previously generated one from scanning * explicitly defined one or a previously generated one from scanning
* @return whether the definitions are considered as compatible, with the * @return whether the definitions are considered as compatible, with the
* new definition to be skipped in favor of the existing definition * new definition to be skipped in favor of the existing definition
*/ */
protected boolean isCompatible(BeanDefinition newDefinition, BeanDefinition existingDefinition) { protected boolean isCompatible(BeanDefinition newDef, BeanDefinition existingDef) {
return (!(existingDefinition instanceof ScannedGenericBeanDefinition) || // explicitly registered overriding bean return ((newDef.getSource() != null && newDef.getSource().equals(existingDef.getSource())) ||
(newDefinition.getSource() != null && newDefinition.getSource().equals(existingDefinition.getSource())) || // scanned same file twice newDef.equals(existingDef));
newDefinition.equals(existingDefinition)); // scanned equivalent class twice
} }

View File

@ -314,7 +314,7 @@ class ConfigurationClassBeanDefinitionReader {
// At this point, it's a top-level override (probably XML), just having been parsed // At this point, it's a top-level override (probably XML), just having been parsed
// before configuration class processing kicks in... // before configuration class processing kicks in...
if (this.registry instanceof DefaultListableBeanFactory dlbf && !dlbf.isAllowBeanDefinitionOverriding()) { if (this.registry instanceof DefaultListableBeanFactory dlbf && !dlbf.isBeanDefinitionOverridable(beanName)) {
throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(), throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(),
beanName, "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef); beanName, "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef);
} }

View File

@ -360,6 +360,11 @@ public class GenericApplicationContext extends AbstractApplicationContext implem
return this.beanFactory.getBeanDefinition(beanName); return this.beanFactory.getBeanDefinition(beanName);
} }
@Override
public boolean isBeanDefinitionOverridable(String beanName) {
return this.beanFactory.isBeanDefinitionOverridable(beanName);
}
@Override @Override
public boolean isBeanNameInUse(String beanName) { public boolean isBeanNameInUse(String beanName) {
return this.beanFactory.isBeanNameInUse(beanName); return this.beanFactory.isBeanNameInUse(beanName);

View File

@ -197,16 +197,31 @@ public class ClassPathBeanDefinitionScannerTests {
context.registerBeanDefinition("stubFooDao", new RootBeanDefinition(TestBean.class)); context.registerBeanDefinition("stubFooDao", new RootBeanDefinition(TestBean.class));
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
scanner.setIncludeAnnotationConfig(false); scanner.setIncludeAnnotationConfig(false);
// should not fail! // should not fail!
scanner.scan(BASE_PACKAGE); scanner.scan(BASE_PACKAGE);
} }
@Test
public void testSimpleScanWithDefaultFiltersAndOverridingBeanNotAllowed() {
GenericApplicationContext context = new GenericApplicationContext();
context.getDefaultListableBeanFactory().setAllowBeanDefinitionOverriding(false);
context.registerBeanDefinition("stubFooDao", new RootBeanDefinition(TestBean.class));
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
scanner.setIncludeAnnotationConfig(false);
assertThatIllegalStateException().isThrownBy(() -> scanner.scan(BASE_PACKAGE))
.withMessageContaining("stubFooDao")
.withMessageContaining(StubFooDao.class.getName());
}
@Test @Test
public void testSimpleScanWithDefaultFiltersAndDefaultBeanNameClash() { public void testSimpleScanWithDefaultFiltersAndDefaultBeanNameClash() {
GenericApplicationContext context = new GenericApplicationContext(); GenericApplicationContext context = new GenericApplicationContext();
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
scanner.setIncludeAnnotationConfig(false); scanner.setIncludeAnnotationConfig(false);
scanner.scan("org.springframework.context.annotation3"); scanner.scan("org.springframework.context.annotation3");
assertThatIllegalStateException().isThrownBy(() -> scanner.scan(BASE_PACKAGE)) assertThatIllegalStateException().isThrownBy(() -> scanner.scan(BASE_PACKAGE))
.withMessageContaining("stubFooDao") .withMessageContaining("stubFooDao")
.withMessageContaining(StubFooDao.class.getName()); .withMessageContaining(StubFooDao.class.getName());