Skip to content

Commit f337323

Browse files
committed
Allow recursive binding in Maps
Update `Binder` so that Maps containing references to themselves may be bound. The existing stack-overflow protection (required when binding a bean to a non enumerable source) now only applies to bean properties. Fixes gh-9801
1 parent 3ec3b64 commit f337323

File tree

3 files changed

+84
-9
lines changed

3 files changed

+84
-9
lines changed

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

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -183,18 +183,18 @@ public <T> BindResult<T> bind(ConfigurationPropertyName name, Bindable<T> target
183183
Assert.notNull(target, "Target must not be null");
184184
handler = (handler != null ? handler : BindHandler.DEFAULT);
185185
Context context = new Context();
186-
T bound = bind(name, target, handler, context);
186+
T bound = bind(name, target, handler, context, false);
187187
return BindResult.of(bound);
188188
}
189189

190190
protected final <T> T bind(ConfigurationPropertyName name, Bindable<T> target,
191-
BindHandler handler, Context context) {
191+
BindHandler handler, Context context, boolean skipIfHasBoundBean) {
192192
context.clearConfigurationProperty();
193193
try {
194194
if (!handler.onStart(name, target, context)) {
195195
return null;
196196
}
197-
Object bound = bindObject(name, target, handler, context);
197+
Object bound = bindObject(name, target, handler, context, skipIfHasBoundBean);
198198
return handleBindResult(name, target, handler, context, bound);
199199
}
200200
catch (Exception ex) {
@@ -234,7 +234,8 @@ private <T> T convert(Object value, Bindable<T> target) {
234234
}
235235

236236
private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target,
237-
BindHandler handler, Context context) throws Exception {
237+
BindHandler handler, Context context, boolean skipIfHasBoundBean)
238+
throws Exception {
238239
ConfigurationProperty property = findProperty(name, context);
239240
if (property == null && containsNoDescendantOf(context.streamSources(), name)) {
240241
return null;
@@ -246,7 +247,7 @@ private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target
246247
if (property != null) {
247248
return bindProperty(name, target, handler, context, property);
248249
}
249-
return bindBean(name, target, handler, context);
250+
return bindBean(name, target, handler, context, skipIfHasBoundBean);
250251
}
251252

252253
private AggregateBinder<?> getAggregateBinder(Bindable<?> target, Context context) {
@@ -266,7 +267,8 @@ private AggregateBinder<?> getAggregateBinder(Bindable<?> target, Context contex
266267
private <T> Object bindAggregate(ConfigurationPropertyName name, Bindable<T> target,
267268
BindHandler handler, Context context, AggregateBinder<?> aggregateBinder) {
268269
AggregateElementBinder elementBinder = (itemName, itemTarget, source) -> {
269-
Supplier<?> supplier = () -> bind(itemName, itemTarget, handler, context);
270+
Supplier<?> supplier = () -> bind(itemName, itemTarget, handler, context,
271+
false);
270272
return context.withSource(source, supplier);
271273
};
272274
return context.withIncreasedDepth(
@@ -290,15 +292,16 @@ private <T> Object bindProperty(ConfigurationPropertyName name, Bindable<T> targ
290292
}
291293

292294
private Object bindBean(ConfigurationPropertyName name, Bindable<?> target,
293-
BindHandler handler, Context context) {
295+
BindHandler handler, Context context, boolean skipIfHasBoundBean) {
294296
if (containsNoDescendantOf(context.streamSources(), name)
295297
|| isUnbindableBean(name, target, context)) {
296298
return null;
297299
}
298300
BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(
299-
name.append(propertyName), propertyTarget, handler, context);
301+
name.append(propertyName), propertyTarget, handler, context, true);
300302
Class<?> type = target.getType().resolve();
301-
if (context.hasBoundBean(type)) {
303+
if (skipIfHasBoundBean && context.hasBoundBean(type)) {
304+
System.err.println(type + " " + name);
302305
return null;
303306
}
304307
return context.withBean(type, () -> {

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,17 +251,27 @@ public void assertion(BindException ex) throws AssertionError {
251251
@Test
252252
public void bindToValidatedBeanWithResourceAndNonEnumerablePropertySource() {
253253
ConfigurationPropertySources.from(new PropertySource<String>("test") {
254+
254255
@Override
255256
public Object getProperty(String name) {
256257
return null;
257258
}
259+
258260
}).forEach(this.sources::add);
259261
Validator validator = new SpringValidatorAdapter(Validation.byDefaultProvider()
260262
.configure().buildValidatorFactory().getValidator());
261263
this.binder.bind("foo", Bindable.of(ResourceBean.class),
262264
new ValidationBindHandler(validator));
263265
}
264266

267+
@Test
268+
public void bindToBeanWithCycle() throws Exception {
269+
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
270+
this.sources.add(source.nonIterable());
271+
Bindable<CycleBean1> target = Bindable.of(CycleBean1.class);
272+
this.binder.bind("foo", target);
273+
}
274+
265275
public static class JavaBean {
266276

267277
private String value;
@@ -303,4 +313,32 @@ public void setResource(Resource resource) {
303313

304314
}
305315

316+
public static class CycleBean1 {
317+
318+
private CycleBean2 two;
319+
320+
public CycleBean2 getTwo() {
321+
return this.two;
322+
}
323+
324+
public void setTwo(CycleBean2 two) {
325+
this.two = two;
326+
}
327+
328+
}
329+
330+
public static class CycleBean2 {
331+
332+
private CycleBean1 one;
333+
334+
public CycleBean1 getOne() {
335+
return this.one;
336+
}
337+
338+
public void setOne(CycleBean1 one) {
339+
this.one = one;
340+
}
341+
342+
}
343+
306344
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.Collections;
2121
import java.util.HashMap;
22+
import java.util.LinkedHashMap;
2223
import java.util.List;
2324
import java.util.Map;
2425
import java.util.Properties;
@@ -515,6 +516,19 @@ public void bindingWithSquareBracketMap() throws Exception {
515516
assertThat(map).containsEntry("x [B] y", "[ball]");
516517
}
517518

519+
@Test
520+
public void nestedMapsShouldNotBindToNull() throws Exception {
521+
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
522+
source.put("foo.value", "one");
523+
source.put("foo.foos.foo1.value", "two");
524+
source.put("foo.foos.foo2.value", "three");
525+
this.sources.add(source);
526+
BindResult<NestableFoo> foo = this.binder.bind("foo", NestableFoo.class);
527+
assertThat(foo.get().getValue()).isNotNull();
528+
assertThat(foo.get().getFoos().get("foo1").getValue()).isEqualTo("two");
529+
assertThat(foo.get().getFoos().get("foo2").getValue()).isEqualTo("three");
530+
}
531+
518532
private <K, V> Bindable<Map<K, V>> getMapBindable(Class<K> keyGeneric,
519533
ResolvableType valueType) {
520534
ResolvableType keyType = ResolvableType.forClass(keyGeneric);
@@ -547,4 +561,24 @@ public void setPattern(String pattern) {
547561

548562
}
549563

564+
static class NestableFoo {
565+
566+
private Map<String, NestableFoo> foos = new LinkedHashMap<>();
567+
568+
private String value;
569+
570+
public Map<String, NestableFoo> getFoos() {
571+
return this.foos;
572+
}
573+
574+
public String getValue() {
575+
return this.value;
576+
}
577+
578+
public void setValue(String value) {
579+
this.value = value;
580+
}
581+
582+
}
583+
550584
}

0 commit comments

Comments
 (0)