Merge pull request #42073 from nosan

* pr/42073:
  Polish "Add consistent scope support for ConfigurationProperties beans"
  Add consistent scope support for ConfigurationProperties beans

Closes gh-42073
This commit is contained in:
Stéphane Nicoll 2024-09-11 05:39:56 +02:00
commit af850a8d99
6 changed files with 161 additions and 51 deletions

View File

@ -16,12 +16,19 @@
package org.springframework.boot.context.properties;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.context.properties.bind.BindMethod;
import org.springframework.context.annotation.AnnotationScopeMetadataResolver;
import org.springframework.context.annotation.ScopeMetadata;
import org.springframework.context.annotation.ScopeMetadataResolver;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
@ -38,6 +45,8 @@ import org.springframework.util.StringUtils;
*/
final class ConfigurationPropertiesBeanRegistrar {
private static final ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver();
private final BeanDefinitionRegistry registry;
private final BeanFactory beanFactory;
@ -75,17 +84,30 @@ final class ConfigurationPropertiesBeanRegistrar {
MergedAnnotation<ConfigurationProperties> annotation) {
Assert.state(annotation.isPresent(), () -> "No " + ConfigurationProperties.class.getSimpleName()
+ " annotation found on '" + type.getName() + "'.");
this.registry.registerBeanDefinition(beanName, createBeanDefinition(beanName, type));
BeanDefinitionReaderUtils.registerBeanDefinition(createBeanDefinition(beanName, type), this.registry);
}
private BeanDefinition createBeanDefinition(String beanName, Class<?> type) {
private BeanDefinitionHolder createBeanDefinition(String beanName, Class<?> type) {
GenericBeanDefinition definition = new AnnotatedGenericBeanDefinition(type);
BindMethod bindMethod = ConfigurationPropertiesBean.deduceBindMethod(type);
RootBeanDefinition definition = new RootBeanDefinition(type);
BindMethodAttribute.set(definition, bindMethod);
if (bindMethod == BindMethod.VALUE_OBJECT) {
definition.setInstanceSupplier(() -> ConstructorBound.from(this.beanFactory, beanName, type));
}
return definition;
ScopeMetadata metadata = scopeMetadataResolver.resolveScopeMetadata(definition);
definition.setScope(metadata.getScopeName());
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(definition, beanName);
return applyScopedProxyMode(metadata, definitionHolder, this.registry);
}
static BeanDefinitionHolder applyScopedProxyMode(ScopeMetadata metadata, BeanDefinitionHolder definition,
BeanDefinitionRegistry registry) {
ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();
if (scopedProxyMode.equals(ScopedProxyMode.NO)) {
return definition;
}
boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);
return ScopedProxyUtils.createScopedProxy(definition, registry, proxyTargetClass);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-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.
@ -20,12 +20,14 @@ import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.aop.scope.ScopedProxyFactoryBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.context.properties.bind.BindMethod;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
@ -75,7 +77,7 @@ class ConfigurationPropertiesBeanRegistrarTests {
String beanName = "valuecp-" + ValueObjectConfigurationProperties.class.getName();
this.registrar.register(ValueObjectConfigurationProperties.class);
BeanDefinition definition = this.registry.getBeanDefinition(beanName);
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.VALUE_OBJECT));
assertThat(definition).satisfies(hasBindMethodAttribute(BindMethod.VALUE_OBJECT));
}
@Test
@ -83,12 +85,45 @@ class ConfigurationPropertiesBeanRegistrarTests {
String beanName = MultiConstructorBeanConfigurationProperties.class.getName();
this.registrar.register(MultiConstructorBeanConfigurationProperties.class);
BeanDefinition definition = this.registry.getBeanDefinition(beanName);
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
assertThat(definition).satisfies(hasBindMethodAttribute(BindMethod.JAVA_BEAN));
}
private Consumer<BeanDefinition> configurationPropertiesBeanDefinition(BindMethod bindMethod) {
@Test
void registerWhenNoScopeUsesSingleton() {
String beanName = "beancp-" + BeanConfigurationProperties.class.getName();
this.registrar.register(BeanConfigurationProperties.class);
BeanDefinition definition = this.registry.getBeanDefinition(beanName);
assertThat(definition).isNotNull();
assertThat(definition.getScope()).isEqualTo(BeanDefinition.SCOPE_SINGLETON);
}
@Test
void registerScopedBeanDefinition() {
String beanName = "beancp-" + ScopedBeanConfigurationProperties.class.getName();
this.registrar.register(ScopedBeanConfigurationProperties.class);
BeanDefinition beanDefinition = this.registry.getBeanDefinition(beanName);
assertThat(beanDefinition).isNotNull();
assertThat(beanDefinition.getBeanClassName()).isEqualTo(ScopedBeanConfigurationProperties.class.getName());
assertThat(beanDefinition.getScope()).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE);
}
@Test
void registerScopedBeanDefinitionWithProxyMode() {
String beanName = "beancp-" + ProxyScopedBeanConfigurationProperties.class.getName();
this.registrar.register(ProxyScopedBeanConfigurationProperties.class);
BeanDefinition proxiedBeanDefinition = this.registry.getBeanDefinition(beanName);
assertThat(proxiedBeanDefinition).isNotNull();
assertThat(proxiedBeanDefinition.getBeanClassName()).isEqualTo(ScopedProxyFactoryBean.class.getName());
String targetBeanName = (String) proxiedBeanDefinition.getPropertyValues().get("targetBeanName");
assertThat(targetBeanName).isNotNull();
BeanDefinition beanDefinition = this.registry.getBeanDefinition(targetBeanName);
assertThat(beanDefinition).isNotNull();
assertThat(beanDefinition.getBeanClassName()).isEqualTo(ProxyScopedBeanConfigurationProperties.class.getName());
assertThat(beanDefinition.getScope()).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE);
}
private Consumer<BeanDefinition> hasBindMethodAttribute(BindMethod bindMethod) {
return (definition) -> {
assertThat(definition).isExactlyInstanceOf(RootBeanDefinition.class);
assertThat(definition.hasAttribute(BindMethod.class.getName())).isTrue();
assertThat(definition.getAttribute(BindMethod.class.getName())).isEqualTo(bindMethod);
};
@ -99,6 +134,18 @@ class ConfigurationPropertiesBeanRegistrarTests {
}
@ConfigurationProperties(prefix = "beancp")
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
static class ScopedBeanConfigurationProperties {
}
@ConfigurationProperties(prefix = "beancp")
@Scope(scopeName = BeanDefinition.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
static class ProxyScopedBeanConfigurationProperties {
}
static class NoAnnotationConfigurationProperties {
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-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.
@ -17,13 +17,13 @@
package org.springframework.boot.context.properties;
import java.io.IOException;
import java.util.Locale;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.context.properties.bind.BindMethod;
import org.springframework.boot.context.properties.scan.combined.c.CombinedConfiguration;
import org.springframework.boot.context.properties.scan.combined.d.OtherCombinedConfiguration;
@ -56,9 +56,9 @@ class ConfigurationPropertiesScanRegistrarTests {
"foo-org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration$FooProperties");
BeanDefinition barDefinition = this.beanFactory.getBeanDefinition(
"bar-org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration$BarProperties");
assertThat(bingDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
assertThat(fooDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
assertThat(barDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.VALUE_OBJECT));
assertThat(bingDefinition).satisfies(hasBindMethod(BindMethod.JAVA_BEAN));
assertThat(fooDefinition).satisfies(hasBindMethod(BindMethod.JAVA_BEAN));
assertThat(barDefinition).satisfies(hasBindMethod(BindMethod.VALUE_OBJECT));
}
@Test
@ -67,9 +67,12 @@ class ConfigurationPropertiesScanRegistrarTests {
beanFactory.setAllowBeanDefinitionOverriding(false);
this.registrar.registerBeanDefinitions(
getAnnotationMetadata(ConfigurationPropertiesScanConfiguration.TestConfiguration.class), beanFactory);
BeanDefinition fooDefinition = beanFactory.getBeanDefinition(
"foo-org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration$FooProperties");
assertThat(fooDefinition).isExactlyInstanceOf(RootBeanDefinition.class);
assertThat(beanFactory.containsBeanDefinition(
"foo-org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration$FooProperties"))
.isTrue();
assertThat(beanFactory.getBeanDefinitionNames())
.filteredOn((name) -> name.toLowerCase(Locale.ENGLISH).contains("fooproperties"))
.hasSize(1);
}
@Test
@ -88,11 +91,11 @@ class ConfigurationPropertiesScanRegistrarTests {
"b.first-org.springframework.boot.context.properties.scan.valid.b.BScanConfiguration$BFirstProperties");
BeanDefinition bSecondDefinition = beanFactory.getBeanDefinition(
"b.second-org.springframework.boot.context.properties.scan.valid.b.BScanConfiguration$BSecondProperties");
assertThat(aDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
assertThat(aDefinition).satisfies(hasBindMethod(BindMethod.JAVA_BEAN));
// Constructor injection
assertThat(bFirstDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.VALUE_OBJECT));
assertThat(bFirstDefinition).satisfies(hasBindMethod(BindMethod.VALUE_OBJECT));
// Post-processing injection
assertThat(bSecondDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
assertThat(bSecondDefinition).satisfies(hasBindMethod(BindMethod.JAVA_BEAN));
}
@Test
@ -112,9 +115,8 @@ class ConfigurationPropertiesScanRegistrarTests {
assertThat(beanFactory.getBeanDefinitionCount()).isZero();
}
private Consumer<BeanDefinition> configurationPropertiesBeanDefinition(BindMethod bindMethod) {
private Consumer<BeanDefinition> hasBindMethod(BindMethod bindMethod) {
return (definition) -> {
assertThat(definition).isExactlyInstanceOf(RootBeanDefinition.class);
assertThat(definition.hasAttribute(BindMethod.class.getName())).isTrue();
assertThat(definition.getAttribute(BindMethod.class.getName())).isEqualTo(bindMethod);
};

View File

@ -48,6 +48,7 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
@ -584,13 +585,21 @@ class ConfigurationPropertiesTests {
}
@Test
void loadShouldSupportRebindableConfigurationProperties() {
// gh-9160
void loadShouldSupportRebindableConfigurationPropertiesRegisteredAsBean() {
testRebindableConfigurationProperties(PrototypePropertiesBeanConfiguration.class);
}
@Test
void loadShouldSupportRebindableConfigurationPropertiesRegisteredUsingRegistrar() {
testRebindableConfigurationProperties(PrototypePropertiesRegistrarConfiguration.class);
}
void testRebindableConfigurationProperties(Class<?> configurationClass) {
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();
Map<String, Object> source = new LinkedHashMap<>();
source.put("example.one", "foo");
sources.addFirst(new MapPropertySource("test-source", source));
this.context.register(PrototypePropertiesConfiguration.class);
this.context.register(configurationClass);
this.context.refresh();
PrototypeBean first = this.context.getBean(PrototypeBean.class);
assertThat(first.getOne()).isEqualTo("foo");
@ -602,12 +611,24 @@ class ConfigurationPropertiesTests {
}
@Test
void loadWhenHasPropertySourcesPlaceholderConfigurerShouldSupportRebindableConfigurationProperties() {
void loadWhenHasPropertySourcesPlaceholderConfigurerShouldSupportRebindableConfigurationPropertiesRegisteredAsBean() {
testPropertySourcesPlaceholderConfigurerShouldSupportRebindableConfigurationProperties(
PrototypePropertiesBeanConfiguration.class);
}
@Test
void loadWhenHasPropertySourcesPlaceholderConfigurerShouldSupportRebindableConfigurationPropertiesRegisteredUsingRegistrar() {
testPropertySourcesPlaceholderConfigurerShouldSupportRebindableConfigurationProperties(
PrototypePropertiesRegistrarConfiguration.class);
}
void testPropertySourcesPlaceholderConfigurerShouldSupportRebindableConfigurationProperties(
Class<?> configurationClass) {
MutablePropertySources sources = this.context.getEnvironment().getPropertySources();
Map<String, Object> source = new LinkedHashMap<>();
source.put("example.one", "foo");
sources.addFirst(new MapPropertySource("test-source", source));
this.context.register(PrototypePropertiesConfiguration.class);
this.context.register(configurationClass);
this.context.register(PropertySourcesPlaceholderConfigurer.class);
this.context.refresh();
PrototypeBean first = this.context.getBean(PrototypeBean.class);
@ -1495,10 +1516,10 @@ class ConfigurationPropertiesTests {
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
static class PrototypePropertiesConfiguration {
static class PrototypePropertiesBeanConfiguration {
@Bean
@Scope("prototype")
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
@ConfigurationProperties("example")
PrototypeBean prototypeBean() {
return new PrototypeBean();
@ -1506,6 +1527,18 @@ class ConfigurationPropertiesTests {
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PrototypeBeanProperties.class)
static class PrototypePropertiesRegistrarConfiguration {
}
@ConfigurationProperties("example")
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
static class PrototypeBeanProperties extends PrototypeBean {
}
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "test")
static class PropertiesWithResource {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-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.
@ -23,7 +23,6 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.context.properties.bind.BindMethod;
import org.springframework.core.type.AnnotationMetadata;
@ -57,7 +56,7 @@ class EnableConfigurationPropertiesRegistrarTests {
register(TestConfiguration.class);
BeanDefinition definition = this.beanFactory
.getBeanDefinition("foo-" + getClass().getName() + "$FooProperties");
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
assertThat(definition).satisfies(hasBindMethod(BindMethod.JAVA_BEAN));
}
@Test
@ -65,7 +64,7 @@ class EnableConfigurationPropertiesRegistrarTests {
register(TestConfiguration.class);
BeanDefinition definition = this.beanFactory
.getBeanDefinition("bar-" + getClass().getName() + "$BarProperties");
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.VALUE_OBJECT));
assertThat(definition).satisfies(hasBindMethod(BindMethod.VALUE_OBJECT));
}
@Test
@ -73,7 +72,7 @@ class EnableConfigurationPropertiesRegistrarTests {
register(TestConfiguration.class);
BeanDefinition definition = this.beanFactory
.getBeanDefinition("bing-" + getClass().getName() + "$BingProperties");
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
assertThat(definition).satisfies(hasBindMethod(BindMethod.JAVA_BEAN));
}
@Test
@ -99,9 +98,8 @@ class EnableConfigurationPropertiesRegistrarTests {
}
}
private Consumer<BeanDefinition> configurationPropertiesBeanDefinition(BindMethod bindMethod) {
private Consumer<BeanDefinition> hasBindMethod(BindMethod bindMethod) {
return (definition) -> {
assertThat(definition).isExactlyInstanceOf(RootBeanDefinition.class);
assertThat(definition.hasAttribute(BindMethod.class.getName())).isTrue();
assertThat(definition.getAttribute(BindMethod.class.getName())).isEqualTo(bindMethod);
};

View File

@ -1,9 +1,24 @@
/*
* Copyright 2012-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.boot.context.properties
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.support.DefaultListableBeanFactory
import org.springframework.beans.factory.support.RootBeanDefinition
import org.springframework.boot.context.properties.bind.BindMethod
/**
@ -19,15 +34,7 @@ class KotlinConfigurationPropertiesBeanRegistrarTests {
private val registrar = ConfigurationPropertiesBeanRegistrar(beanFactory)
@Test
fun `type with default constructor should register root bean definition`() {
this.registrar.register(FooProperties::class.java)
val beanDefinition = this.beanFactory.getBeanDefinition(
"foo-org.springframework.boot.context.properties.KotlinConfigurationPropertiesBeanRegistrarTests\$FooProperties")
assertThat(beanDefinition).isExactlyInstanceOf(RootBeanDefinition::class.java)
}
@Test
fun `type with primary constructor and no autowired should register configuration properties bean definition`() {
fun `type with primary constructor and no autowired should register value object configuration properties`() {
this.registrar.register(BarProperties::class.java)
val beanDefinition = this.beanFactory.getBeanDefinition(
"bar-org.springframework.boot.context.properties.KotlinConfigurationPropertiesBeanRegistrarTests\$BarProperties")
@ -36,11 +43,12 @@ class KotlinConfigurationPropertiesBeanRegistrarTests {
}
@Test
fun `type with no primary constructor should register root bean definition`() {
fun `type with no primary constructor should register java bean configuration properties`() {
this.registrar.register(BingProperties::class.java)
val beanDefinition = this.beanFactory.getBeanDefinition(
"bing-org.springframework.boot.context.properties.KotlinConfigurationPropertiesBeanRegistrarTests\$BingProperties")
assertThat(beanDefinition).isExactlyInstanceOf(RootBeanDefinition::class.java)
assertThat(beanDefinition.hasAttribute(BindMethod::class.java.name)).isTrue()
assertThat(beanDefinition.getAttribute(BindMethod::class.java.name)).isEqualTo(BindMethod.JAVA_BEAN)
}
@ConfigurationProperties(prefix = "foo")