diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java index 6e8af61bb13..286059addd2 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java @@ -255,6 +255,7 @@ public class AnnotatedBeanDefinitionReader { return; } + abd.setAttribute(ConfigurationClassUtils.CANDIDATE_ATTRIBUTE, Boolean.TRUE); abd.setInstanceSupplier(supplier); ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName()); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java index acfd6f6c3bb..4d557ae9ea4 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java @@ -56,6 +56,17 @@ public abstract class ConfigurationClassUtils { static final String CONFIGURATION_CLASS_LITE = "lite"; + /** + * When set to {@link Boolean#TRUE}, this attribute signals that the bean class + * for the given {@link BeanDefinition} should be considered as a candidate + * configuration class in 'lite' mode by default. + *
For example, a class registered directly with an {@code ApplicationContext} + * should always be considered a configuration class candidate. + * @since 6.0.10 + */ + static final String CANDIDATE_ATTRIBUTE = + Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "candidate"); + static final String CONFIGURATION_CLASS_ATTRIBUTE = Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "configurationClass"); @@ -136,7 +147,8 @@ public abstract class ConfigurationClassUtils { if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } - else if (config != null || isConfigurationCandidate(metadata)) { + else if (config != null || Boolean.TRUE.equals(beanDef.getAttribute(CANDIDATE_ATTRIBUTE)) || + isConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); } else { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BeanLiteModeTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BeanLiteModeTests.java new file mode 100644 index 00000000000..bcc82723aa2 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/BeanLiteModeTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2023 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 org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Bean @Bean} 'lite' mode features that are not covered + * elsewhere in the test suite. + * + * @author Sam Brannen + * @since 6.0.10 + * @see ConfigurationClassPostProcessorTests + */ +class BeanLiteModeTests { + + @Test + void beanMethodsAreFoundWhenInheritedAsInterfaceDefaultMethods() { + assertBeansAreFound(InterfaceDefaultMethodsConfig.class); + } + + @Test + void beanMethodsAreFoundWhenDeclaredLocally() { + assertBeansAreFound(BaseConfig.class); + } + + @Test + void beanMethodsAreFoundWhenDeclaredLocallyAndInSuperclass() { + assertBeansAreFound(OverridingConfig.class, "foo", "xyz"); + } + + @Test // gh-30449 + void beanMethodsAreFoundWhenDeclaredOnlyInSuperclass() { + assertBeansAreFound(ExtendedConfig.class, "foo", "xyz"); + } + + private static void assertBeansAreFound(Class> configClass) { + assertBeansAreFound(configClass, "foo", "bar"); + } + + private static void assertBeansAreFound(Class> configClass, String expected1, String expected2) { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configClass)) { + String bean1 = context.getBean("bean1", String.class); + String bean2 = context.getBean("bean2", String.class); + + assertThat(bean1).isEqualTo(expected1); + assertThat(bean2).isEqualTo(expected2); + } + } + + + interface ConfigInterface { + + @Bean + default String bean1() { + return "foo"; + } + + @Bean + default String bean2() { + return "bar"; + } + } + + static class InterfaceDefaultMethodsConfig implements ConfigInterface { + } + + static class BaseConfig { + + @Bean + String bean1() { + return "foo"; + } + + @Bean + String bean2() { + return "bar"; + } + } + + static class OverridingConfig extends BaseConfig { + + @Bean + @Override + String bean2() { + return "xyz"; + } + } + + static class ExtendedConfig extends OverridingConfig { + } + +}