Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2012-present 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.jspecify.annotations.Nullable;

import org.springframework.beans.BeansException;
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;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.util.ClassUtils;

/**
* {@link BeanDefinitionRegistryPostProcessor} that ensures any bean registered
* programmatically (for example, via {@code BeanRegistrar}) that is annotated with
* {@link ConfigurationProperties @ConfigurationProperties} is enriched with the correct
* {@link BindMethodAttribute} and, for constructor-bound types, an
* {@code instanceSupplier}. Enrichment is delegated to
* {@link ConfigurationPropertiesBeanRegistrar#enrichBeanDefinition}.
*
* @author Ujjawal Tyagi
* @since 4.0.0
*/
final class ConfigurationPropertiesBeanDefinitionPostProcessor
implements BeanDefinitionRegistryPostProcessor, PriorityOrdered {

static final String BEAN_NAME = ConfigurationPropertiesBeanDefinitionPostProcessor.class.getName();

@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
ConfigurableListableBeanFactory beanFactory = (registry instanceof ConfigurableListableBeanFactory clbf)
? clbf : null;
@Nullable ClassLoader classLoader = (beanFactory != null) ? beanFactory.getBeanClassLoader()
: ClassUtils.getDefaultClassLoader();
for (String beanName : registry.getBeanDefinitionNames()) {
BeanDefinition definition = registry.getBeanDefinition(beanName);
if (BindMethodAttribute.get(definition) != null) {
continue; // already enriched (e.g. via @EnableConfigurationProperties or scan)
}
Class<?> type = resolveClass(definition, classLoader);
if (type == null) {
continue;
}
if (!MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY)
.isPresent(ConfigurationProperties.class)) {
continue;
}
if (beanFactory != null) {
ConfigurationPropertiesBeanRegistrar.enrichBeanDefinition(beanName, definition, type, beanFactory);
}
}
}

private @Nullable Class<?> resolveClass(BeanDefinition definition, @Nullable ClassLoader classLoader) {
if (definition instanceof AbstractBeanDefinition abstractDef && abstractDef.hasBeanClass()) {
return abstractDef.getBeanClass();
}
String className = definition.getBeanClassName();
if (className != null) {
try {
return ClassUtils.forName(className, classLoader);
}
catch (Throwable ex) {
// Ignore
}
}
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.context.properties.bind.BindMethod;
Expand Down Expand Up @@ -119,4 +121,27 @@ static BeanDefinitionHolder applyScopedProxyMode(ScopeMetadata metadata, BeanDef
return definition;
}

/**
* Enrich an already-registered {@link BeanDefinition} for a
* {@link ConfigurationProperties @ConfigurationProperties} class by stamping the
* {@link BindMethodAttribute} and, for {@link BindMethod#VALUE_OBJECT} types, setting
* an {@code instanceSupplier} that performs constructor-based binding via
* {@link ConstructorBound}. This avoids the need to create a new bean definition and
* is intended for beans registered programmatically (for example, via
* {@code BeanRegistrar}) that bypass the standard
* {@link ConfigurationPropertiesBeanRegistrar} path.
* @param beanName the name of the bean
* @param definition the existing bean definition to enrich
* @param type the configuration properties class
* @param beanFactory the bean factory, used to resolve the binder for VALUE_OBJECT types
*/
static void enrichBeanDefinition(String beanName, BeanDefinition definition, Class<?> type,
BeanFactory beanFactory) {
BindMethod bindMethod = ConfigurationPropertiesBean.deduceBindMethod(type);
BindMethodAttribute.set(definition, bindMethod);
if (bindMethod == BindMethod.VALUE_OBJECT && definition instanceof AbstractBeanDefinition abstractDefinition) {
abstractDefinition.setInstanceSupplier(() -> ConstructorBound.from(beanFactory, beanName, type));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ public static void register(BeanDefinitionRegistry registry) {
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(BEAN_NAME, definition);
}
if (!registry.containsBeanDefinition(ConfigurationPropertiesBeanDefinitionPostProcessor.BEAN_NAME)) {
BeanDefinition definition = BeanDefinitionBuilder
.rootBeanDefinition(ConfigurationPropertiesBeanDefinitionPostProcessor.class)
.getBeanDefinition();
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(ConfigurationPropertiesBeanDefinitionPostProcessor.BEAN_NAME, definition);
}
ConfigurationPropertiesBinder.register(registry);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Copyright 2012-present 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.junit.jupiter.api.Test;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.context.properties.bind.BindMethod;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.test.context.support.TestPropertySourceUtils;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for {@link ConfigurationPropertiesBeanDefinitionPostProcessor}.
*
* @author Ujjawal Tyagi
*/
class ConfigurationPropertiesBeanDefinitionPostProcessorTests {

@Test
void postProcessorStampsBindMethodAttributeOnJavaBeanProperties() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
BeanDefinition definition = BeanDefinitionBuilder
.genericBeanDefinition(JavaBeanProperties.class)
.getBeanDefinition();
beanFactory.registerBeanDefinition("javaBean", definition);

new ConfigurationPropertiesBeanDefinitionPostProcessor().postProcessBeanDefinitionRegistry(beanFactory);

BeanDefinition processed = beanFactory.getBeanDefinition("javaBean");
assertThat(processed.getAttribute(BindMethod.class.getName())).isEqualTo(BindMethod.JAVA_BEAN);
}

@Test
void postProcessorStampsBindMethodAttributeAndInstanceSupplierOnValueObjectProperties() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
BeanDefinition definition = BeanDefinitionBuilder
.genericBeanDefinition(ValueObjectProperties.class)
.getBeanDefinition();
beanFactory.registerBeanDefinition("valueObject", definition);

new ConfigurationPropertiesBeanDefinitionPostProcessor().postProcessBeanDefinitionRegistry(beanFactory);

BeanDefinition processed = beanFactory.getBeanDefinition("valueObject");
assertThat(processed.getAttribute(BindMethod.class.getName())).isEqualTo(BindMethod.VALUE_OBJECT);
assertThat(((AbstractBeanDefinition) processed).getInstanceSupplier()).isNotNull();
}

@Test
void postProcessorSkipsBeansAlreadyHavingBindMethodAttribute() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
BeanDefinition definition = BeanDefinitionBuilder
.genericBeanDefinition(JavaBeanProperties.class)
.getBeanDefinition();
// Pre-stamp with VALUE_OBJECT to simulate a bean already processed by the registrar
BindMethodAttribute.set(definition, BindMethod.VALUE_OBJECT);
beanFactory.registerBeanDefinition("alreadyProcessed", definition);

new ConfigurationPropertiesBeanDefinitionPostProcessor().postProcessBeanDefinitionRegistry(beanFactory);

// Must not override the pre-existing attribute
assertThat(definition.getAttribute(BindMethod.class.getName())).isEqualTo(BindMethod.VALUE_OBJECT);
}

@Test
void endToEndJavaBeanPropertiesBoundWhenRegisteredProgrammatically() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, "test.java-prop=hello");
ConfigurationPropertiesBindingPostProcessor.register(context);
context.registerBeanDefinition("javaBean",
BeanDefinitionBuilder.genericBeanDefinition(JavaBeanProperties.class).getBeanDefinition());
context.refresh();

JavaBeanProperties bean = context.getBean(JavaBeanProperties.class);
assertThat(bean.getJavaProp()).isEqualTo("hello");
}
}

@Test
void endToEndValueObjectPropertiesBoundWhenRegisteredProgrammatically() {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, "test.value-prop=world");
ConfigurationPropertiesBindingPostProcessor.register(context);
context.registerBeanDefinition("valueObject",
BeanDefinitionBuilder.genericBeanDefinition(ValueObjectProperties.class).getBeanDefinition());
context.refresh();

ValueObjectProperties bean = context.getBean(ValueObjectProperties.class);
assertThat(bean.getValueProp()).isEqualTo("world");
}
}

@ConfigurationProperties("test")
static class JavaBeanProperties {

private String javaProp = "";

public String getJavaProp() {
return this.javaProp;
}

public void setJavaProp(String javaProp) {
this.javaProp = javaProp;
}

}

@ConfigurationProperties("test")
static class ValueObjectProperties {

private final String valueProp;

ValueObjectProperties(String valueProp) {
this.valueProp = valueProp;
}

public String getValueProp() {
return this.valueProp;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package org.springframework.boot.context.properties

import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.support.BeanDefinitionBuilder
import org.springframework.beans.factory.support.BeanDefinitionRegistry
import org.springframework.beans.factory.support.RootBeanDefinition
import org.springframework.context.annotation.AnnotationConfigApplicationContext
Expand Down Expand Up @@ -136,4 +137,22 @@ class KotlinConfigurationPropertiesTests {
var bar: String = ""
}

@Test
fun `programmatically registered Kotlin data class configuration properties can be bound`() {
AnnotationConfigApplicationContext().use { context ->
ConfigurationPropertiesBindingPostProcessor.register(context)
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, "my-prefix.my-property=hello")
val beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(MyProperties::class.java).getBeanDefinition()
context.registerBeanDefinition("myProperties", beanDefinition)
context.refresh()
val myProperties = context.getBean(MyProperties::class.java)
assertThat(myProperties.myProperty).isEqualTo("hello")
}
}

@ConfigurationProperties("my-prefix")
data class MyProperties(
val myProperty: String = ""
)

}