diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java b/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java index 9ff6d16d73..32544002ce 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java @@ -29,6 +29,11 @@ import java.lang.annotation.Target; *

If all beans but one among multiple matching candidates are marked * as a fallback, the remaining bean will be selected. * + *

Just like primary beans, fallback beans only have an effect when + * finding multiple candidates for single injection points. + * All type-matching beans are included when autowiring arrays, + * collections, maps, or ObjectProvider streams. + * * @author Juergen Hoeller * @since 6.2 * @see Primary diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Primary.java b/spring-context/src/main/java/org/springframework/context/annotation/Primary.java index 5ff345fa7a..92c26789f4 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Primary.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Primary.java @@ -27,6 +27,10 @@ import java.lang.annotation.Target; * are qualified to autowire a single-valued dependency. If exactly one * 'primary' bean exists among the candidates, it will be the autowired value. * + *

Primary beans only have an effect when finding multiple candidates + * for single injection points. All type-matching beans are included when + * autowiring arrays, collections, maps, or ObjectProvider streams. + * *

This annotation is semantically equivalent to the {@code } element's * {@code primary} attribute in Spring XML. * diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java index d9ed7e70d0..cb42e8fcb6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java @@ -18,9 +18,13 @@ package org.springframework.context.annotation.configuration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; @@ -38,6 +42,7 @@ import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -47,6 +52,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Chris Beams * @author Juergen Hoeller + * @author Yanming Zhou */ class BeanMethodQualificationTests { @@ -54,10 +60,12 @@ class BeanMethodQualificationTests { void standard() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(StandardConfig.class, StandardPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); StandardPojo pojo = ctx.getBean(StandardPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + ctx.close(); } @@ -65,10 +73,12 @@ class BeanMethodQualificationTests { void scoped() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ScopedConfig.class, StandardPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); StandardPojo pojo = ctx.getBean(StandardPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + ctx.close(); } @@ -76,36 +86,37 @@ class BeanMethodQualificationTests { void scopedProxy() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ScopedProxyConfig.class, StandardPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isTrue(); // a shared scoped proxy StandardPojo pojo = ctx.getBean(StandardPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + ctx.close(); } - @Test - void primary() { + @SuppressWarnings("unchecked") + @ParameterizedTest + @ValueSource(classes = {PrimaryConfig.class, FallbackConfig.class}) + void primaryVersusFallback(Class configClass) { AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(PrimaryConfig.class, StandardPojo.class, ConstructorPojo.class); - StandardPojo pojo = ctx.getBean(StandardPojo.class); - assertThat(pojo.testBean.getName()).isEqualTo("interesting"); - assertThat(pojo.testBean2.getName()).isEqualTo("boring"); - ConstructorPojo pojo2 = ctx.getBean(ConstructorPojo.class); - assertThat(pojo2.testBean.getName()).isEqualTo("interesting"); - assertThat(pojo2.testBean2.getName()).isEqualTo("boring"); - ctx.close(); - } + new AnnotationConfigApplicationContext(configClass, StandardPojo.class, ConstructorPojo.class); - @Test - void fallback() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(FallbackConfig.class, StandardPojo.class, ConstructorPojo.class); StandardPojo pojo = ctx.getBean(StandardPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + assertThat(pojo.testBean2.getSpouse().getName()).isEqualTo("interesting"); + assertThat((List) pojo.testBean2.getPets()).contains( + ctx.getBean("testBean1x"), ctx.getBean("testBean2x")); // array injection + assertThat((List) pojo.testBean2.getSomeList()).contains( + ctx.getBean("testBean1x"), ctx.getBean("testBean2x")); // list injection + assertThat((Map) pojo.testBean2.getSomeMap()).containsKeys( + "testBean1x", "testBean2x"); // map injection + ConstructorPojo pojo2 = ctx.getBean(ConstructorPojo.class); - assertThat(pojo2.testBean.getName()).isEqualTo("interesting"); - assertThat(pojo2.testBean2.getName()).isEqualTo("boring"); + assertThat(pojo2.testBean).isSameAs(pojo.testBean); + assertThat(pojo2.testBean2).isSameAs(pojo.testBean2); + ctx.close(); } @@ -113,16 +124,20 @@ class BeanMethodQualificationTests { void customWithLazyResolution() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(CustomConfig.class, CustomPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); assertThat(BeanFactoryAnnotationUtils.isQualifierMatch(value -> value.equals("boring"), "testBean2", ctx.getDefaultListableBeanFactory())).isTrue(); + CustomPojo pojo = ctx.getBean(CustomPojo.class); assertThat(pojo.plainBean).isNull(); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + TestBean testBean2 = BeanFactoryAnnotationUtils.qualifiedBeanOfType( ctx.getDefaultListableBeanFactory(), TestBean.class, "boring"); assertThat(testBean2.getName()).isEqualTo("boring"); + ctx.close(); } @@ -131,13 +146,16 @@ class BeanMethodQualificationTests { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(CustomConfig.class, CustomPojo.class); ctx.refresh(); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); ctx.getBean("testBean2"); assertThat(BeanFactoryAnnotationUtils.isQualifierMatch(value -> value.equals("boring"), "testBean2", ctx.getDefaultListableBeanFactory())).isTrue(); + CustomPojo pojo = ctx.getBean(CustomPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + ctx.close(); } @@ -149,10 +167,12 @@ class BeanMethodQualificationTests { customPojo.setLazyInit(true); ctx.registerBeanDefinition("customPojo", customPojo); ctx.refresh(); + assertThat(ctx.getBeanFactory().containsSingleton("testBean1")).isFalse(); assertThat(ctx.getBeanFactory().containsSingleton("testBean2")).isFalse(); CustomPojo pojo = ctx.getBean(CustomPojo.class); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + ctx.close(); } @@ -160,21 +180,29 @@ class BeanMethodQualificationTests { void customWithAttributeOverride() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(CustomConfigWithAttributeOverride.class, CustomPojo.class); + assertThat(ctx.getBeanFactory().containsSingleton("testBeanX")).isFalse(); CustomPojo pojo = ctx.getBean(CustomPojo.class); assertThat(pojo.plainBean).isNull(); assertThat(pojo.testBean.getName()).isEqualTo("interesting"); assertThat(pojo.nestedTestBean).isNull(); + ctx.close(); } @Test void beanNamesForAnnotation() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(StandardConfig.class); - assertThat(ctx.getBeanNamesForAnnotation(Configuration.class)).isEqualTo(new String[] {"beanMethodQualificationTests.StandardConfig"}); - assertThat(ctx.getBeanNamesForAnnotation(Scope.class)).isEqualTo(new String[] {}); - assertThat(ctx.getBeanNamesForAnnotation(Lazy.class)).isEqualTo(new String[] {"testBean1"}); - assertThat(ctx.getBeanNamesForAnnotation(Boring.class)).isEqualTo(new String[] {"beanMethodQualificationTests.StandardConfig", "testBean2"}); + + assertThat(ctx.getBeanNamesForAnnotation(Configuration.class)).isEqualTo( + new String[] {"beanMethodQualificationTests.StandardConfig"}); + assertThat(ctx.getBeanNamesForAnnotation(Scope.class)).isEqualTo( + new String[] {}); + assertThat(ctx.getBeanNamesForAnnotation(Lazy.class)).isEqualTo( + new String[] {"testBean1"}); + assertThat(ctx.getBeanNamesForAnnotation(Boring.class)).isEqualTo( + new String[] {"beanMethodQualificationTests.StandardConfig", "testBean2"}); + ctx.close(); } @@ -196,6 +224,7 @@ class BeanMethodQualificationTests { } } + @Configuration @Boring static class ScopedConfig { @@ -213,6 +242,7 @@ class BeanMethodQualificationTests { } } + @Configuration @Boring static class ScopedProxyConfig { @@ -230,6 +260,7 @@ class BeanMethodQualificationTests { } } + @Configuration static class PrimaryConfig { @@ -244,9 +275,14 @@ class BeanMethodQualificationTests { } @Bean @Boring @Primary - public TestBean testBean2(TestBean testBean1) { + public TestBean testBean2(TestBean testBean1, TestBean[] testBeanArray, + List testBeanList, Map testBeanMap) { + TestBean tb = new TestBean("boring"); tb.setSpouse(testBean1); + tb.setPets(CollectionUtils.arrayToList(testBeanArray)); + tb.setSomeList(testBeanList); + tb.setSomeMap(testBeanMap); return tb; } @@ -256,6 +292,7 @@ class BeanMethodQualificationTests { } } + @Configuration static class FallbackConfig { @@ -270,9 +307,14 @@ class BeanMethodQualificationTests { } @Bean @Boring - public TestBean testBean2(TestBean testBean1) { + public TestBean testBean2(TestBean testBean1, TestBean[] testBeanArray, + List testBeanList, Map testBeanMap) { + TestBean tb = new TestBean("boring"); tb.setSpouse(testBean1); + tb.setPets(CollectionUtils.arrayToList(testBeanArray)); + tb.setSomeList(testBeanList); + tb.setSomeMap(testBeanMap); return tb; } @@ -282,6 +324,7 @@ class BeanMethodQualificationTests { } } + @Component @Lazy static class StandardPojo { @@ -290,6 +333,7 @@ class BeanMethodQualificationTests { @Autowired @Boring TestBean testBean2; } + @Component @Lazy static class ConstructorPojo { @@ -303,10 +347,6 @@ class BeanMethodQualificationTests { } } - @Qualifier - @Retention(RetentionPolicy.RUNTIME) - public @interface Boring { - } @Configuration static class CustomConfig { @@ -324,6 +364,7 @@ class BeanMethodQualificationTests { } } + @Configuration static class CustomConfigWithAttributeOverride { @@ -340,6 +381,7 @@ class BeanMethodQualificationTests { } } + @InterestingPojo static class CustomPojo { @@ -354,6 +396,12 @@ class BeanMethodQualificationTests { } } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @interface Boring { + } + @Bean(defaultCandidate=false) @Lazy @Qualifier("interesting") @Retention(RetentionPolicy.RUNTIME) @interface InterestingBean {