Support ConfigurationProperties BindHandler advise

Allow custom `BinderHandler` advise to be applied to the `Binder` used
for `@ConfigurationProperties`.  This mechanism has been added to allow
Spring Cloud Stream to manipulate `Bindable` instances before binding
occurs.

NOTE: This commit introduces a breaking change to the `BindHandler`
interface since the `onStart` method now returns a `Bindable` rather
than a `boolean`.

Closes gh-14745
This commit is contained in:
Phillip Webb 2018-10-10 12:33:35 -07:00
parent 8da295998b
commit 33c2d24560
8 changed files with 273 additions and 12 deletions

View File

@ -76,12 +76,12 @@ public class ProjectInfoAutoConfiguration {
protected Properties loadFrom(Resource location, String prefix, Charset encoding)
throws IOException {
String p = prefix.endsWith(".") ? prefix : prefix + ".";
prefix = prefix.endsWith(".") ? prefix : prefix + ".";
Properties source = loadSource(location, encoding);
Properties target = new Properties();
for (String key : source.stringPropertyNames()) {
if (key.startsWith(p)) {
target.put(key.substring(p.length()), source.get(key));
if (key.startsWith(prefix)) {
target.put(key.substring(prefix.length()), source.get(key));
}
}
return target;
@ -93,9 +93,7 @@ public class ProjectInfoAutoConfiguration {
return PropertiesLoaderUtils
.loadProperties(new EncodedResource(location, encoding));
}
else {
return PropertiesLoaderUtils.loadProperties(location);
}
return PropertiesLoaderUtils.loadProperties(location);
}
static class GitResourceAvailableCondition extends SpringBootCondition {

View File

@ -0,0 +1,41 @@
/*
* Copyright 2012-2018 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
*
* http://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.boot.context.properties.bind.AbstractBindHandler;
import org.springframework.boot.context.properties.bind.BindHandler;
/**
* Allows additional functionality to be applied to the {@link BindHandler} used by the
* {@link ConfigurationPropertiesBindingPostProcessor}.
*
* @author Phillip Webb
* @since 2.1.0
* @see AbstractBindHandler
*/
@FunctionalInterface
public interface ConfigurationPropertiesBindHandlerAdvisor {
/**
* Apply additional functionality to the source bind handler.
* @param bindHandler the source bind handler
* @return a replacement bind hander that delegates to the source and provides
* additional functionality
*/
BindHandler apply(BindHandler bindHandler);
}

View File

@ -19,6 +19,7 @@ package org.springframework.boot.context.properties;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.boot.context.properties.bind.BindHandler;
@ -126,9 +127,18 @@ class ConfigurationPropertiesBinder {
handler = new ValidationBindHandler(handler,
validators.toArray(new Validator[0]));
}
for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
handler = advisor.apply(handler);
}
return handler;
}
private List<ConfigurationPropertiesBindHandlerAdvisor> getBindHandlerAdvisors() {
return this.applicationContext
.getBeanProvider(ConfigurationPropertiesBindHandlerAdvisor.class)
.orderedStream().collect(Collectors.toList());
}
private Binder getBinder() {
if (this.binder == null) {
this.binder = new Binder(getConfigurationPropertySources(),

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -47,7 +47,7 @@ public abstract class AbstractBindHandler implements BindHandler {
}
@Override
public boolean onStart(ConfigurationPropertyName name, Bindable<?> target,
public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target,
BindContext context) {
return this.parent.onStart(name, target, context);
}

View File

@ -28,6 +28,12 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyS
*/
public interface BindContext {
/**
* Return the source binder that is performing the bind operation.
* @return the source binder
*/
Binder getBinder();
/**
* Return the current depth of the binding. Root binding starts with a depth of
* {@code 0}. Each subsequent property binding increases the depth by {@code 1}.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2017 the original author or authors.
* Copyright 2012-2018 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.
@ -37,14 +37,15 @@ public interface BindHandler {
/**
* Called when binding of an element starts but before any result has been determined.
* @param <T> the bindable source type
* @param name the name of the element being bound
* @param target the item being bound
* @param context the bind context
* @return {@code true} if binding should proceed
*/
default boolean onStart(ConfigurationPropertyName name, Bindable<?> target,
default <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target,
BindContext context) {
return true;
return target;
}
/**

View File

@ -214,7 +214,8 @@ public class Binder {
BindHandler handler, Context context, boolean allowRecursiveBinding) {
context.clearConfigurationProperty();
try {
if (!handler.onStart(name, target, context)) {
target = handler.onStart(name, target, context);
if (target == null) {
return null;
}
Object bound = bindObject(name, target, handler, context,
@ -467,6 +468,11 @@ public class Binder {
return this.converter;
}
@Override
public Binder getBinder() {
return Binder.this;
}
@Override
public int getDepth() {
return this.depth;

View File

@ -0,0 +1,199 @@
/*
* Copyright 2012-2018 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
*
* http://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 java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import org.junit.After;
import org.junit.Test;
import org.springframework.boot.context.properties.bind.AbstractBindHandler;
import org.springframework.boot.context.properties.bind.BindContext;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.support.TestPropertySourceUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ConfigurationPropertiesBindHandlerAdvisor}.
*
* @author Phillip Webb
*/
public class ConfigurationPropertiesBindHandlerAdvisorTests {
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@After
public void cleanup() {
this.context.close();
}
@Test
public void loadWithoutConfigurationPropertiesBindHandlerAdvisor() {
load(WithoutConfigurationPropertiesBindHandlerAdvisor.class,
"foo.bar.default.content-type=text/plain",
"foo.bar.bindings.input.destination=d1",
"foo.bar.bindings.input.content-type=text/xml",
"foo.bar.bindings.output.destination=d2");
BindingServiceProperties properties = this.context
.getBean(BindingServiceProperties.class);
BindingProperties input = properties.getBindings().get("input");
assertThat(input.getDestination()).isEqualTo("d1");
assertThat(input.getContentType()).isEqualTo("text/xml");
BindingProperties output = properties.getBindings().get("output");
assertThat(output.getDestination()).isEqualTo("d2");
assertThat(output.getContentType()).isEqualTo("application/json");
}
@Test
public void loadWithConfigurationPropertiesBindHandlerAdvisor() {
load(WithConfigurationPropertiesBindHandlerAdvisor.class,
"foo.bar.default.content-type=text/plain",
"foo.bar.bindings.input.destination=d1",
"foo.bar.bindings.input.content-type=text/xml",
"foo.bar.bindings.output.destination=d2");
BindingServiceProperties properties = this.context
.getBean(BindingServiceProperties.class);
BindingProperties input = properties.getBindings().get("input");
assertThat(input.getDestination()).isEqualTo("d1");
assertThat(input.getContentType()).isEqualTo("text/xml");
BindingProperties output = properties.getBindings().get("output");
assertThat(output.getDestination()).isEqualTo("d2");
assertThat(output.getContentType()).isEqualTo("text/plain");
}
private AnnotationConfigApplicationContext load(Class<?> configuration,
String... inlinedProperties) {
return load(new Class<?>[] { configuration }, inlinedProperties);
}
private AnnotationConfigApplicationContext load(Class<?>[] configuration,
String... inlinedProperties) {
this.context.register(configuration);
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
inlinedProperties);
this.context.refresh();
return this.context;
}
@Configuration
@EnableConfigurationProperties(BindingServiceProperties.class)
static class WithoutConfigurationPropertiesBindHandlerAdvisor {
}
@Configuration
@EnableConfigurationProperties(BindingServiceProperties.class)
@Import(DefaultValuesConfigurationPropertiesBindHandlerAdvisor.class)
static class WithConfigurationPropertiesBindHandlerAdvisor {
}
static class DefaultValuesConfigurationPropertiesBindHandlerAdvisor
implements ConfigurationPropertiesBindHandlerAdvisor {
@Override
public BindHandler apply(BindHandler bindHandler) {
return new DefaultValuesBindHandler(bindHandler);
}
}
static class DefaultValuesBindHandler extends AbstractBindHandler {
private final Map<ConfigurationPropertyName, ConfigurationPropertyName> mappings;
DefaultValuesBindHandler(BindHandler bindHandler) {
super(bindHandler);
this.mappings = new LinkedHashMap<>();
this.mappings.put(ConfigurationPropertyName.of("foo.bar.bindings"),
ConfigurationPropertyName.of("foo.bar.default"));
}
@Override
public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target,
BindContext context) {
ConfigurationPropertyName defaultName = getDefaultName(name);
if (defaultName != null) {
BindResult<T> result = context.getBinder().bind(defaultName, target);
if (result.isBound()) {
return target.withExistingValue(result.get());
}
}
return super.onStart(name, target, context);
}
private ConfigurationPropertyName getDefaultName(ConfigurationPropertyName name) {
for (Map.Entry<ConfigurationPropertyName, ConfigurationPropertyName> mapping : this.mappings
.entrySet()) {
ConfigurationPropertyName from = mapping.getKey();
ConfigurationPropertyName to = mapping.getValue();
if (name.getNumberOfElements() == from.getNumberOfElements() + 1
&& from.isParentOf(name)) {
return to;
}
}
return null;
}
}
@ConfigurationProperties("foo.bar")
static class BindingServiceProperties {
private Map<String, BindingProperties> bindings = new TreeMap<>();
public Map<String, BindingProperties> getBindings() {
return this.bindings;
}
}
static class BindingProperties {
private String destination;
private String contentType = "application/json";
public String getDestination() {
return this.destination;
}
public void setDestination(String destination) {
this.destination = destination;
}
public String getContentType() {
return this.contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
}
}