diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java index 5be39a0eaa1..aee39bc138e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * 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. @@ -178,6 +178,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { * Set whether this bean is a primary autowire candidate. *

If this value is {@code true} for exactly one bean among multiple * matching candidates, it will serve as a tie-breaker. + * @see #setFallback */ void setPrimary(boolean primary); @@ -186,6 +187,21 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { */ boolean isPrimary(); + /** + * Set whether this bean is a fallback autowire candidate. + *

If this value is {@code true} for all beans but one among multiple + * matching candidates, the remaining bean will be selected. + * @since 6.2 + * @see #setPrimary + */ + void setFallback(boolean fallback); + + /** + * Return whether this bean is a fallback autowire candidate. + * @since 6.2 + */ + boolean isFallback(); + /** * Specify the factory bean to use, if any. * This the name of the bean to call the specified factory method on. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java index 6222cca3c69..85c7e5cadab 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -189,6 +189,8 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess private boolean primary = false; + private boolean fallback = false; + private final Map qualifiers = new LinkedHashMap<>(); @Nullable @@ -288,6 +290,7 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess setAutowireCandidate(originalAbd.isAutowireCandidate()); setDefaultCandidate(originalAbd.isDefaultCandidate()); setPrimary(originalAbd.isPrimary()); + setFallback(originalAbd.isFallback()); copyQualifiersFrom(originalAbd); setInstanceSupplier(originalAbd.getInstanceSupplier()); setNonPublicAccessAllowed(originalAbd.isNonPublicAccessAllowed()); @@ -365,6 +368,7 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess setAutowireCandidate(otherAbd.isAutowireCandidate()); setDefaultCandidate(otherAbd.isDefaultCandidate()); setPrimary(otherAbd.isPrimary()); + setFallback(otherAbd.isFallback()); copyQualifiersFrom(otherAbd); setInstanceSupplier(otherAbd.getInstanceSupplier()); setNonPublicAccessAllowed(otherAbd.isNonPublicAccessAllowed()); @@ -742,6 +746,7 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess * Set whether this bean is a primary autowire candidate. *

Default is {@code false}. If this value is {@code true} for exactly one * bean among multiple matching candidates, it will serve as a tie-breaker. + * @see #setFallback */ @Override public void setPrimary(boolean primary) { @@ -756,6 +761,25 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess return this.primary; } + /** + * Set whether this bean is a fallback autowire candidate. + *

Default is {@code false}. If this value is {@code true} for all beans but + * one among multiple matching candidates, the remaining bean will be selected. + * @since 6.2 + * @see #setPrimary + */ + public void setFallback(boolean fallback) { + this.fallback = fallback; + } + + /** + * Return whether this bean is a fallback autowire candidate. + * @since 6.2 + */ + public boolean isFallback() { + return this.fallback; + } + /** * Register a qualifier to be used for autowire candidate resolution, * keyed by the qualifier's type name. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index 0bcd895f7f6..67c9142e5d7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1796,6 +1796,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto @Nullable protected String determinePrimaryCandidate(Map candidates, Class requiredType) { String primaryBeanName = null; + // First pass: identify unique primary candidate for (Map.Entry entry : candidates.entrySet()) { String candidateBeanName = entry.getKey(); Object beanInstance = entry.getValue(); @@ -1816,6 +1817,19 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto } } } + // Second pass: identify unique non-fallback candidate + if (primaryBeanName == null) { + for (Map.Entry entry : candidates.entrySet()) { + String candidateBeanName = entry.getKey(); + Object beanInstance = entry.getValue(); + if (!isFallback(candidateBeanName, beanInstance)) { + if (primaryBeanName != null) { + return null; + } + primaryBeanName = candidateBeanName; + } + } + } return primaryBeanName; } @@ -1878,6 +1892,23 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto parent.isPrimary(transformedBeanName, beanInstance)); } + /** + * Return whether the bean definition for the given bean name has been + * marked as a fallback bean. + * @param beanName the name of the bean + * @param beanInstance the corresponding bean instance (can be {@code null}) + * @return whether the given bean qualifies as fallback + * @since 6.2 + */ + protected boolean isFallback(String beanName, Object beanInstance) { + String transformedBeanName = transformedBeanName(beanName); + if (containsBeanDefinition(transformedBeanName)) { + return getMergedLocalBeanDefinition(transformedBeanName).isFallback(); + } + return (getParentBeanFactory() instanceof DefaultListableBeanFactory parent && + parent.isFallback(transformedBeanName, beanInstance)); + } + /** * Return the priority assigned for the given bean instance by * the {@code jakarta.annotation.Priority} annotation. diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java index 903984ab0fd..b9d4a79e19f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -246,6 +246,9 @@ public abstract class AnnotationConfigUtils { if (metadata.isAnnotated(Primary.class.getName())) { abd.setPrimary(true); } + if (metadata.isAnnotated(Fallback.class.getName())) { + abd.setFallback(true); + } AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class); if (dependsOn != null) { abd.setDependsOn(dependsOn.getStringArray("value")); 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 new file mode 100644 index 00000000000..9ff6d16d738 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Fallback.java @@ -0,0 +1,44 @@ +/* + * 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.context.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; + +/** + * Indicates that a bean qualifies as a fallback autowire candidate. + * This is a companion and alternative to the {@link Primary} annotation. + * + *

If all beans but one among multiple matching candidates are marked + * as a fallback, the remaining bean will be selected. + * + * @author Juergen Hoeller + * @since 6.2 + * @see Primary + * @see Lazy + * @see Bean + * @see org.springframework.beans.factory.config.BeanDefinition#setFallback + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Fallback { + +} 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 3832996e448..5ff345fa7a6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * 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. @@ -77,10 +77,12 @@ import java.lang.annotation.Target; * @author Chris Beams * @author Juergen Hoeller * @since 3.0 + * @see Fallback * @see Lazy * @see Bean * @see ComponentScan * @see org.springframework.stereotype.Component + * @see org.springframework.beans.factory.config.BeanDefinition#setPrimary */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) 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 a99227b6476..cd969c928d5 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 @@ -30,7 +30,9 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Fallback; import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.core.annotation.AliasFor; @@ -80,6 +82,26 @@ class BeanMethodQualificationTests { ctx.close(); } + @Test + void primary() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(PrimaryConfig.class, StandardPojo.class); + StandardPojo pojo = ctx.getBean(StandardPojo.class); + assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + ctx.close(); + } + + @Test + void fallback() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(FallbackConfig.class, StandardPojo.class); + StandardPojo pojo = ctx.getBean(StandardPojo.class); + assertThat(pojo.testBean.getName()).isEqualTo("interesting"); + assertThat(pojo.testBean2.getName()).isEqualTo("boring"); + ctx.close(); + } + @Test void customWithLazyResolution() { AnnotationConfigApplicationContext ctx = @@ -201,6 +223,58 @@ class BeanMethodQualificationTests { } } + @Configuration + static class PrimaryConfig { + + @Bean @Qualifier("interesting") @Primary + public static TestBean testBean1() { + return new TestBean("interesting"); + } + + @Bean @Qualifier("interesting") + public static TestBean testBean1x() { + return new TestBean("interesting"); + } + + @Bean @Boring @Primary + public TestBean testBean2(TestBean testBean1) { + TestBean tb = new TestBean("boring"); + tb.setSpouse(testBean1); + return tb; + } + + @Bean @Boring + public TestBean testBean2x() { + return new TestBean("boring"); + } + } + + @Configuration + static class FallbackConfig { + + @Bean @Qualifier("interesting") + public static TestBean testBean1() { + return new TestBean("interesting"); + } + + @Bean @Qualifier("interesting") @Fallback + public static TestBean testBean1x() { + return new TestBean("interesting"); + } + + @Bean @Boring + public TestBean testBean2(TestBean testBean1) { + TestBean tb = new TestBean("boring"); + tb.setSpouse(testBean1); + return tb; + } + + @Bean @Boring @Fallback + public TestBean testBean2x() { + return new TestBean("boring"); + } + } + @Component @Lazy static class StandardPojo {