Defer creation of maps in MapBinder

Closes gh-39375
This commit is contained in:
Moritz Halbritter 2024-04-25 17:07:03 +02:00
parent 8a3b0cd221
commit be5039041c
2 changed files with 69 additions and 10 deletions

View File

@ -53,18 +53,22 @@ class MapBinder extends AggregateBinder<Map<Object, Object>> {
@Override
protected Object bindAggregate(ConfigurationPropertyName name, Bindable<?> target,
AggregateElementBinder elementBinder) {
Map<Object, Object> 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<Object, Object> 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);

View File

@ -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.LinkedHashMap;
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.mockito.stubbing.Answer;
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<ConfigurationPropertySource> 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 @@ class MapBinderTests {
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<String, ExampleEnum> result = this.binder.bind("foo", Bindable.mapOf(String.class, ExampleEnum.class))
.get();
Binder binder = new Binder(this.sources, new PropertySourcesPlaceholdersResolver(environment));
Map<String, ExampleEnum> 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<Map<String, ExampleEnum>> result = this.binder.bind("foo",
Bindable.mapOf(String.class, ExampleEnum.class));
assertThat(result.isBound()).isFalse();
@ -624,6 +625,18 @@ class MapBinderTests {
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 <K, V> Bindable<Map<K, V>> getMapBindable(Class<K> keyGeneric, ResolvableType valueType) {
ResolvableType keyType = ResolvableType.forClass(keyGeneric);
return Bindable.of(ResolvableType.forClassWithGenerics(Map.class, keyType, valueType));
@ -761,6 +774,48 @@ class MapBinderTests {
}
static class CustomMapWithoutDefaultCtor {
private final CustomMap customMap;
CustomMapWithoutDefaultCtor(CustomMap customMap) {
this.customMap = customMap;
}
CustomMap getCustomMap() {
return this.customMap;
}
static final class CustomMap extends AbstractMap<String, Object> {
private final String source;
CustomMap(String source) {
this.source = source;
}
@Override
public Set<Entry<String, Object>> entrySet() {
return Collections.emptySet();
}
String getSource() {
return this.source;
}
}
}
private static final class CustomMapConverter implements Converter<String, CustomMap> {
@Override
public CustomMap convert(String source) {
return new CustomMap(source);
}
}
private static final class InvocationArgument<T> implements Answer<T> {
private final int index;