Skip to content

Commit 480051a

Browse files
committed
Introduce fallback flag and annotation (as companion to primary)
Closes gh-26241
1 parent c077805 commit 480051a

File tree

7 files changed

+197
-3
lines changed

7 files changed

+197
-3
lines changed

Diff for: spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -178,6 +178,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
178178
* Set whether this bean is a primary autowire candidate.
179179
* <p>If this value is {@code true} for exactly one bean among multiple
180180
* matching candidates, it will serve as a tie-breaker.
181+
* @see #setFallback
181182
*/
182183
void setPrimary(boolean primary);
183184

@@ -186,6 +187,21 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
186187
*/
187188
boolean isPrimary();
188189

190+
/**
191+
* Set whether this bean is a fallback autowire candidate.
192+
* <p>If this value is {@code true} for all beans but one among multiple
193+
* matching candidates, the remaining bean will be selected.
194+
* @since 6.2
195+
* @see #setPrimary
196+
*/
197+
void setFallback(boolean fallback);
198+
199+
/**
200+
* Return whether this bean is a fallback autowire candidate.
201+
* @since 6.2
202+
*/
203+
boolean isFallback();
204+
189205
/**
190206
* Specify the factory bean to use, if any.
191207
* This the name of the bean to call the specified factory method on.

Diff for: spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java

+24
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess
189189

190190
private boolean primary = false;
191191

192+
private boolean fallback = false;
193+
192194
private final Map<String, AutowireCandidateQualifier> qualifiers = new LinkedHashMap<>();
193195

194196
@Nullable
@@ -288,6 +290,7 @@ protected AbstractBeanDefinition(BeanDefinition original) {
288290
setAutowireCandidate(originalAbd.isAutowireCandidate());
289291
setDefaultCandidate(originalAbd.isDefaultCandidate());
290292
setPrimary(originalAbd.isPrimary());
293+
setFallback(originalAbd.isFallback());
291294
copyQualifiersFrom(originalAbd);
292295
setInstanceSupplier(originalAbd.getInstanceSupplier());
293296
setNonPublicAccessAllowed(originalAbd.isNonPublicAccessAllowed());
@@ -365,6 +368,7 @@ public void overrideFrom(BeanDefinition other) {
365368
setAutowireCandidate(otherAbd.isAutowireCandidate());
366369
setDefaultCandidate(otherAbd.isDefaultCandidate());
367370
setPrimary(otherAbd.isPrimary());
371+
setFallback(otherAbd.isFallback());
368372
copyQualifiersFrom(otherAbd);
369373
setInstanceSupplier(otherAbd.getInstanceSupplier());
370374
setNonPublicAccessAllowed(otherAbd.isNonPublicAccessAllowed());
@@ -742,6 +746,7 @@ public boolean isDefaultCandidate() {
742746
* Set whether this bean is a primary autowire candidate.
743747
* <p>Default is {@code false}. If this value is {@code true} for exactly one
744748
* bean among multiple matching candidates, it will serve as a tie-breaker.
749+
* @see #setFallback
745750
*/
746751
@Override
747752
public void setPrimary(boolean primary) {
@@ -756,6 +761,25 @@ public boolean isPrimary() {
756761
return this.primary;
757762
}
758763

764+
/**
765+
* Set whether this bean is a fallback autowire candidate.
766+
* <p>Default is {@code false}. If this value is {@code true} for all beans but
767+
* one among multiple matching candidates, the remaining bean will be selected.
768+
* @since 6.2
769+
* @see #setPrimary
770+
*/
771+
public void setFallback(boolean fallback) {
772+
this.fallback = fallback;
773+
}
774+
775+
/**
776+
* Return whether this bean is a fallback autowire candidate.
777+
* @since 6.2
778+
*/
779+
public boolean isFallback() {
780+
return this.fallback;
781+
}
782+
759783
/**
760784
* Register a qualifier to be used for autowire candidate resolution,
761785
* keyed by the qualifier's type name.

Diff for: spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java

+31
Original file line numberDiff line numberDiff line change
@@ -1796,6 +1796,7 @@ protected String determineAutowireCandidate(Map<String, Object> candidates, Depe
17961796
@Nullable
17971797
protected String determinePrimaryCandidate(Map<String, Object> candidates, Class<?> requiredType) {
17981798
String primaryBeanName = null;
1799+
// First pass: identify unique primary candidate
17991800
for (Map.Entry<String, Object> entry : candidates.entrySet()) {
18001801
String candidateBeanName = entry.getKey();
18011802
Object beanInstance = entry.getValue();
@@ -1816,6 +1817,19 @@ else if (candidateLocal) {
18161817
}
18171818
}
18181819
}
1820+
// Second pass: identify unique non-fallback candidate
1821+
if (primaryBeanName == null) {
1822+
for (Map.Entry<String, Object> entry : candidates.entrySet()) {
1823+
String candidateBeanName = entry.getKey();
1824+
Object beanInstance = entry.getValue();
1825+
if (!isFallback(candidateBeanName, beanInstance)) {
1826+
if (primaryBeanName != null) {
1827+
return null;
1828+
}
1829+
primaryBeanName = candidateBeanName;
1830+
}
1831+
}
1832+
}
18191833
return primaryBeanName;
18201834
}
18211835

@@ -1878,6 +1892,23 @@ protected boolean isPrimary(String beanName, Object beanInstance) {
18781892
parent.isPrimary(transformedBeanName, beanInstance));
18791893
}
18801894

1895+
/**
1896+
* Return whether the bean definition for the given bean name has been
1897+
* marked as a fallback bean.
1898+
* @param beanName the name of the bean
1899+
* @param beanInstance the corresponding bean instance (can be {@code null})
1900+
* @return whether the given bean qualifies as fallback
1901+
* @since 6.2
1902+
*/
1903+
protected boolean isFallback(String beanName, Object beanInstance) {
1904+
String transformedBeanName = transformedBeanName(beanName);
1905+
if (containsBeanDefinition(transformedBeanName)) {
1906+
return getMergedLocalBeanDefinition(transformedBeanName).isFallback();
1907+
}
1908+
return (getParentBeanFactory() instanceof DefaultListableBeanFactory parent &&
1909+
parent.isFallback(transformedBeanName, beanInstance));
1910+
}
1911+
18811912
/**
18821913
* Return the priority assigned for the given bean instance by
18831914
* the {@code jakarta.annotation.Priority} annotation.

Diff for: spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -246,6 +246,9 @@ else if (abd.getMetadata() != metadata) {
246246
if (metadata.isAnnotated(Primary.class.getName())) {
247247
abd.setPrimary(true);
248248
}
249+
if (metadata.isAnnotated(Fallback.class.getName())) {
250+
abd.setFallback(true);
251+
}
249252
AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class);
250253
if (dependsOn != null) {
251254
abd.setDependsOn(dependsOn.getStringArray("value"));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.context.annotation;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
/**
26+
* Indicates that a bean qualifies as a fallback autowire candidate.
27+
* This is a companion and alternative to the {@link Primary} annotation.
28+
*
29+
* <p>If all beans but one among multiple matching candidates are marked
30+
* as a fallback, the remaining bean will be selected.
31+
*
32+
* @author Juergen Hoeller
33+
* @since 6.2
34+
* @see Primary
35+
* @see Lazy
36+
* @see Bean
37+
* @see org.springframework.beans.factory.config.BeanDefinition#setFallback
38+
*/
39+
@Target({ElementType.TYPE, ElementType.METHOD})
40+
@Retention(RetentionPolicy.RUNTIME)
41+
@Documented
42+
public @interface Fallback {
43+
44+
}

Diff for: spring-context/src/main/java/org/springframework/context/annotation/Primary.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -77,10 +77,12 @@
7777
* @author Chris Beams
7878
* @author Juergen Hoeller
7979
* @since 3.0
80+
* @see Fallback
8081
* @see Lazy
8182
* @see Bean
8283
* @see ComponentScan
8384
* @see org.springframework.stereotype.Component
85+
* @see org.springframework.beans.factory.config.BeanDefinition#setPrimary
8486
*/
8587
@Target({ElementType.TYPE, ElementType.METHOD})
8688
@Retention(RetentionPolicy.RUNTIME)

Diff for: spring-context/src/test/java/org/springframework/context/annotation/configuration/BeanMethodQualificationTests.java

+74
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3131
import org.springframework.context.annotation.Bean;
3232
import org.springframework.context.annotation.Configuration;
33+
import org.springframework.context.annotation.Fallback;
3334
import org.springframework.context.annotation.Lazy;
35+
import org.springframework.context.annotation.Primary;
3436
import org.springframework.context.annotation.Scope;
3537
import org.springframework.context.annotation.ScopedProxyMode;
3638
import org.springframework.core.annotation.AliasFor;
@@ -80,6 +82,26 @@ void scopedProxy() {
8082
ctx.close();
8183
}
8284

85+
@Test
86+
void primary() {
87+
AnnotationConfigApplicationContext ctx =
88+
new AnnotationConfigApplicationContext(PrimaryConfig.class, StandardPojo.class);
89+
StandardPojo pojo = ctx.getBean(StandardPojo.class);
90+
assertThat(pojo.testBean.getName()).isEqualTo("interesting");
91+
assertThat(pojo.testBean2.getName()).isEqualTo("boring");
92+
ctx.close();
93+
}
94+
95+
@Test
96+
void fallback() {
97+
AnnotationConfigApplicationContext ctx =
98+
new AnnotationConfigApplicationContext(FallbackConfig.class, StandardPojo.class);
99+
StandardPojo pojo = ctx.getBean(StandardPojo.class);
100+
assertThat(pojo.testBean.getName()).isEqualTo("interesting");
101+
assertThat(pojo.testBean2.getName()).isEqualTo("boring");
102+
ctx.close();
103+
}
104+
83105
@Test
84106
void customWithLazyResolution() {
85107
AnnotationConfigApplicationContext ctx =
@@ -201,6 +223,58 @@ public TestBean testBean2(TestBean testBean1) {
201223
}
202224
}
203225

226+
@Configuration
227+
static class PrimaryConfig {
228+
229+
@Bean @Qualifier("interesting") @Primary
230+
public static TestBean testBean1() {
231+
return new TestBean("interesting");
232+
}
233+
234+
@Bean @Qualifier("interesting")
235+
public static TestBean testBean1x() {
236+
return new TestBean("interesting");
237+
}
238+
239+
@Bean @Boring @Primary
240+
public TestBean testBean2(TestBean testBean1) {
241+
TestBean tb = new TestBean("boring");
242+
tb.setSpouse(testBean1);
243+
return tb;
244+
}
245+
246+
@Bean @Boring
247+
public TestBean testBean2x() {
248+
return new TestBean("boring");
249+
}
250+
}
251+
252+
@Configuration
253+
static class FallbackConfig {
254+
255+
@Bean @Qualifier("interesting")
256+
public static TestBean testBean1() {
257+
return new TestBean("interesting");
258+
}
259+
260+
@Bean @Qualifier("interesting") @Fallback
261+
public static TestBean testBean1x() {
262+
return new TestBean("interesting");
263+
}
264+
265+
@Bean @Boring
266+
public TestBean testBean2(TestBean testBean1) {
267+
TestBean tb = new TestBean("boring");
268+
tb.setSpouse(testBean1);
269+
return tb;
270+
}
271+
272+
@Bean @Boring @Fallback
273+
public TestBean testBean2x() {
274+
return new TestBean("boring");
275+
}
276+
}
277+
204278
@Component @Lazy
205279
static class StandardPojo {
206280

0 commit comments

Comments
 (0)