Skip to content

Commit 33c2d24

Browse files
committed
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
1 parent 8da2959 commit 33c2d24

File tree

8 files changed

+273
-12
lines changed

8 files changed

+273
-12
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfiguration.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ public BuildProperties buildProperties() throws Exception {
7676

7777
protected Properties loadFrom(Resource location, String prefix, Charset encoding)
7878
throws IOException {
79-
String p = prefix.endsWith(".") ? prefix : prefix + ".";
79+
prefix = prefix.endsWith(".") ? prefix : prefix + ".";
8080
Properties source = loadSource(location, encoding);
8181
Properties target = new Properties();
8282
for (String key : source.stringPropertyNames()) {
83-
if (key.startsWith(p)) {
84-
target.put(key.substring(p.length()), source.get(key));
83+
if (key.startsWith(prefix)) {
84+
target.put(key.substring(prefix.length()), source.get(key));
8585
}
8686
}
8787
return target;
@@ -93,9 +93,7 @@ private Properties loadSource(Resource location, Charset encoding)
9393
return PropertiesLoaderUtils
9494
.loadProperties(new EncodedResource(location, encoding));
9595
}
96-
else {
97-
return PropertiesLoaderUtils.loadProperties(location);
98-
}
96+
return PropertiesLoaderUtils.loadProperties(location);
9997
}
10098

10199
static class GitResourceAvailableCondition extends SpringBootCondition {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2012-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.context.properties;
18+
19+
import org.springframework.boot.context.properties.bind.AbstractBindHandler;
20+
import org.springframework.boot.context.properties.bind.BindHandler;
21+
22+
/**
23+
* Allows additional functionality to be applied to the {@link BindHandler} used by the
24+
* {@link ConfigurationPropertiesBindingPostProcessor}.
25+
*
26+
* @author Phillip Webb
27+
* @since 2.1.0
28+
* @see AbstractBindHandler
29+
*/
30+
@FunctionalInterface
31+
public interface ConfigurationPropertiesBindHandlerAdvisor {
32+
33+
/**
34+
* Apply additional functionality to the source bind handler.
35+
* @param bindHandler the source bind handler
36+
* @return a replacement bind hander that delegates to the source and provides
37+
* additional functionality
38+
*/
39+
BindHandler apply(BindHandler bindHandler);
40+
41+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.List;
2121
import java.util.function.Consumer;
22+
import java.util.stream.Collectors;
2223

2324
import org.springframework.beans.PropertyEditorRegistry;
2425
import org.springframework.boot.context.properties.bind.BindHandler;
@@ -126,9 +127,18 @@ private BindHandler getBindHandler(ConfigurationProperties annotation,
126127
handler = new ValidationBindHandler(handler,
127128
validators.toArray(new Validator[0]));
128129
}
130+
for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
131+
handler = advisor.apply(handler);
132+
}
129133
return handler;
130134
}
131135

136+
private List<ConfigurationPropertiesBindHandlerAdvisor> getBindHandlerAdvisors() {
137+
return this.applicationContext
138+
.getBeanProvider(ConfigurationPropertiesBindHandlerAdvisor.class)
139+
.orderedStream().collect(Collectors.toList());
140+
}
141+
132142
private Binder getBinder() {
133143
if (this.binder == null) {
134144
this.binder = new Binder(getConfigurationPropertySources(),

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AbstractBindHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2017 the original author or authors.
2+
* Copyright 2012-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -47,7 +47,7 @@ public AbstractBindHandler(BindHandler parent) {
4747
}
4848

4949
@Override
50-
public boolean onStart(ConfigurationPropertyName name, Bindable<?> target,
50+
public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target,
5151
BindContext context) {
5252
return this.parent.onStart(name, target, context);
5353
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828
*/
2929
public interface BindContext {
3030

31+
/**
32+
* Return the source binder that is performing the bind operation.
33+
* @return the source binder
34+
*/
35+
Binder getBinder();
36+
3137
/**
3238
* Return the current depth of the binding. Root binding starts with a depth of
3339
* {@code 0}. Each subsequent property binding increases the depth by {@code 1}.

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2017 the original author or authors.
2+
* Copyright 2012-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,14 +37,15 @@ public interface BindHandler {
3737

3838
/**
3939
* Called when binding of an element starts but before any result has been determined.
40+
* @param <T> the bindable source type
4041
* @param name the name of the element being bound
4142
* @param target the item being bound
4243
* @param context the bind context
4344
* @return {@code true} if binding should proceed
4445
*/
45-
default boolean onStart(ConfigurationPropertyName name, Bindable<?> target,
46+
default <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target,
4647
BindContext context) {
47-
return true;
48+
return target;
4849
}
4950

5051
/**

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ protected final <T> T bind(ConfigurationPropertyName name, Bindable<T> target,
214214
BindHandler handler, Context context, boolean allowRecursiveBinding) {
215215
context.clearConfigurationProperty();
216216
try {
217-
if (!handler.onStart(name, target, context)) {
217+
target = handler.onStart(name, target, context);
218+
if (target == null) {
218219
return null;
219220
}
220221
Object bound = bindObject(name, target, handler, context,
@@ -467,6 +468,11 @@ public BindConverter getConverter() {
467468
return this.converter;
468469
}
469470

471+
@Override
472+
public Binder getBinder() {
473+
return Binder.this;
474+
}
475+
470476
@Override
471477
public int getDepth() {
472478
return this.depth;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* Copyright 2012-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.context.properties;
18+
19+
import java.util.LinkedHashMap;
20+
import java.util.Map;
21+
import java.util.TreeMap;
22+
23+
import org.junit.After;
24+
import org.junit.Test;
25+
26+
import org.springframework.boot.context.properties.bind.AbstractBindHandler;
27+
import org.springframework.boot.context.properties.bind.BindContext;
28+
import org.springframework.boot.context.properties.bind.BindHandler;
29+
import org.springframework.boot.context.properties.bind.BindResult;
30+
import org.springframework.boot.context.properties.bind.Bindable;
31+
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
32+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
33+
import org.springframework.context.annotation.Configuration;
34+
import org.springframework.context.annotation.Import;
35+
import org.springframework.test.context.support.TestPropertySourceUtils;
36+
37+
import static org.assertj.core.api.Assertions.assertThat;
38+
39+
/**
40+
* Tests for {@link ConfigurationPropertiesBindHandlerAdvisor}.
41+
*
42+
* @author Phillip Webb
43+
*/
44+
public class ConfigurationPropertiesBindHandlerAdvisorTests {
45+
46+
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
47+
48+
@After
49+
public void cleanup() {
50+
this.context.close();
51+
}
52+
53+
@Test
54+
public void loadWithoutConfigurationPropertiesBindHandlerAdvisor() {
55+
load(WithoutConfigurationPropertiesBindHandlerAdvisor.class,
56+
"foo.bar.default.content-type=text/plain",
57+
"foo.bar.bindings.input.destination=d1",
58+
"foo.bar.bindings.input.content-type=text/xml",
59+
"foo.bar.bindings.output.destination=d2");
60+
BindingServiceProperties properties = this.context
61+
.getBean(BindingServiceProperties.class);
62+
BindingProperties input = properties.getBindings().get("input");
63+
assertThat(input.getDestination()).isEqualTo("d1");
64+
assertThat(input.getContentType()).isEqualTo("text/xml");
65+
BindingProperties output = properties.getBindings().get("output");
66+
assertThat(output.getDestination()).isEqualTo("d2");
67+
assertThat(output.getContentType()).isEqualTo("application/json");
68+
}
69+
70+
@Test
71+
public void loadWithConfigurationPropertiesBindHandlerAdvisor() {
72+
load(WithConfigurationPropertiesBindHandlerAdvisor.class,
73+
"foo.bar.default.content-type=text/plain",
74+
"foo.bar.bindings.input.destination=d1",
75+
"foo.bar.bindings.input.content-type=text/xml",
76+
"foo.bar.bindings.output.destination=d2");
77+
BindingServiceProperties properties = this.context
78+
.getBean(BindingServiceProperties.class);
79+
BindingProperties input = properties.getBindings().get("input");
80+
assertThat(input.getDestination()).isEqualTo("d1");
81+
assertThat(input.getContentType()).isEqualTo("text/xml");
82+
BindingProperties output = properties.getBindings().get("output");
83+
assertThat(output.getDestination()).isEqualTo("d2");
84+
assertThat(output.getContentType()).isEqualTo("text/plain");
85+
}
86+
87+
private AnnotationConfigApplicationContext load(Class<?> configuration,
88+
String... inlinedProperties) {
89+
return load(new Class<?>[] { configuration }, inlinedProperties);
90+
}
91+
92+
private AnnotationConfigApplicationContext load(Class<?>[] configuration,
93+
String... inlinedProperties) {
94+
this.context.register(configuration);
95+
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.context,
96+
inlinedProperties);
97+
this.context.refresh();
98+
return this.context;
99+
}
100+
101+
@Configuration
102+
@EnableConfigurationProperties(BindingServiceProperties.class)
103+
static class WithoutConfigurationPropertiesBindHandlerAdvisor {
104+
105+
}
106+
107+
@Configuration
108+
@EnableConfigurationProperties(BindingServiceProperties.class)
109+
@Import(DefaultValuesConfigurationPropertiesBindHandlerAdvisor.class)
110+
static class WithConfigurationPropertiesBindHandlerAdvisor {
111+
112+
}
113+
114+
static class DefaultValuesConfigurationPropertiesBindHandlerAdvisor
115+
implements ConfigurationPropertiesBindHandlerAdvisor {
116+
117+
@Override
118+
public BindHandler apply(BindHandler bindHandler) {
119+
return new DefaultValuesBindHandler(bindHandler);
120+
}
121+
122+
}
123+
124+
static class DefaultValuesBindHandler extends AbstractBindHandler {
125+
126+
private final Map<ConfigurationPropertyName, ConfigurationPropertyName> mappings;
127+
128+
DefaultValuesBindHandler(BindHandler bindHandler) {
129+
super(bindHandler);
130+
this.mappings = new LinkedHashMap<>();
131+
this.mappings.put(ConfigurationPropertyName.of("foo.bar.bindings"),
132+
ConfigurationPropertyName.of("foo.bar.default"));
133+
}
134+
135+
@Override
136+
public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target,
137+
BindContext context) {
138+
ConfigurationPropertyName defaultName = getDefaultName(name);
139+
if (defaultName != null) {
140+
BindResult<T> result = context.getBinder().bind(defaultName, target);
141+
if (result.isBound()) {
142+
return target.withExistingValue(result.get());
143+
}
144+
}
145+
return super.onStart(name, target, context);
146+
147+
}
148+
149+
private ConfigurationPropertyName getDefaultName(ConfigurationPropertyName name) {
150+
for (Map.Entry<ConfigurationPropertyName, ConfigurationPropertyName> mapping : this.mappings
151+
.entrySet()) {
152+
ConfigurationPropertyName from = mapping.getKey();
153+
ConfigurationPropertyName to = mapping.getValue();
154+
if (name.getNumberOfElements() == from.getNumberOfElements() + 1
155+
&& from.isParentOf(name)) {
156+
return to;
157+
}
158+
}
159+
return null;
160+
}
161+
162+
}
163+
164+
@ConfigurationProperties("foo.bar")
165+
static class BindingServiceProperties {
166+
167+
private Map<String, BindingProperties> bindings = new TreeMap<>();
168+
169+
public Map<String, BindingProperties> getBindings() {
170+
return this.bindings;
171+
}
172+
173+
}
174+
175+
static class BindingProperties {
176+
177+
private String destination;
178+
179+
private String contentType = "application/json";
180+
181+
public String getDestination() {
182+
return this.destination;
183+
}
184+
185+
public void setDestination(String destination) {
186+
this.destination = destination;
187+
}
188+
189+
public String getContentType() {
190+
return this.contentType;
191+
}
192+
193+
public void setContentType(String contentType) {
194+
this.contentType = contentType;
195+
}
196+
197+
}
198+
199+
}

0 commit comments

Comments
 (0)