diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java index ef0ef9757cf4..9a4b1bad6a90 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java @@ -53,18 +53,22 @@ protected boolean isAllowRecursiveBinding(ConfigurationPropertySource source) { @Override protected Object bindAggregate(ConfigurationPropertyName name, Bindable target, AggregateElementBinder elementBinder) { - Map map = CollectionFactory - .createMap((target.getValue() != null) ? Map.class : target.getType().resolve(Object.class), 0); Bindable resolvedTarget = resolveTarget(target); boolean hasDescendants = hasDescendants(name); - for (ConfigurationPropertySource source : getContext().getSources()) { - if (!ConfigurationPropertyName.EMPTY.equals(name)) { + if (!hasDescendants && !ConfigurationPropertyName.EMPTY.equals(name)) { + for (ConfigurationPropertySource source : getContext().getSources()) { ConfigurationProperty property = source.getConfigurationProperty(name); - if (property != null && !hasDescendants) { + if (property != null) { getContext().setConfigurationProperty(property); Object result = getContext().getPlaceholdersResolver().resolvePlaceholders(property.getValue()); return getContext().getConverter().convert(result, target); } + } + } + Map map = CollectionFactory + .createMap((target.getValue() != null) ? Map.class : target.getType().resolve(Object.class), 0); + for (ConfigurationPropertySource source : getContext().getSources()) { + if (!ConfigurationPropertyName.EMPTY.equals(name)) { source = source.filter(name::isAncestorOf); } new EntryBinder(name, resolvedTarget, elementBinder).bindEntries(source, map); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java index 1a4306298f67..0a23259e0964 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.context.properties.bind; import java.net.InetAddress; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -24,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -33,6 +35,7 @@ import org.springframework.boot.context.properties.bind.BinderTests.ExampleEnum; import org.springframework.boot.context.properties.bind.BinderTests.JavaBean; +import org.springframework.boot.context.properties.bind.MapBinderTests.CustomMapWithoutDefaultCtor.CustomMap; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; @@ -78,7 +81,7 @@ class MapBinderTests { private final List sources = new ArrayList<>(); - private Binder binder = new Binder(this.sources); + private final Binder binder = new Binder(this.sources); @Test void bindToMapShouldReturnPopulatedMap() { @@ -315,15 +318,13 @@ void bindToMapWithPlaceholdersShouldBeGreedyForScalars() { TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, "foo=boo"); MockConfigurationPropertySource source = new MockConfigurationPropertySource("foo.aaa.bbb.ccc", "baz-${foo}"); this.sources.add(source); - this.binder = new Binder(this.sources, new PropertySourcesPlaceholdersResolver(environment)); - Map result = this.binder.bind("foo", Bindable.mapOf(String.class, ExampleEnum.class)) - .get(); + Binder binder = new Binder(this.sources, new PropertySourcesPlaceholdersResolver(environment)); + Map result = binder.bind("foo", Bindable.mapOf(String.class, ExampleEnum.class)).get(); assertThat(result).containsEntry("aaa.bbb.ccc", ExampleEnum.BAZ_BOO); } @Test void bindToMapWithNoPropertiesShouldReturnUnbound() { - this.binder = new Binder(this.sources); BindResult> result = this.binder.bind("foo", Bindable.mapOf(String.class, ExampleEnum.class)); assertThat(result.isBound()).isFalse(); @@ -624,6 +625,18 @@ void bindToMapWithPlaceholdersShouldResolve() { assertThat(map).containsKey("bcd"); } + @Test + void bindToCustomMapWithoutCtorAndConverterShouldResolve() { + DefaultConversionService conversionService = new DefaultConversionService(); + conversionService.addConverter(new CustomMapConverter()); + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("foo.custom-map", "value"); + this.sources.add(source); + Binder binder = new Binder(this.sources, null, conversionService, null); + CustomMapWithoutDefaultCtor result = binder.bind("foo", Bindable.of(CustomMapWithoutDefaultCtor.class)).get(); + assertThat(result.getCustomMap().getSource()).isEqualTo("value"); + } + private Bindable> getMapBindable(Class keyGeneric, ResolvableType valueType) { ResolvableType keyType = ResolvableType.forClass(keyGeneric); return Bindable.of(ResolvableType.forClassWithGenerics(Map.class, keyType, valueType)); @@ -761,6 +774,48 @@ void setAddresses(Map> addresses) } + static class CustomMapWithoutDefaultCtor { + + private final CustomMap customMap; + + CustomMapWithoutDefaultCtor(CustomMap customMap) { + this.customMap = customMap; + } + + CustomMap getCustomMap() { + return this.customMap; + } + + static final class CustomMap extends AbstractMap { + + private final String source; + + CustomMap(String source) { + this.source = source; + } + + @Override + public Set> entrySet() { + return Collections.emptySet(); + } + + String getSource() { + return this.source; + } + + } + + } + + private static final class CustomMapConverter implements Converter { + + @Override + public CustomMap convert(String source) { + return new CustomMap(source); + } + + } + private static final class InvocationArgument implements Answer { private final int index;