From 45e9f04b57f0814a1fc97f3b825adcb4160e3f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sun, 9 Jun 2024 10:16:36 +0200 Subject: [PATCH] Remove TestOverrideMetadata and use dummy implementation TestOverrideMetadata was doing byName lookup by default, even without a bean name. This makes writing tests that need to do by type lookup way more complicated that it should. This commit introduces DummyBean, that merely replaces two types with hardcoded values. It also exposes the raw attribute of OverrideMetadata and doesn't override anything from it to make sure the behavior is not altered. Closes gh-32982 --- ...OverrideBeanFactoryPostProcessorTests.java | 222 +++++++----------- ...OverrideContextCustomizerFactoryTests.java | 55 +---- .../test/context/bean/override/DummyBean.java | 86 +++++++ .../bean/override/OverrideMetadataTests.java | 45 +--- .../ExampleBeanOverrideAnnotation.java | 40 ---- .../example/ExampleBeanOverrideProcessor.java | 40 ---- .../TestBeanOverrideMetaAnnotation.java | 27 --- .../example/TestOverrideMetadata.java | 144 ------------ 8 files changed, 189 insertions(+), 470 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideAnnotation.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestBeanOverrideMetaAnnotation.java delete mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java 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 b603cb6df4..139c5b4a2a 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 @@ -33,16 +33,10 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.context.support.SimpleThreadScope; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.test.context.MergedContextConfiguration; -import org.springframework.test.context.bean.override.example.ExampleBeanOverrideAnnotation; -import org.springframework.test.context.bean.override.example.ExampleService; -import org.springframework.test.context.bean.override.example.FailingExampleService; -import org.springframework.test.context.bean.override.example.RealExampleService; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.Assert; @@ -56,123 +50,123 @@ import static org.mockito.Mockito.mock; * {@link BeanOverrideRegistrar}. * * @author Simon Baslé + * @author Stephane Nicoll */ class BeanOverrideBeanFactoryPostProcessorTests { @Test void canReplaceExistingBeanDefinitions() { - AnnotationConfigApplicationContext context = createContext(ReplaceBeans.class); - context.register(ReplaceBeans.class); - context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected")); - context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected")); - + AnnotationConfigApplicationContext context = createContext(CaseByNameAndByType.class); + context.registerBean("descriptionBean", String.class, () -> "Original"); + context.registerBean("someInteger", Integer.class, () -> 1); context.refresh(); - assertThat(context.getBean("explicit")).isSameAs(OVERRIDE_SERVICE); - assertThat(context.getBean("implicitName")).isSameAs(OVERRIDE_SERVICE); + assertThat(context.getBean("descriptionBean")).isEqualTo("overridden"); + assertThat(context.getBean("someInteger")).isSameAs(42); } @Test - void cannotReplaceIfNoBeanMatching() { - AnnotationConfigApplicationContext context = createContext(ReplaceBeans.class); - context.register(ReplaceBeans.class); - //note we don't register any original bean here + void cannotReplaceIfNoBeanNameMatching() { + AnnotationConfigApplicationContext context = createContext(CaseByName.class); assertThatIllegalStateException() .isThrownBy(context::refresh) - .withMessage("Unable to override bean 'explicit': there is no bean definition " + - "to replace with that name of type org.springframework.test.context.bean.override.example.ExampleService"); + .withMessage("Unable to override bean 'descriptionBean': there is no bean definition " + + "to replace with that name of type java.lang.String"); + } + + @Test + void cannotReplaceIfNoBeanTypeMatching() { + AnnotationConfigApplicationContext context = createContext(CaseByType.class); + + assertThatIllegalStateException() + .isThrownBy(context::refresh) + .withMessage("Unable to override bean: no bean definitions of type java.lang.Integer " + + "(as required by annotated field 'CaseByType.counter')"); } @Test void canReplaceExistingBeanDefinitionsWithCreateReplaceStrategy() { - AnnotationConfigApplicationContext context = createContext(CreateIfOriginalIsMissingBean.class); - context.register(CreateIfOriginalIsMissingBean.class); - context.registerBean("explicit", ExampleService.class, () -> new RealExampleService("unexpected")); - context.registerBean("implicitName", ExampleService.class, () -> new RealExampleService("unexpected")); - + AnnotationConfigApplicationContext context = createContext(CaseByNameAndByTypeWithReplaceOrCreateStrategy.class); + context.register(CaseByNameAndByTypeWithReplaceOrCreateStrategy.class); + context.registerBean("descriptionBean", String.class, () -> "Original"); + context.registerBean("someInteger", Integer.class, () -> 1); context.refresh(); - assertThat(context.getBean("explicit")).isSameAs(OVERRIDE_SERVICE); - assertThat(context.getBean("implicitName")).isSameAs(OVERRIDE_SERVICE); + assertThat(context.getBean("descriptionBean")).isEqualTo("overridden"); + assertThat(context.getBean("someInteger")).isEqualTo(42); } @Test void canCreateIfOriginalMissingWithCreateReplaceStrategy() { - AnnotationConfigApplicationContext context = createContext(CreateIfOriginalIsMissingBean.class); - context.register(CreateIfOriginalIsMissingBean.class); - //note we don't register original beans here - + AnnotationConfigApplicationContext context = createContext(CaseByNameAndByTypeWithReplaceOrCreateStrategy.class); context.refresh(); - assertThat(context.getBean("explicit")).isSameAs(OVERRIDE_SERVICE); - assertThat(context.getBean("implicitName")).isSameAs(OVERRIDE_SERVICE); + String byTypeGeneratedBeanName = "java.lang.Integer#0"; + assertThat(context.getBeanDefinitionNames()).contains("descriptionBean", byTypeGeneratedBeanName); + assertThat(context.getBean("descriptionBean")).isEqualTo("overridden"); + assertThat(context.getBean(byTypeGeneratedBeanName)).isEqualTo(42); } @Test void canOverrideBeanProducedByFactoryBeanWithClassObjectTypeAttribute() { - AnnotationConfigApplicationContext context = createContext(OverriddenFactoryBean.class); - RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class); - factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, SomeInterface.class); - context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition); - context.register(OverriddenFactoryBean.class); - + AnnotationConfigApplicationContext context = prepareContextWithFactoryBean(CharSequence.class); context.refresh(); - assertThat(context.getBean("beanToBeOverridden")).isSameAs(OVERRIDE); + assertThat(context.getBean("beanToBeOverridden")).isEqualTo("overridden"); } @Test void canOverrideBeanProducedByFactoryBeanWithResolvableTypeObjectTypeAttribute() { - AnnotationConfigApplicationContext context = createContext(OverriddenFactoryBean.class); - RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class); - ResolvableType objectType = ResolvableType.forClass(SomeInterface.class); - factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, objectType); - context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition); - context.register(OverriddenFactoryBean.class); - + AnnotationConfigApplicationContext context = prepareContextWithFactoryBean(ResolvableType.forClass(CharSequence.class)); context.refresh(); - assertThat(context.getBean("beanToBeOverridden")).isSameAs(OVERRIDE); + assertThat(context.getBean("beanToBeOverridden")).isEqualTo("overridden"); + } + + private AnnotationConfigApplicationContext prepareContextWithFactoryBean(Object objectTypeAttribute) { + AnnotationConfigApplicationContext context = createContext(CaseOverrideBeanProducedByFactoryBean.class); + context.registerBean("testFactoryBean", TestFactoryBean.class, TestFactoryBean::new); + RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class); + factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, objectTypeAttribute); + context.registerBeanDefinition("beanToBeOverridden", factoryBeanDefinition); + return context; } @Test void postProcessorShouldNotTriggerEarlyInitialization() { - AnnotationConfigApplicationContext context = createContext(EagerInitBean.class); + AnnotationConfigApplicationContext context = createContext(CaseByTypeWithReplaceOrCreateStrategy.class); context.register(FactoryBeanRegisteringPostProcessor.class); context.register(EarlyBeanInitializationDetector.class); - context.register(EagerInitBean.class); assertThatNoException().isThrownBy(context::refresh); } @Test void allowReplaceDefinitionWhenSingletonDefinitionPresent() { - AnnotationConfigApplicationContext context = createContext(SingletonBean.class); + AnnotationConfigApplicationContext context = createContext(CaseByName.class); RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL"); definition.setScope(BeanDefinition.SCOPE_SINGLETON); - context.registerBeanDefinition("singleton", definition); - context.register(SingletonBean.class); + context.registerBeanDefinition("descriptionBean", definition); assertThatNoException().isThrownBy(context::refresh); - assertThat(context.isSingleton("singleton")).as("isSingleton").isTrue(); - assertThat(context.getBean("singleton")).as("overridden").isEqualTo("USED THIS"); + assertThat(context.isSingleton("descriptionBean")).as("isSingleton").isTrue(); + assertThat(context.getBean("descriptionBean")).isEqualTo("overridden"); } @Test void copyDefinitionPrimaryFallbackAndScope() { - AnnotationConfigApplicationContext context = createContext(SingletonBean.class); + AnnotationConfigApplicationContext context = createContext(CaseByName.class); context.getBeanFactory().registerScope("customScope", new SimpleThreadScope()); RootBeanDefinition definition = new RootBeanDefinition(String.class, () -> "ORIGINAL"); definition.setScope("customScope"); definition.setPrimary(true); definition.setFallback(true); - context.registerBeanDefinition("singleton", definition); - context.register(SingletonBean.class); + context.registerBeanDefinition("descriptionBean", definition); assertThatNoException().isThrownBy(context::refresh); - assertThat(context.getBeanDefinition("singleton")) + assertThat(context.getBeanDefinition("descriptionBean")) .isNotSameAs(definition) .matches(BeanDefinition::isPrimary, "isPrimary") .matches(BeanDefinition::isFallback, "isFallback") @@ -183,22 +177,20 @@ class BeanOverrideBeanFactoryPostProcessorTests { @Test void createDefinitionShouldSetQualifierElement() { - AnnotationConfigApplicationContext context = createContext(QualifiedBean.class); - context.registerBeanDefinition("singleton", new RootBeanDefinition(String.class, () -> "ORIGINAL")); - context.register(QualifiedBean.class); + AnnotationConfigApplicationContext context = createContext(CaseByNameWithQualifier.class); + context.registerBeanDefinition("descriptionBean", new RootBeanDefinition(String.class, () -> "ORIGINAL")); assertThatNoException().isThrownBy(context::refresh); - - assertThat(context.getBeanDefinition("singleton")) + assertThat(context.getBeanDefinition("descriptionBean")) .isInstanceOfSatisfying(RootBeanDefinition.class, this::isTheValueField); } private void isTheValueField(RootBeanDefinition def) { assertThat(def.getQualifiedElement()).isInstanceOfSatisfying(Field.class, field -> { - assertThat(field.getDeclaringClass()).isEqualTo(QualifiedBean.class); + assertThat(field.getDeclaringClass()).isEqualTo(CaseByNameWithQualifier.class); assertThat(field.getName()).as("annotated field name") - .isEqualTo("value"); + .isEqualTo("description"); }); } @@ -210,99 +202,67 @@ class BeanOverrideBeanFactoryPostProcessorTests { } - /* - Classes to parse and register with the bean post processor - ----- - Note that some of these are both a @Configuration class and bean override field holder. - This is for this test convenience, as typically the bean override annotated fields - should not be in configuration classes but rather in test case classes - (where a TestExecutionListener automatically discovers and parses them). - */ + static class CaseByName { - static final SomeInterface OVERRIDE = new SomeImplementation(); + @DummyBean(beanName = "descriptionBean") + private String description; - static final ExampleService OVERRIDE_SERVICE = new FailingExampleService(); - - static class ReplaceBeans { - - @ExampleBeanOverrideAnnotation(value = "useThis", beanName = "explicit") - private ExampleService explicitName; - - @ExampleBeanOverrideAnnotation(value = "useThis") - private ExampleService implicitName; - - static ExampleService useThis() { - return OVERRIDE_SERVICE; - } } - static class CreateIfOriginalIsMissingBean { + static class CaseByType { - @ExampleBeanOverrideAnnotation(value = "useThis", createIfMissing = true, beanName = "explicit") - private ExampleService explicitName; + @DummyBean + private Integer counter; - @ExampleBeanOverrideAnnotation(value = "useThis", createIfMissing = true) - private ExampleService implicitName; - - static ExampleService useThis() { - return OVERRIDE_SERVICE; - } } - @Configuration(proxyBeanMethods = false) - static class OverriddenFactoryBean { + static class CaseByNameAndByType { - @ExampleBeanOverrideAnnotation(value = "fOverride", beanName = "beanToBeOverridden") - SomeInterface f; + @DummyBean(beanName = "descriptionBean") + private String description; - static SomeInterface fOverride() { - return OVERRIDE; - } + @DummyBean + private Integer counter; - @Bean - TestFactoryBean testFactoryBean() { - return new TestFactoryBean(); - } } - static class EagerInitBean { + static class CaseByTypeWithReplaceOrCreateStrategy { - @ExampleBeanOverrideAnnotation(value = "useThis", createIfMissing = true) - private ExampleService service; + @DummyBean(strategy = BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION) + private String description; - static ExampleService useThis() { - return OVERRIDE_SERVICE; - } } - static class SingletonBean { + static class CaseByNameAndByTypeWithReplaceOrCreateStrategy { - @ExampleBeanOverrideAnnotation(beanName = "singleton", - value = "useThis", createIfMissing = false) - private String value; + @DummyBean(beanName = "descriptionBean", strategy = BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION) + private String description; + + @DummyBean(strategy = BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION) + private Integer counter; - static String useThis() { - return "USED THIS"; - } } - static class QualifiedBean { + static class CaseOverrideBeanProducedByFactoryBean { + + @DummyBean(beanName = "beanToBeOverridden") + CharSequence description; + + } + + static class CaseByNameWithQualifier { @Qualifier("preferThis") - @ExampleBeanOverrideAnnotation(beanName = "singleton", - value = "useThis", createIfMissing = false) - private String value; + @DummyBean(beanName = "descriptionBean") + private String description; - static String useThis() { - return "USED THIS"; - } } static class TestFactoryBean implements FactoryBean { @Override public Object getObject() { - return new SomeImplementation(); + return "test"; } @Override @@ -341,10 +301,4 @@ class BeanOverrideBeanFactoryPostProcessorTests { } } - interface SomeInterface { - } - - static class SomeImplementation implements SomeInterface { - } - } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java index d26473270a..bf70345024 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizerFactoryTests.java @@ -25,10 +25,9 @@ import org.junit.jupiter.api.Test; import org.springframework.lang.Nullable; import org.springframework.test.context.bean.override.BeanOverrideContextCustomizerFactoryTests.Test2.Green; import org.springframework.test.context.bean.override.BeanOverrideContextCustomizerFactoryTests.Test2.Orange; -import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor.DummyOverrideMetadata; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link BeanOverrideContextCustomizerFactory}. @@ -48,14 +47,14 @@ class BeanOverrideContextCustomizerFactoryTests { void createContextCustomizerWhenTestHasSingleBeanOverride() { BeanOverrideContextCustomizer customizer = createContextCustomizer(Test1.class); assertThat(customizer).isNotNull(); - assertThat(customizer.getMetadata()).singleElement().satisfies(testMetadata(null, String.class)); + assertThat(customizer.getMetadata()).singleElement().satisfies(dummyMetadata(null, String.class)); } @Test void createContextCustomizerWhenNestedTestHasSingleBeanOverrideInParent() { BeanOverrideContextCustomizer customizer = createContextCustomizer(Orange.class); assertThat(customizer).isNotNull(); - assertThat(customizer.getMetadata()).singleElement().satisfies(testMetadata(null, String.class)); + assertThat(customizer.getMetadata()).singleElement().satisfies(dummyMetadata(null, String.class)); } @Test @@ -63,25 +62,19 @@ class BeanOverrideContextCustomizerFactoryTests { BeanOverrideContextCustomizer customizer = createContextCustomizer(Green.class); assertThat(customizer).isNotNull(); assertThat(customizer.getMetadata()) - .anySatisfy(testMetadata(null, String.class)) - .anySatisfy(testMetadata("counterBean", Integer.class)) + .anySatisfy(dummyMetadata(null, String.class)) + .anySatisfy(dummyMetadata("counterBean", Integer.class)) .hasSize(2); } - @Test - void createContextCustomizerWhenTestHasInvalidTestBeanTargetMethod() { - assertThatIllegalStateException() - .isThrownBy(() -> createContextCustomizer(InvalidTestMissingMethod.class)) - .withMessageContaining("Failed to find a static test bean factory method"); + + private Consumer dummyMetadata(@Nullable String beanName, Class beanType) { + return dummyMetadata(beanName, beanType, BeanOverrideStrategy.REPLACE_DEFINITION); } - - private Consumer testMetadata(@Nullable String beanName, Class beanType) { - return overrideMetadata(beanName, beanType, BeanOverrideStrategy.REPLACE_DEFINITION); - } - - private Consumer overrideMetadata(@Nullable String beanName, Class beanType, BeanOverrideStrategy strategy) { + private Consumer dummyMetadata(@Nullable String beanName, Class beanType, BeanOverrideStrategy strategy) { return metadata -> { + assertThat(metadata).isExactlyInstanceOf(DummyOverrideMetadata.class); assertThat(metadata.getBeanName()).isEqualTo(beanName); assertThat(metadata.getBeanType().toClass()).isEqualTo(beanType); assertThat(metadata.getStrategy()).isEqualTo(strategy); @@ -95,24 +88,16 @@ class BeanOverrideContextCustomizerFactoryTests { static class Test1 { - @TestBean(methodName = "descriptor") + @DummyBean private String descriptor; - private static String descriptor() { - return "Overridden descriptor"; - } - } static class Test2 { - @TestBean(methodName = "name") + @DummyBean private String name; - private static String name() { - return "Overridden name"; - } - @Nested class Orange { @@ -121,24 +106,10 @@ class BeanOverrideContextCustomizerFactoryTests { @Nested class Green { - @TestBean(name = "counterBean", methodName = "counter") + @DummyBean(beanName = "counterBean") private Integer counter; - private static Integer counter() { - return 42; - } } } - static class InvalidTestMissingMethod { - - @TestBean(methodName = "descriptor") - private String descriptor; - - private String descriptor() { - return "Never called, not static"; - } - - } - } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java new file mode 100644 index 0000000000..55f01fd035 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/DummyBean.java @@ -0,0 +1,86 @@ +/* + * 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.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor; +import org.springframework.util.StringUtils; + +/** + * A dummy {@link BeanOverride} implementation that only handles {@link CharSequence} + * and {@link Integer} and replace them with {@code "overridden"} and {@code 42}, + * respectively. + * + * @author Stephane Nicoll + */ +@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@BeanOverride(DummyBeanOverrideProcessor.class) +@interface DummyBean { + + String beanName() default ""; + + BeanOverrideStrategy strategy() default BeanOverrideStrategy.REPLACE_DEFINITION; + + class DummyBeanOverrideProcessor implements BeanOverrideProcessor { + + @Override + public OverrideMetadata createMetadata(Annotation annotation, Class testClass, Field field) { + DummyBean dummyBean = (DummyBean) annotation; + String beanName = (StringUtils.hasText(dummyBean.beanName()) ? dummyBean.beanName() : null); + return new DummyBeanOverrideProcessor.DummyOverrideMetadata(field, field.getType(), beanName, + dummyBean.strategy()); + } + + // Bare bone, "dummy", implementation that should not override anything + // else than createOverride. + static class DummyOverrideMetadata extends OverrideMetadata { + + DummyOverrideMetadata(Field field, Class typeToOverride, @Nullable String beanName, + BeanOverrideStrategy strategy) { + + super(field, ResolvableType.forClass(typeToOverride), beanName, strategy); + } + + @Override + protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, + @Nullable Object existingBeanInstance) { + + Class beanType = getField().getType(); + if (CharSequence.class.isAssignableFrom(beanType)) { + return "overridden"; + } + else if (Integer.class.isAssignableFrom(beanType)) { + return 42; + } + throw new IllegalStateException("Could not handle bean type " + beanType); + } + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java index 8724b39313..1cf31c70c3 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/OverrideMetadataTests.java @@ -16,8 +16,6 @@ package org.springframework.test.context.bean.override; -import java.lang.annotation.Annotation; -import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -30,9 +28,8 @@ import java.util.function.Consumer; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; +import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor.DummyOverrideMetadata; import org.springframework.test.context.bean.override.example.CustomQualifier; import org.springframework.test.context.bean.override.example.ExampleService; import org.springframework.util.ReflectionUtils; @@ -157,7 +154,7 @@ public class OverrideMetadataTests { } private OverrideMetadata createMetadata(Field field, @Nullable String name) { - return new DummyOverrideMetadata(field, field.getType(), name); + return new DummyOverrideMetadata(field, field.getType(), name, BeanOverrideStrategy.REPLACE_DEFINITION); } private Field field(Class target, String fieldName) { @@ -248,42 +245,4 @@ public class OverrideMetadataTests { @DummyBean public @interface MetaDummyBean {} - @Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) - @Retention(RetentionPolicy.RUNTIME) - @Documented - @BeanOverride(DummyBeanOverrideProcessor.class) - public @interface DummyBean {} - - static class DummyBeanOverrideProcessor implements BeanOverrideProcessor { - - @Override - public OverrideMetadata createMetadata(Annotation overrideAnnotation, Class testClass, Field field) { - return new DummyOverrideMetadata(field, field.getType(), null); - } - } - - static class DummyOverrideMetadata extends OverrideMetadata { - - @Nullable - private final String beanName; - - DummyOverrideMetadata(Field field, Class typeToOverride, @Nullable String beanName) { - super(field, ResolvableType.forClass(typeToOverride), beanName, BeanOverrideStrategy.REPLACE_DEFINITION); - this.beanName = beanName; - } - - @Override - @Nullable - public String getBeanName() { - return this.beanName; - } - - @Override - protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, - @Nullable Object existingBeanInstance) { - - return BeanOverrideStrategy.REPLACE_DEFINITION; - } - } - } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideAnnotation.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideAnnotation.java deleted file mode 100644 index 81e61b8870..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideAnnotation.java +++ /dev/null @@ -1,40 +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.example; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.test.context.bean.override.BeanOverride; - -@BeanOverride(ExampleBeanOverrideProcessor.class) -@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface ExampleBeanOverrideAnnotation { - - String DEFAULT_VALUE = "TEST OVERRIDE"; - // Any metadata using this as the prefix for the bean name will be considered equal - String DUPLICATE_TRIGGER = "DUPLICATE"; - - String value() default DEFAULT_VALUE; - - boolean createIfMissing() default false; - - String beanName() default ""; -} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java deleted file mode 100644 index 6ecbe2f1bc..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/ExampleBeanOverrideProcessor.java +++ /dev/null @@ -1,40 +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.example; - -import java.lang.annotation.Annotation; -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; - -// Intentionally NOT public -class ExampleBeanOverrideProcessor implements BeanOverrideProcessor { - - @Override - public OverrideMetadata createMetadata(Annotation overrideAnnotation, Class testClass, Field field) { - if (!(overrideAnnotation instanceof ExampleBeanOverrideAnnotation annotation)) { - throw new IllegalStateException("unexpected annotation"); - } - if (annotation.value().startsWith(ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER)) { - return new TestOverrideMetadata(annotation.value()); - } - return new TestOverrideMetadata(field, annotation, ResolvableType.forField(field, testClass)); - } - -} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestBeanOverrideMetaAnnotation.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestBeanOverrideMetaAnnotation.java deleted file mode 100644 index 49410ca315..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestBeanOverrideMetaAnnotation.java +++ /dev/null @@ -1,27 +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.example; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.FIELD, ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@ExampleBeanOverrideAnnotation("foo") -public @interface TestBeanOverrideMetaAnnotation { } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java deleted file mode 100644 index 5395937a5d..0000000000 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/TestOverrideMetadata.java +++ /dev/null @@ -1,144 +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.example; - - -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; - -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; -import org.springframework.test.context.bean.override.BeanOverrideStrategy; -import org.springframework.test.context.bean.override.OverrideMetadata; -import org.springframework.util.StringUtils; - -import static org.springframework.test.context.bean.override.example.ExampleBeanOverrideAnnotation.DEFAULT_VALUE; - -class TestOverrideMetadata extends OverrideMetadata { - - @Nullable - private final Method method; - - private final String methodName; - - @Nullable - private static Method findMethod(AnnotatedElement element, String methodName) { - if (DEFAULT_VALUE.equals(methodName)) { - return null; - } - if (element instanceof Field f) { - for (Method m : f.getDeclaringClass().getDeclaredMethods()) { - if (!Modifier.isStatic(m.getModifiers())) { - continue; - } - if (m.getName().equals(methodName)) { - return m; - } - } - throw new IllegalStateException("Expected a static method named <" + methodName + "> alongside annotated field <" + f.getName() + ">"); - } - if (element instanceof Method m) { - if (m.getName().equals(methodName) && Modifier.isStatic(m.getModifiers())) { - return m; - } - throw new IllegalStateException("Expected the annotated method to be static and named <" + methodName + ">"); - } - if (element instanceof Class c) { - for (Method m : c.getDeclaredMethods()) { - if (!Modifier.isStatic(m.getModifiers())) { - continue; - } - if (m.getName().equals(methodName)) { - return m; - } - } - throw new IllegalStateException("Expected a static method named <" + methodName + "> on annotated class <" + c.getSimpleName() + ">"); - } - throw new IllegalStateException("Expected the annotated element to be a Field, Method or Class"); - } - - public TestOverrideMetadata(Field field, ExampleBeanOverrideAnnotation overrideAnnotation, ResolvableType typeToOverride) { - super(field, typeToOverride, overrideAnnotation.beanName(), overrideAnnotation.createIfMissing() ? - BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION: BeanOverrideStrategy.REPLACE_DEFINITION); - this.method = findMethod(field, overrideAnnotation.value()); - this.methodName = overrideAnnotation.value(); - } - - //Used to trigger duplicate detection in parser test - TestOverrideMetadata(String duplicateTrigger) { - super(null, null, duplicateTrigger, null); - this.method = null; - this.methodName = duplicateTrigger; - } - - @Override - public String getBeanName() { - String name = super.getBeanName(); - if (StringUtils.hasText(name)) { - return name; - } - return getField().getName(); - } - - String getAnnotationMethodName() { - return this.methodName; - } - - @Override - protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) { - if (this.method == null) { - return DEFAULT_VALUE; - } - try { - this.method.setAccessible(true); - return this.method.invoke(null); - } - catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - @Override - public boolean equals(Object obj) { - if (this.method == null) { - return obj instanceof TestOverrideMetadata tem && - tem.getBeanName() != null && - tem.getBeanName().startsWith(ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER); - } - return super.equals(obj); - } - - @Override - public int hashCode() { - if (this.method == null) { - return ExampleBeanOverrideAnnotation.DUPLICATE_TRIGGER.hashCode(); - } - return super.hashCode(); - } - - @Override - public String toString() { - if (this.method == null) { - return "{" + this.getBeanName() + "}"; - } - return this.methodName; - } -}