Skip to content

Commit be50390

Browse files
committed
Defer creation of maps in MapBinder
Closes gh-39375
1 parent 8a3b0cd commit be50390

File tree

2 files changed

+69
-10
lines changed
  • spring-boot-project/spring-boot/src

2 files changed

+69
-10
lines changed

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,22 @@ protected boolean isAllowRecursiveBinding(ConfigurationPropertySource source) {
5353
@Override
5454
protected Object bindAggregate(ConfigurationPropertyName name, Bindable<?> target,
5555
AggregateElementBinder elementBinder) {
56-
Map<Object, Object> map = CollectionFactory
57-
.createMap((target.getValue() != null) ? Map.class : target.getType().resolve(Object.class), 0);
5856
Bindable<?> resolvedTarget = resolveTarget(target);
5957
boolean hasDescendants = hasDescendants(name);
60-
for (ConfigurationPropertySource source : getContext().getSources()) {
61-
if (!ConfigurationPropertyName.EMPTY.equals(name)) {
58+
if (!hasDescendants && !ConfigurationPropertyName.EMPTY.equals(name)) {
59+
for (ConfigurationPropertySource source : getContext().getSources()) {
6260
ConfigurationProperty property = source.getConfigurationProperty(name);
63-
if (property != null && !hasDescendants) {
61+
if (property != null) {
6462
getContext().setConfigurationProperty(property);
6563
Object result = getContext().getPlaceholdersResolver().resolvePlaceholders(property.getValue());
6664
return getContext().getConverter().convert(result, target);
6765
}
66+
}
67+
}
68+
Map<Object, Object> map = CollectionFactory
69+
.createMap((target.getValue() != null) ? Map.class : target.getType().resolve(Object.class), 0);
70+
for (ConfigurationPropertySource source : getContext().getSources()) {
71+
if (!ConfigurationPropertyName.EMPTY.equals(name)) {
6872
source = source.filter(name::isAncestorOf);
6973
}
7074
new EntryBinder(name, resolvedTarget, elementBinder).bindEntries(source, map);

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

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
package org.springframework.boot.context.properties.bind;
1818

1919
import java.net.InetAddress;
20+
import java.util.AbstractMap;
2021
import java.util.ArrayList;
2122
import java.util.Collections;
2223
import java.util.HashMap;
2324
import java.util.LinkedHashMap;
2425
import java.util.List;
2526
import java.util.Map;
2627
import java.util.Properties;
28+
import java.util.Set;
2729
import java.util.stream.Collectors;
2830

2931
import org.junit.jupiter.api.Test;
@@ -33,6 +35,7 @@
3335

3436
import org.springframework.boot.context.properties.bind.BinderTests.ExampleEnum;
3537
import org.springframework.boot.context.properties.bind.BinderTests.JavaBean;
38+
import org.springframework.boot.context.properties.bind.MapBinderTests.CustomMapWithoutDefaultCtor.CustomMap;
3639
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
3740
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
3841
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
@@ -78,7 +81,7 @@ class MapBinderTests {
7881

7982
private final List<ConfigurationPropertySource> sources = new ArrayList<>();
8083

81-
private Binder binder = new Binder(this.sources);
84+
private final Binder binder = new Binder(this.sources);
8285

8386
@Test
8487
void bindToMapShouldReturnPopulatedMap() {
@@ -315,15 +318,13 @@ void bindToMapWithPlaceholdersShouldBeGreedyForScalars() {
315318
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, "foo=boo");
316319
MockConfigurationPropertySource source = new MockConfigurationPropertySource("foo.aaa.bbb.ccc", "baz-${foo}");
317320
this.sources.add(source);
318-
this.binder = new Binder(this.sources, new PropertySourcesPlaceholdersResolver(environment));
319-
Map<String, ExampleEnum> result = this.binder.bind("foo", Bindable.mapOf(String.class, ExampleEnum.class))
320-
.get();
321+
Binder binder = new Binder(this.sources, new PropertySourcesPlaceholdersResolver(environment));
322+
Map<String, ExampleEnum> result = binder.bind("foo", Bindable.mapOf(String.class, ExampleEnum.class)).get();
321323
assertThat(result).containsEntry("aaa.bbb.ccc", ExampleEnum.BAZ_BOO);
322324
}
323325

324326
@Test
325327
void bindToMapWithNoPropertiesShouldReturnUnbound() {
326-
this.binder = new Binder(this.sources);
327328
BindResult<Map<String, ExampleEnum>> result = this.binder.bind("foo",
328329
Bindable.mapOf(String.class, ExampleEnum.class));
329330
assertThat(result.isBound()).isFalse();
@@ -624,6 +625,18 @@ void bindToMapWithPlaceholdersShouldResolve() {
624625
assertThat(map).containsKey("bcd");
625626
}
626627

628+
@Test
629+
void bindToCustomMapWithoutCtorAndConverterShouldResolve() {
630+
DefaultConversionService conversionService = new DefaultConversionService();
631+
conversionService.addConverter(new CustomMapConverter());
632+
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
633+
source.put("foo.custom-map", "value");
634+
this.sources.add(source);
635+
Binder binder = new Binder(this.sources, null, conversionService, null);
636+
CustomMapWithoutDefaultCtor result = binder.bind("foo", Bindable.of(CustomMapWithoutDefaultCtor.class)).get();
637+
assertThat(result.getCustomMap().getSource()).isEqualTo("value");
638+
}
639+
627640
private <K, V> Bindable<Map<K, V>> getMapBindable(Class<K> keyGeneric, ResolvableType valueType) {
628641
ResolvableType keyType = ResolvableType.forClass(keyGeneric);
629642
return Bindable.of(ResolvableType.forClassWithGenerics(Map.class, keyType, valueType));
@@ -761,6 +774,48 @@ void setAddresses(Map<String, ? extends List<? extends InetAddress>> addresses)
761774

762775
}
763776

777+
static class CustomMapWithoutDefaultCtor {
778+
779+
private final CustomMap customMap;
780+
781+
CustomMapWithoutDefaultCtor(CustomMap customMap) {
782+
this.customMap = customMap;
783+
}
784+
785+
CustomMap getCustomMap() {
786+
return this.customMap;
787+
}
788+
789+
static final class CustomMap extends AbstractMap<String, Object> {
790+
791+
private final String source;
792+
793+
CustomMap(String source) {
794+
this.source = source;
795+
}
796+
797+
@Override
798+
public Set<Entry<String, Object>> entrySet() {
799+
return Collections.emptySet();
800+
}
801+
802+
String getSource() {
803+
return this.source;
804+
}
805+
806+
}
807+
808+
}
809+
810+
private static final class CustomMapConverter implements Converter<String, CustomMap> {
811+
812+
@Override
813+
public CustomMap convert(String source) {
814+
return new CustomMap(source);
815+
}
816+
817+
}
818+
764819
private static final class InvocationArgument<T> implements Answer<T> {
765820

766821
private final int index;

0 commit comments

Comments
 (0)