From a00fb1b5aeaa7d4722ed86b93978f8c7935e0c65 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:27:14 +0200 Subject: [PATCH] Avoid redundant object construction in DataBinder.createMap() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../validation/DataBinder.java | 3 ++ .../validation/DataBinderConstructTests.java | 36 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index 414bfeb0285..5a453b28545 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -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)); diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java index dc4b0d5cb3f..3eac155ad32 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java @@ -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 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 countingMap) { + } + + private record DataClassArrayRecord(DataClass[] dataClassArray) { }