From a8fb16b47cc64d77ffdd83275aa7038a92cef767 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 20 Feb 2024 12:00:47 +0100 Subject: [PATCH] Introduce defaultCandidate flag (for plain type vs. qualified match) Closes gh-26528 --- ...erAnnotationAutowireCandidateResolver.java | 5 ++- .../support/AbstractBeanDefinition.java | 41 ++++++++++++++++--- .../context/annotation/Bean.java | 18 +++++++- ...onfigurationClassBeanDefinitionReader.java | 7 +++- .../BeanMethodQualificationTests.java | 15 ++++--- 5 files changed, 72 insertions(+), 14 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java index 940ce0207d..4b6605984e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java @@ -167,12 +167,14 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa if (ObjectUtils.isEmpty(annotationsToSearch)) { return true; } + boolean qualifierFound = false; SimpleTypeConverter typeConverter = new SimpleTypeConverter(); for (Annotation annotation : annotationsToSearch) { Class type = annotation.annotationType(); boolean checkMeta = true; boolean fallbackToMeta = false; if (isQualifier(type)) { + qualifierFound = true; if (!checkQualifier(bdHolder, annotation, typeConverter)) { fallbackToMeta = true; } @@ -185,6 +187,7 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa for (Annotation metaAnn : type.getAnnotations()) { Class metaType = metaAnn.annotationType(); if (isQualifier(metaType)) { + qualifierFound = true; foundMeta = true; // Only accept fallback match if @Qualifier annotation has a value... // Otherwise, it is just a marker for a custom qualifier annotation. @@ -199,7 +202,7 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa } } } - return true; + return (qualifierFound || ((RootBeanDefinition) bdHolder.getBeanDefinition()).isDefaultCandidate()); } /** 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 ebdadd211b..6222cca3c6 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 @@ -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. @@ -185,6 +185,8 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess private boolean autowireCandidate = true; + private boolean defaultCandidate = true; + private boolean primary = false; private final Map qualifiers = new LinkedHashMap<>(); @@ -284,6 +286,7 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess setDependencyCheck(originalAbd.getDependencyCheck()); setDependsOn(originalAbd.getDependsOn()); setAutowireCandidate(originalAbd.isAutowireCandidate()); + setDefaultCandidate(originalAbd.isDefaultCandidate()); setPrimary(originalAbd.isPrimary()); copyQualifiersFrom(originalAbd); setInstanceSupplier(originalAbd.getInstanceSupplier()); @@ -360,6 +363,7 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess setDependencyCheck(otherAbd.getDependencyCheck()); setDependsOn(otherAbd.getDependsOn()); setAutowireCandidate(otherAbd.isAutowireCandidate()); + setDefaultCandidate(otherAbd.isDefaultCandidate()); setPrimary(otherAbd.isPrimary()); copyQualifiersFrom(otherAbd); setInstanceSupplier(otherAbd.getInstanceSupplier()); @@ -686,7 +690,10 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess } /** - * Set whether this bean is a candidate for getting autowired into some other bean. + * Set whether this bean is a candidate for getting autowired into some other + * bean at all. + *

Default is {@code true}, allowing injection by type at any injection point. + * Switch this to {@code false} in order to disable autowiring by type for this bean. *

Note that this flag is designed to only affect type-based autowiring. * It does not affect explicit references by name, which will get resolved even * if the specified bean is not marked as an autowire candidate. As a consequence, @@ -700,17 +707,41 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess } /** - * Return whether this bean is a candidate for getting autowired into some other bean. + * Return whether this bean is a candidate for getting autowired into some other + * bean at all. */ @Override public boolean isAutowireCandidate() { return this.autowireCandidate; } + /** + * Set whether this bean is a candidate for getting autowired into some other + * bean based on the plain type, without any further indications such as a + * qualifier match. + *

Default is {@code true}, allowing injection by type at any injection point. + * Switch this to {@code false} in order to restrict injection by default, + * effectively enforcing an additional indication such as a qualifier match. + * @since 6.2 + */ + public void setDefaultCandidate(boolean defaultCandidate) { + this.defaultCandidate = defaultCandidate; + } + + /** + * Return whether this bean is a candidate for getting autowired into some other + * bean based on the plain type, without any further indications such as a + * qualifier match? + * @since 6.2 + */ + public boolean isDefaultCandidate() { + return this.defaultCandidate; + } + /** * 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. + *

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. */ @Override public void setPrimary(boolean primary) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Bean.java b/spring-context/src/main/java/org/springframework/context/annotation/Bean.java index bbc64c4b35..a921a4bd2b 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Bean.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Bean.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. @@ -239,13 +239,27 @@ public @interface Bean { String[] name() default {}; /** - * Is this bean a candidate for getting autowired into some other bean? + * Is this bean a candidate for getting autowired into some other bean at all? *

Default is {@code true}; set this to {@code false} for internal delegates * that are not meant to get in the way of beans of the same type in other places. * @since 5.1 + * @see #defaultCandidate() */ boolean autowireCandidate() default true; + /** + * Is this bean a candidate for getting autowired into some other bean based on + * the plain type, without any further indications such as a qualifier match? + *

Default is {@code true}; set this to {@code false} for restricted delegates + * that are supposed to be injectable in certain areas but are not meant to get + * in the way of beans of the same type in other places. + *

This is a variation of {@link #autowireCandidate()} which does not disable + * injection in general, just enforces an additional indication such as a qualifier. + * @since 6.2 + * @see #autowireCandidate() + */ + boolean defaultCandidate() default true; + /** * The optional name of a method to call on the bean instance during initialization. * Not commonly used, given that the method may be called programmatically directly diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index 55d1db9fab..e29443838e 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.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. @@ -241,6 +241,11 @@ class ConfigurationClassBeanDefinitionReader { beanDef.setAutowireCandidate(false); } + boolean defaultCandidate = bean.getBoolean("defaultCandidate"); + if (!defaultCandidate) { + beanDef.setDefaultCandidate(false); + } + String initMethodName = bean.getString("initMethod"); if (StringUtils.hasText(initMethodName)) { beanDef.setInitMethodName(initMethodName); 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 a8676e55c8..a99227b647 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 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. @@ -89,6 +89,7 @@ class BeanMethodQualificationTests { 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"); @@ -132,7 +133,9 @@ class BeanMethodQualificationTests { 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(); } @@ -219,7 +222,7 @@ class BeanMethodQualificationTests { return new TestBean("interesting"); } - @Bean @Qualifier("boring") @Lazy + @Bean(defaultCandidate=false) @Qualifier("boring") @Lazy public TestBean testBean2(@Lazy TestBean testBean1) { TestBean tb = new TestBean("boring"); tb.setSpouse(testBean1); @@ -235,7 +238,7 @@ class BeanMethodQualificationTests { return new TestBean("interesting"); } - @Bean @Qualifier("boring") + @Bean(defaultCandidate=false) @Qualifier("boring") public TestBean testBean2(@Lazy TestBean testBean1) { TestBean tb = new TestBean("boring"); tb.setSpouse(testBean1); @@ -246,17 +249,19 @@ class BeanMethodQualificationTests { @InterestingPojo static class CustomPojo { + @Autowired(required=false) TestBean plainBean; + @InterestingNeed TestBean testBean; @InterestingNeedWithRequiredOverride(required=false) NestedTestBean nestedTestBean; } - @Bean @Lazy @Qualifier("interesting") + @Bean(defaultCandidate=false) @Lazy @Qualifier("interesting") @Retention(RetentionPolicy.RUNTIME) @interface InterestingBean { } - @Bean @Lazy @Qualifier("interesting") + @Bean(defaultCandidate=false) @Lazy @Qualifier("interesting") @Retention(RetentionPolicy.RUNTIME) @interface InterestingBeanWithName {