diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java index 8d0866b9b159..d6603e3fad6c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java @@ -104,19 +104,6 @@ private void postProcessWithRegistry(ConfigurableListableBeanFactory beanFactory } } - /** - * Copy certain details of a {@link BeanDefinition} to the definition created by - * this processor for a given {@link OverrideMetadata}. - *

The default implementation copies the {@linkplain BeanDefinition#isPrimary() - * primary flag}, @{@linkplain BeanDefinition#isFallback() fallback flag} - * and the {@linkplain BeanDefinition#getScope() scope}. - */ - protected void copyBeanDefinitionDetails(BeanDefinition from, RootBeanDefinition to) { - to.setPrimary(from.isPrimary()); - to.setFallback(from.isFallback()); - to.setScope(from.getScope()); - } - private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry, OverrideMetadata overrideMetadata) { @@ -132,6 +119,8 @@ private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, B private void registerReplaceDefinition(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry, OverrideMetadata overrideMetadata, boolean enforceExistingDefinition) { + // The following is a "dummy" bean definition which should not be used to + // create an actual bean instance. RootBeanDefinition beanDefinition = createBeanDefinition(overrideMetadata); String beanName = overrideMetadata.getBeanName(); String beanNameIncludingFactory; @@ -157,22 +146,27 @@ else if (enforceExistingDefinition) { beanNameIncludingFactory = beanName; } + // Process existing bean definition. if (existingBeanDefinition != null) { - copyBeanDefinitionDetails(existingBeanDefinition, beanDefinition); + copyBeanDefinitionProperties(existingBeanDefinition, beanDefinition); registry.removeBeanDefinition(beanName); } + + // At this point, we either removed an existing bean definition above, or + // there was no bean definition to begin with. So, we register the dummy bean + // definition to ensure that a bean definition exists for the given bean name. registry.registerBeanDefinition(beanName, beanDefinition); Object override = overrideMetadata.createOverride(beanName, existingBeanDefinition, null); + overrideMetadata.track(override, beanFactory); + this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanNameIncludingFactory); + if (beanFactory.isSingleton(beanNameIncludingFactory)) { - // 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. + // 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 an exception if there is such an instance already. beanFactory.registerSingleton(beanName, override); } - - overrideMetadata.track(override, beanFactory); - this.overrideRegistrar.registerNameForMetadata(overrideMetadata, beanNameIncludingFactory); } private String getBeanNameForType(ConfigurableListableBeanFactory beanFactory, BeanDefinitionRegistry registry, @@ -231,13 +225,6 @@ private void registerWrapBean(ConfigurableListableBeanFactory beanFactory, Overr this.overrideRegistrar.registerNameForMetadata(metadata, beanName); } - RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) { - RootBeanDefinition definition = new RootBeanDefinition(metadata.getBeanType().resolve()); - definition.setTargetType(metadata.getBeanType()); - definition.setQualifiedElement(metadata.getField()); - return definition; - } - private Set getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory, OverrideMetadata metadata, boolean checkAutowiredCandidate) { @@ -272,6 +259,26 @@ private Set getExistingBeanNamesByType(ConfigurableListableBeanFactory b } + private static RootBeanDefinition createBeanDefinition(OverrideMetadata metadata) { + RootBeanDefinition definition = new RootBeanDefinition(metadata.getBeanType().resolve()); + definition.setTargetType(metadata.getBeanType()); + definition.setQualifiedElement(metadata.getField()); + return definition; + } + + /** + * Copy the following properties of the source {@link BeanDefinition} to the + * target: the {@linkplain BeanDefinition#isPrimary() primary flag}, the + * {@linkplain BeanDefinition#isFallback() fallback flag}, and the + * {@linkplain BeanDefinition#getScope() scope}. + */ + private static void copyBeanDefinitionProperties(BeanDefinition source, RootBeanDefinition target) { + target.setPrimary(source.isPrimary()); + target.setFallback(source.isFallback()); + target.setScope(source.getScope()); + } + + static class WrapEarlyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, PriorityOrdered { diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java index 719ade2252b0..d6fa2383cf0d 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessorTests.java @@ -51,6 +51,7 @@ * * @author Simon Baslé * @author Stephane Nicoll + * @author Sam Brannen */ class BeanOverrideBeanFactoryPostProcessorTests { @@ -200,7 +201,7 @@ void postProcessorShouldNotTriggerEarlyInitialization() { } @Test - void allowReplaceDefinitionWhenSingletonDefinitionPresent() { + void replaceBeanByNameWithMatchingBeanDefinitionWithExplicitSingletonScope() { AnnotationConfigApplicationContext context = createContext(CaseByName.class); RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL"); definition.setScope(BeanDefinition.SCOPE_SINGLETON); @@ -212,7 +213,7 @@ void allowReplaceDefinitionWhenSingletonDefinitionPresent() { } @Test - void copyDefinitionPrimaryFallbackAndScope() { + void replaceBeanByNameWithMatchingBeanDefinitionRetainsPrimaryFallbackAndScopeProperties() { AnnotationConfigApplicationContext context = createContext(CaseByName.class); context.getBeanFactory().registerScope("customScope", new SimpleThreadScope()); RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL"); @@ -232,22 +233,22 @@ void copyDefinitionPrimaryFallbackAndScope() { } @Test - void createDefinitionShouldSetQualifierElement() { + void qualifiedElementIsSetToBeanOverrideField() { AnnotationConfigApplicationContext context = createContext(CaseByNameWithQualifier.class); context.registerBeanDefinition("descriptionBean", new RootBeanDefinition(String.class, () -> "ORIGINAL")); assertThatNoException().isThrownBy(context::refresh); assertThat(context.getBeanDefinition("descriptionBean")) - .isInstanceOfSatisfying(RootBeanDefinition.class, this::isTheValueField); + .isInstanceOfSatisfying(RootBeanDefinition.class, this::qualifiedElementIsField); } - private void isTheValueField(RootBeanDefinition def) { - assertThat(def.getQualifiedElement()).isInstanceOfSatisfying(Field.class, field -> { - assertThat(field.getDeclaringClass()).isEqualTo(CaseByNameWithQualifier.class); - assertThat(field.getName()).as("annotated field name") - .isEqualTo("description"); - }); + private void qualifiedElementIsField(RootBeanDefinition def) { + assertThat(def.getQualifiedElement()).isInstanceOfSatisfying(Field.class, + field -> { + assertThat(field.getDeclaringClass()).isEqualTo(CaseByNameWithQualifier.class); + assertThat(field.getName()).as("annotated field name").isEqualTo("description"); + }); } private AnnotationConfigApplicationContext createContext(Class testClass) {