Skip to content

Commit

Permalink
Introduce fallback flag and annotation (as companion to primary)
Browse files Browse the repository at this point in the history
Closes gh-26241
  • Loading branch information
jhoeller committed Feb 20, 2024
1 parent c077805 commit 480051a
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -178,6 +178,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
* Set whether this bean is a primary autowire candidate.
* <p>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);

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

/**
* Set whether this bean is a fallback autowire candidate.
* <p>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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess

private boolean primary = false;

private boolean fallback = false;

private final Map<String, AutowireCandidateQualifier> qualifiers = new LinkedHashMap<>();

@Nullable
Expand Down Expand Up @@ -288,6 +290,7 @@ protected AbstractBeanDefinition(BeanDefinition original) {
setAutowireCandidate(originalAbd.isAutowireCandidate());
setDefaultCandidate(originalAbd.isDefaultCandidate());
setPrimary(originalAbd.isPrimary());
setFallback(originalAbd.isFallback());
copyQualifiersFrom(originalAbd);
setInstanceSupplier(originalAbd.getInstanceSupplier());
setNonPublicAccessAllowed(originalAbd.isNonPublicAccessAllowed());
Expand Down Expand Up @@ -365,6 +368,7 @@ public void overrideFrom(BeanDefinition other) {
setAutowireCandidate(otherAbd.isAutowireCandidate());
setDefaultCandidate(otherAbd.isDefaultCandidate());
setPrimary(otherAbd.isPrimary());
setFallback(otherAbd.isFallback());
copyQualifiersFrom(otherAbd);
setInstanceSupplier(otherAbd.getInstanceSupplier());
setNonPublicAccessAllowed(otherAbd.isNonPublicAccessAllowed());
Expand Down Expand Up @@ -742,6 +746,7 @@ public boolean isDefaultCandidate() {
* Set whether this bean is a primary autowire candidate.
* <p>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) {
Expand All @@ -756,6 +761,25 @@ public boolean isPrimary() {
return this.primary;
}

/**
* Set whether this bean is a fallback autowire candidate.
* <p>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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1796,6 +1796,7 @@ protected String determineAutowireCandidate(Map<String, Object> candidates, Depe
@Nullable
protected String determinePrimaryCandidate(Map<String, Object> candidates, Class<?> requiredType) {
String primaryBeanName = null;
// First pass: identify unique primary candidate
for (Map.Entry<String, Object> entry : candidates.entrySet()) {
String candidateBeanName = entry.getKey();
Object beanInstance = entry.getValue();
Expand All @@ -1816,6 +1817,19 @@ else if (candidateLocal) {
}
}
}
// Second pass: identify unique non-fallback candidate
if (primaryBeanName == null) {
for (Map.Entry<String, Object> entry : candidates.entrySet()) {
String candidateBeanName = entry.getKey();
Object beanInstance = entry.getValue();
if (!isFallback(candidateBeanName, beanInstance)) {
if (primaryBeanName != null) {
return null;
}
primaryBeanName = candidateBeanName;
}
}
}
return primaryBeanName;
}

Expand Down Expand Up @@ -1878,6 +1892,23 @@ protected boolean isPrimary(String beanName, Object beanInstance) {
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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -246,6 +246,9 @@ else if (abd.getMetadata() != metadata) {
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"));
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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 {

}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -77,10 +77,12 @@
* @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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
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;
Expand Down Expand Up @@ -80,6 +82,26 @@ void scopedProxy() {
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 =
Expand Down Expand Up @@ -201,6 +223,58 @@ public TestBean testBean2(TestBean testBean1) {
}
}

@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 {

Expand Down

0 comments on commit 480051a

Please sign in to comment.