Add bind method to ConfigurationProperties bean definition

This commit reworks the configuration properties registrar to use
RootBeanDefinition and a standard attribute rather than relying on
a package private sub-class. This allows other components to inspect
the metadata if necessary.

Closes gh-27821
This commit is contained in:
Stephane Nicoll 2021-09-06 10:25:00 +02:00
parent 542e6ab0d3
commit b4b71ec015
7 changed files with 84 additions and 94 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
@ -21,7 +21,7 @@ import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean.BindMethod;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
@ -89,12 +89,24 @@ final class ConfigurationPropertiesBeanRegistrar {
}
private BeanDefinition createBeanDefinition(String beanName, Class<?> type) {
if (BindMethod.forType(type) == BindMethod.VALUE_OBJECT) {
return new ConfigurationPropertiesValueObjectBeanDefinition(this.beanFactory, beanName, type);
BindMethod bindMethod = BindMethod.forType(type);
RootBeanDefinition definition = new RootBeanDefinition(type);
definition.setAttribute(BindMethod.class.getName(), bindMethod);
if (bindMethod == BindMethod.VALUE_OBJECT) {
definition.setInstanceSupplier(() -> createValueObject(beanName, type));
}
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(type);
return definition;
}
private Object createValueObject(String beanName, Class<?> beanType) {
ConfigurationPropertiesBean bean = ConfigurationPropertiesBean.forValueObject(beanType, beanName);
ConfigurationPropertiesBinder binder = ConfigurationPropertiesBinder.get(this.beanFactory);
try {
return binder.bindOrCreate(bean);
}
catch (Exception ex) {
throw new ConfigurationPropertiesBindException(bean, ex);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
@ -94,8 +94,8 @@ public class ConfigurationPropertiesBindingPostProcessor
}
private boolean hasBoundValueObject(String beanName) {
return this.registry.containsBeanDefinition(beanName) && this.registry
.getBeanDefinition(beanName) instanceof ConfigurationPropertiesValueObjectBeanDefinition;
return this.registry.containsBeanDefinition(beanName) && BindMethod.VALUE_OBJECT
.equals(this.registry.getBeanDefinition(beanName).getAttribute(BindMethod.class.getName()));
}
/**
@ -108,7 +108,7 @@ public class ConfigurationPropertiesBindingPostProcessor
Assert.notNull(registry, "Registry must not be null");
if (!registry.containsBeanDefinition(BEAN_NAME)) {
BeanDefinition definition = BeanDefinitionBuilder
.genericBeanDefinition(ConfigurationPropertiesBindingPostProcessor.class,
.rootBeanDefinition(ConfigurationPropertiesBindingPostProcessor.class,
ConfigurationPropertiesBindingPostProcessor::new)
.getBeanDefinition();
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);

View File

@ -1,56 +0,0 @@
/*
* Copyright 2012-2020 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.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.GenericBeanDefinition;
/**
* {@link BeanDefinition} that is used for registering
* {@link ConfigurationProperties @ConfigurationProperties} value object beans that are
* bound at creation time.
*
* @author Stephane Nicoll
* @author Madhura Bhave
* @author Phillip Webb
*/
final class ConfigurationPropertiesValueObjectBeanDefinition extends GenericBeanDefinition {
private final BeanFactory beanFactory;
private final String beanName;
ConfigurationPropertiesValueObjectBeanDefinition(BeanFactory beanFactory, String beanName, Class<?> beanClass) {
this.beanFactory = beanFactory;
this.beanName = beanName;
setBeanClass(beanClass);
setInstanceSupplier(this::createBean);
}
private Object createBean() {
ConfigurationPropertiesBean bean = ConfigurationPropertiesBean.forValueObject(getBeanClass(), this.beanName);
ConfigurationPropertiesBinder binder = ConfigurationPropertiesBinder.get(this.beanFactory);
try {
return binder.bindOrCreate(bean);
}
catch (Exception ex) {
throw new ConfigurationPropertiesBindException(bean, ex);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
@ -16,12 +16,16 @@
package org.springframework.boot.context.properties;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
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.ConfigurationPropertiesBean.BindMethod;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
@ -70,15 +74,23 @@ class ConfigurationPropertiesBeanRegistrarTests {
String beanName = "valuecp-" + ValueObjectConfigurationProperties.class.getName();
this.registrar.register(ValueObjectConfigurationProperties.class);
BeanDefinition definition = this.registry.getBeanDefinition(beanName);
assertThat(definition).isInstanceOf(ConfigurationPropertiesValueObjectBeanDefinition.class);
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.VALUE_OBJECT));
}
@Test
void registerWhenNotValueObjectRegistersGenericBeanDefinition() {
void registerWhenNotValueObjectRegistersRootBeanDefinitionWithJavaBeanBindMethod() {
String beanName = MultiConstructorBeanConfigurationProperties.class.getName();
this.registrar.register(MultiConstructorBeanConfigurationProperties.class);
BeanDefinition definition = this.registry.getBeanDefinition(beanName);
assertThat(definition).isInstanceOf(GenericBeanDefinition.class);
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
}
private Consumer<BeanDefinition> configurationPropertiesBeanDefinition(BindMethod bindMethod) {
return (definition) -> {
assertThat(definition).isExactlyInstanceOf(RootBeanDefinition.class);
assertThat(definition.hasAttribute(BindMethod.class.getName())).isTrue();
assertThat(definition.getAttribute(BindMethod.class.getName())).isEqualTo(bindMethod);
};
}
@ConfigurationProperties(prefix = "beancp")

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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,12 +17,14 @@
package org.springframework.boot.context.properties;
import java.io.IOException;
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.GenericBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean.BindMethod;
import org.springframework.boot.context.properties.scan.combined.c.CombinedConfiguration;
import org.springframework.boot.context.properties.scan.combined.d.OtherCombinedConfiguration;
import org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration;
@ -54,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).isExactlyInstanceOf(GenericBeanDefinition.class);
assertThat(fooDefinition).isExactlyInstanceOf(GenericBeanDefinition.class);
assertThat(barDefinition).isExactlyInstanceOf(ConfigurationPropertiesValueObjectBeanDefinition.class);
assertThat(bingDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
assertThat(fooDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
assertThat(barDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.VALUE_OBJECT));
}
@Test
@ -67,7 +69,7 @@ class ConfigurationPropertiesScanRegistrarTests {
getAnnotationMetadata(ConfigurationPropertiesScanConfiguration.TestConfiguration.class), beanFactory);
BeanDefinition fooDefinition = beanFactory.getBeanDefinition(
"foo-org.springframework.boot.context.properties.scan.valid.ConfigurationPropertiesScanConfiguration$FooProperties");
assertThat(fooDefinition).isExactlyInstanceOf(GenericBeanDefinition.class);
assertThat(fooDefinition).isExactlyInstanceOf(RootBeanDefinition.class);
}
@Test
@ -86,11 +88,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).isExactlyInstanceOf(GenericBeanDefinition.class);
assertThat(aDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
// Constructor injection
assertThat(bFirstDefinition).isExactlyInstanceOf(ConfigurationPropertiesValueObjectBeanDefinition.class);
assertThat(bFirstDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.VALUE_OBJECT));
// Post-processing injection
assertThat(bSecondDefinition).isExactlyInstanceOf(GenericBeanDefinition.class);
assertThat(bSecondDefinition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
}
@Test
@ -110,6 +112,14 @@ class ConfigurationPropertiesScanRegistrarTests {
assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(0);
}
private Consumer<BeanDefinition> configurationPropertiesBeanDefinition(BindMethod bindMethod) {
return (definition) -> {
assertThat(definition).isExactlyInstanceOf(RootBeanDefinition.class);
assertThat(definition.hasAttribute(BindMethod.class.getName())).isTrue();
assertThat(definition.getAttribute(BindMethod.class.getName())).isEqualTo(bindMethod);
};
}
private AnnotationMetadata getAnnotationMetadata(Class<?> source) throws IOException {
return new SimpleMetadataReaderFactory().getMetadataReader(source.getName()).getAnnotationMetadata();
}

View File

@ -16,12 +16,15 @@
package org.springframework.boot.context.properties;
import java.util.function.Consumer;
import org.junit.jupiter.api.BeforeEach;
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.GenericBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean.BindMethod;
import org.springframework.core.type.AnnotationMetadata;
import static org.assertj.core.api.Assertions.assertThat;
@ -51,27 +54,27 @@ class EnableConfigurationPropertiesRegistrarTests {
}
@Test
void typeWithDefaultConstructorShouldRegisterGenericBeanDefinition() {
void typeWithDefaultConstructorShouldRegisterRootBeanDefinition() {
register(TestConfiguration.class);
BeanDefinition beanDefinition = this.beanFactory
BeanDefinition definition = this.beanFactory
.getBeanDefinition("foo-" + getClass().getName() + "$FooProperties");
assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition.class);
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
}
@Test
void typeWithConstructorBindingShouldRegisterConfigurationPropertiesBeanDefinition() {
register(TestConfiguration.class);
BeanDefinition beanDefinition = this.beanFactory
BeanDefinition definition = this.beanFactory
.getBeanDefinition("bar-" + getClass().getName() + "$BarProperties");
assertThat(beanDefinition).isExactlyInstanceOf(ConfigurationPropertiesValueObjectBeanDefinition.class);
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.VALUE_OBJECT));
}
@Test
void typeWithMultipleConstructorsShouldRegisterGenericBeanDefinition() {
register(TestConfiguration.class);
BeanDefinition beanDefinition = this.beanFactory
BeanDefinition definition = this.beanFactory
.getBeanDefinition("bing-" + getClass().getName() + "$BingProperties");
assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition.class);
assertThat(definition).satisfies(configurationPropertiesBeanDefinition(BindMethod.JAVA_BEAN));
}
@Test
@ -97,6 +100,14 @@ class EnableConfigurationPropertiesRegistrarTests {
}
}
private Consumer<BeanDefinition> configurationPropertiesBeanDefinition(BindMethod bindMethod) {
return (definition) -> {
assertThat(definition).isExactlyInstanceOf(RootBeanDefinition.class);
assertThat(definition.hasAttribute(BindMethod.class.getName())).isTrue();
assertThat(definition.getAttribute(BindMethod.class.getName())).isEqualTo(bindMethod);
};
}
private void register(Class<?> configuration) {
AnnotationMetadata metadata = AnnotationMetadata.introspect(configuration);
this.registrar.registerBeanDefinitions(metadata, this.beanFactory);

View File

@ -3,7 +3,7 @@ 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.GenericBeanDefinition
import org.springframework.beans.factory.support.RootBeanDefinition
/**
* Tests for `ConfigurationPropertiesBeanRegistrar`.
@ -18,11 +18,11 @@ class KotlinConfigurationPropertiesBeanRegistrarTests {
private val registrar = ConfigurationPropertiesBeanRegistrar(beanFactory)
@Test
fun `type with default constructor should register generic bean definition`() {
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(GenericBeanDefinition::class.java)
assertThat(beanDefinition).isExactlyInstanceOf(RootBeanDefinition::class.java)
}
@Test
@ -30,16 +30,17 @@ class KotlinConfigurationPropertiesBeanRegistrarTests {
this.registrar.register(BarProperties::class.java)
val beanDefinition = this.beanFactory.getBeanDefinition(
"bar-org.springframework.boot.context.properties.KotlinConfigurationPropertiesBeanRegistrarTests\$BarProperties")
assertThat(beanDefinition).isExactlyInstanceOf(
ConfigurationPropertiesValueObjectBeanDefinition::class.java)
assertThat(beanDefinition.hasAttribute(ConfigurationPropertiesBean.BindMethod::class.java.name)).isTrue()
assertThat(beanDefinition.getAttribute(ConfigurationPropertiesBean.BindMethod::class.java.name))
.isEqualTo(ConfigurationPropertiesBean.BindMethod.VALUE_OBJECT)
}
@Test
fun `type with no primary constructor should register generic bean definition`() {
fun `type with no primary constructor should register root bean definition`() {
this.registrar.register(BingProperties::class.java)
val beanDefinition = this.beanFactory.getBeanDefinition(
"bing-org.springframework.boot.context.properties.KotlinConfigurationPropertiesBeanRegistrarTests\$BingProperties")
assertThat(beanDefinition).isExactlyInstanceOf(GenericBeanDefinition::class.java)
assertThat(beanDefinition).isExactlyInstanceOf(RootBeanDefinition::class.java)
}
@ConfigurationProperties(prefix = "foo")