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:
parent
8da295998b
commit
33c2d24560
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue