mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 08:24:13 +00:00
Avoid redundant object construction in DataBinder.createMap()
Previously, createMap() invoked createIndexedValue() – and therefore createObject() for non-simple value types – once per matching parameter name rather than once per distinct map key, causing redundant nested object construction for map entries whose value type has multiple constructor parameters. To address that, this commit aligns createMap() with createList() and createArray() by skipping construction for keys that have already been resolved. Closes gh-37019
This commit is contained in:
@@ -1069,6 +1069,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
||||
if (map == null) {
|
||||
map = CollectionFactory.createMap(paramType, 16);
|
||||
}
|
||||
else if (map.containsKey(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String indexedPath = name.substring(0, endIdx + 1);
|
||||
map.put(key, createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver));
|
||||
|
||||
+36
@@ -21,6 +21,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -158,6 +159,27 @@ class DataBinderConstructTests {
|
||||
assertThat(map.get("c").param1()).isEqualTo("value3");
|
||||
}
|
||||
|
||||
@Test // gh-37019
|
||||
void dataClassWithMapBindingConstructsValueOncePerKey() {
|
||||
CountingRecord.constructorCallCount.set(0);
|
||||
MapValueResolver valueResolver = new MapValueResolver(Map.of(
|
||||
"countingMap[a].param1", "value1", "countingMap[a].param2", "value2",
|
||||
"countingMap[b].param1", "value3", "countingMap[b].param2", "value4"));
|
||||
|
||||
DataBinder binder = initDataBinder(CountingMapRecord.class);
|
||||
binder.construct(valueResolver);
|
||||
|
||||
CountingMapRecord target = getTarget(binder);
|
||||
Map<String, CountingRecord> map = target.countingMap();
|
||||
|
||||
assertThat(map).hasSize(2);
|
||||
assertThat(map.get("a").param1()).isEqualTo("value1");
|
||||
assertThat(map.get("a").param2()).isEqualTo("value2");
|
||||
assertThat(map.get("b").param1()).isEqualTo("value3");
|
||||
assertThat(map.get("b").param2()).isEqualTo("value4");
|
||||
assertThat(CountingRecord.constructorCallCount).hasValue(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void dataClassWithArrayBinding() {
|
||||
MapValueResolver valueResolver = new MapValueResolver(Map.of(
|
||||
@@ -327,6 +349,20 @@ class DataBinderConstructTests {
|
||||
}
|
||||
|
||||
|
||||
record CountingRecord(String param1, String param2) {
|
||||
|
||||
static final AtomicInteger constructorCallCount = new AtomicInteger();
|
||||
|
||||
CountingRecord {
|
||||
constructorCallCount.incrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private record CountingMapRecord(Map<String, CountingRecord> countingMap) {
|
||||
}
|
||||
|
||||
|
||||
private record DataClassArrayRecord(DataClass[] dataClassArray) {
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user