Skip to content

Commit

Permalink
Support to inherit common properties for configuring multiple beans
Browse files Browse the repository at this point in the history
It's used for configuring multiple beans (spring-projects#15732), we could extract common or use primary properties as parent now.

Take `org.springframework.boot.autoconfigure.data.redis.RedisProperties` for example, given:
```
# primary
spring.data.redis:
  host: 127.0.0.1
  port: 6379

# additional
additional.data.redis:
  port: 6380
```
Then effective properties:
```
additional.data.redis:
  host: 127.0.0.1
  port: 6380
```
should be bound to `additionalRedisProperties`:
```java
	@bean(autowireCandidate = false) // do not back off autoconfigured one
	@ConfigurationProperties(prefix = "additional.data.redis", inheritedPrefix = "spring.data.redis")
	RedisProperties additionalRedisProperties() {
		return new RedisProperties();
	}
```
  • Loading branch information
quaff committed Oct 12, 2024
1 parent 35361d1 commit 743331b
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
* values are externalized.
*
* @author Dave Syer
* @author Yanming Zhou
* @since 1.0.0
* @see ConfigurationPropertiesScan
* @see ConstructorBinding
Expand Down Expand Up @@ -69,6 +70,14 @@
@AliasFor("value")
String prefix() default "";

/**
* The prefix of the properties that {@link #prefix()} will inherit, It's used for
* configuring multiple beans which share common properties.
* @return the prefix of the properties to inherit
* @see #prefix()
*/
String inheritedPrefix() default "";

/**
* Flag to indicate that when binding to this object invalid fields should be ignored.
* Invalid means invalid according to the binder that is used, and usually this means
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package org.springframework.boot.context.properties;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import org.springframework.beans.BeansException;
Expand Down Expand Up @@ -49,8 +51,13 @@
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.PropertySources;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
Expand All @@ -61,6 +68,7 @@
*
* @author Stephane Nicoll
* @author Phillip Webb
* @author Yanming Zhou
*/
class ConfigurationPropertiesBinder {

Expand Down Expand Up @@ -89,14 +97,22 @@ BindResult<?> bind(ConfigurationPropertiesBean propertiesBean) {
Bindable<?> target = propertiesBean.asBindTarget();
ConfigurationProperties annotation = propertiesBean.getAnnotation();
BindHandler bindHandler = getBindHandler(target, annotation);
return getBinder().bind(annotation.prefix(), target, bindHandler);
Binder binderToUse = StringUtils.hasText(annotation.prefix())
&& StringUtils.hasText(annotation.inheritedPrefix())
? createBinderForInheritedPrefix(annotation.prefix(), annotation.inheritedPrefix())
: getBinder();
return binderToUse.bind(annotation.prefix(), target, bindHandler);
}

Object bindOrCreate(ConfigurationPropertiesBean propertiesBean) {
Bindable<?> target = propertiesBean.asBindTarget();
ConfigurationProperties annotation = propertiesBean.getAnnotation();
BindHandler bindHandler = getBindHandler(target, annotation);
return getBinder().bindOrCreate(annotation.prefix(), target, bindHandler);
Binder binderToUse = StringUtils.hasText(annotation.prefix())
&& StringUtils.hasText(annotation.inheritedPrefix())
? createBinderForInheritedPrefix(annotation.prefix(), annotation.inheritedPrefix())
: getBinder();
return binderToUse.bindOrCreate(annotation.prefix(), target, bindHandler);
}

private Validator getConfigurationPropertiesValidator(ApplicationContext applicationContext) {
Expand Down Expand Up @@ -182,6 +198,30 @@ private Iterable<ConfigurationPropertySource> getConfigurationPropertySources()
return ConfigurationPropertySources.from(this.propertySources);
}

private Binder createBinderForInheritedPrefix(String prefix, String inheritedPrefix) {
MutablePropertySources propertySourcesToUse = new MutablePropertySources(this.propertySources);
propertySourcesToUse.addLast(createPropertySourceForInheritedPrefix(prefix, inheritedPrefix));
return new Binder(ConfigurationPropertySources.from(propertySourcesToUse),
getPropertySourcesPlaceholdersResolver(), getConversionServices(), getPropertyEditorInitializer(), null,
null);
}

private PropertySource<?> createPropertySourceForInheritedPrefix(String prefix, String inheritedPrefix) {
Map<String, Object> map = new HashMap<>();
for (PropertySource<?> propertySource : this.propertySources) {
if (propertySource instanceof EnumerablePropertySource<?> enumerablePropertySource) {
for (String key : enumerablePropertySource.getPropertyNames()) {
if (key.startsWith(inheritedPrefix + '.')) {
map.put(prefix + '.' + key.substring(inheritedPrefix.length() + 1),
enumerablePropertySource.getProperty(key));
}
}
}
}
return new MapPropertySource(
"MapPropertySource for prefix '%s' inheriting from '%s'".formatted(prefix, inheritedPrefix), map);
}

private PropertySourcesPlaceholdersResolver getPropertySourcesPlaceholdersResolver() {
return new PropertySourcesPlaceholdersResolver(this.propertySources);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
* @author Stephane Nicoll
* @author Madhura Bhave
* @author Vladislav Kisel
* @author Yanming Zhou
*/
@ExtendWith(OutputCaptureExtension.class)
class ConfigurationPropertiesTests {
Expand Down Expand Up @@ -1270,6 +1271,25 @@ void loadWhenBindingToJavaBeanWithConversionToCustomListImplementation() {
assertThat(this.context.getBean(SetterBoundCustomListProperties.class).getValues()).containsExactly("a", "b");
}

@Test
void loadWhenUsingInheritedPrefixForJavaBeanBinder() {
load(SetterBoundInheritedPrefixConfiguration.class, "spring.service.host=127.0.0.1", "spring.service.port=6379",
"additional.service.port=6380");
SetterBoundServiceProperties properties = this.context.getBean("additionalServiceProperties",
SetterBoundServiceProperties.class);
assertThat(properties.getPort()).isEqualTo(6380);
assertThat(properties.getHost()).isEqualTo("127.0.0.1");
}

@Test
void loadWhenUsingInheritedPrefixForValueObjectBinder() {
load(ConstructorBoundInheritedPrefixConfiguration.class, "spring.service.host=127.0.0.1",
"spring.service.port=6379", "additional.service.port=6380");
ConstructorBoundServiceProperties properties = this.context.getBean(ConstructorBoundServiceProperties.class);
assertThat(properties.getPort()).isEqualTo(6380);
assertThat(properties.getHost()).isEqualTo("127.0.0.1");
}

private AnnotationConfigApplicationContext load(Class<?> configuration, String... inlinedProperties) {
return load(new Class<?>[] { configuration }, inlinedProperties);
}
Expand Down Expand Up @@ -3310,4 +3330,68 @@ static final class CustomList<E> extends ArrayList<E> {

}

@ConfigurationProperties(prefix = "spring.service")
static class SetterBoundServiceProperties {

private String host = "localhost";

private int port = 6379;

String getHost() {
return this.host;
}

void setHost(String host) {
this.host = host;
}

int getPort() {
return this.port;
}

void setPort(int port) {
this.port = port;
}

}

@EnableConfigurationProperties(SetterBoundServiceProperties.class)
static class SetterBoundInheritedPrefixConfiguration {

@Bean(autowireCandidate = false) // do not back off auto-configured one
@ConfigurationProperties(prefix = "additional.service", inheritedPrefix = "spring.service")
SetterBoundServiceProperties additionalServiceProperties() {
return new SetterBoundServiceProperties();
}

}

@ConfigurationProperties(prefix = "additional.service", inheritedPrefix = "spring.service")
static class ConstructorBoundServiceProperties {

private final String host;

private final int port;

public ConstructorBoundServiceProperties(@DefaultValue("localhost") String host,
@DefaultValue("6379") int port) {
this.host = host;
this.port = port;
}

String getHost() {
return this.host;
}

int getPort() {
return this.port;
}

}

@EnableConfigurationProperties(ConstructorBoundServiceProperties.class)
static class ConstructorBoundInheritedPrefixConfiguration {

}

}

0 comments on commit 743331b

Please sign in to comment.