Skip to content

Commit ec0ec7a

Browse files
committed
Avoid nested constructor binding if there are no request parameters
Closes gh-31821
1 parent 0970b1d commit ec0ec7a

File tree

5 files changed

+103
-7
lines changed

5 files changed

+103
-7
lines changed

Diff for: spring-context/src/main/java/org/springframework/validation/DataBinder.java

+17-2
Original file line numberDiff line numberDiff line change
@@ -948,7 +948,7 @@ private Object createObject(ResolvableType objectType, String nestedPath, ValueR
948948
Class<?> paramType = paramTypes[i];
949949
Object value = valueResolver.resolveValue(paramPath, paramType);
950950

951-
if (value == null && shouldConstructArgument(param)) {
951+
if (value == null && shouldConstructArgument(param) && hasValuesFor(paramPath, valueResolver)) {
952952
ResolvableType type = ResolvableType.forMethodParameter(param);
953953
args[i] = createObject(type, paramPath + ".", valueResolver);
954954
}
@@ -1022,6 +1022,15 @@ protected boolean shouldConstructArgument(MethodParameter param) {
10221022
type.getPackageName().startsWith("java."));
10231023
}
10241024

1025+
private boolean hasValuesFor(String paramPath, ValueResolver resolver) {
1026+
for (String name : resolver.getNames()) {
1027+
if (name.startsWith(paramPath + ".")) {
1028+
return true;
1029+
}
1030+
}
1031+
return false;
1032+
}
1033+
10251034
private void validateConstructorArgument(
10261035
Class<?> constructorClass, String nestedPath, String name, @Nullable Object value) {
10271036

@@ -1293,7 +1302,6 @@ public interface NameResolver {
12931302
* Strategy for {@link #construct constructor binding} to look up the values
12941303
* to bind to a given constructor parameter.
12951304
*/
1296-
@FunctionalInterface
12971305
public interface ValueResolver {
12981306

12991307
/**
@@ -1305,6 +1313,13 @@ public interface ValueResolver {
13051313
*/
13061314
@Nullable
13071315
Object resolveValue(String name, Class<?> type);
1316+
1317+
/**
1318+
* Return the names of all property values.
1319+
* @since 6.1.2
1320+
*/
1321+
Set<String> getNames();
1322+
13081323
}
13091324

13101325

Diff for: spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java

+46-5
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
import java.beans.ConstructorProperties;
2020
import java.util.Map;
2121
import java.util.Optional;
22+
import java.util.Set;
2223

2324
import jakarta.validation.constraints.NotNull;
2425
import org.junit.jupiter.api.Test;
2526

2627
import org.springframework.core.ResolvableType;
2728
import org.springframework.format.support.DefaultFormattingConversionService;
29+
import org.springframework.lang.Nullable;
2830
import org.springframework.util.Assert;
2931

3032
import static org.assertj.core.api.Assertions.assertThat;
@@ -76,6 +78,17 @@ void dataClassBindingWithMissingParameter() {
7678
assertThat(bindingResult.getFieldValue("param3")).isNull();
7779
}
7880

81+
@Test // gh-31821
82+
void dataClassBindingWithNestedOptionalParameterWithMissingParameter() {
83+
MapValueResolver valueResolver = new MapValueResolver(Map.of("param1", "value1"));
84+
DataBinder binder = initDataBinder(NestedDataClass.class);
85+
binder.construct(valueResolver);
86+
87+
NestedDataClass dataClass = getTarget(binder);
88+
assertThat(dataClass.param1()).isEqualTo("value1");
89+
assertThat(dataClass.nestedParam2()).isNull();
90+
}
91+
7992
@Test
8093
void dataClassBindingWithConversionError() {
8194
MapValueResolver valueResolver = new MapValueResolver(Map.of("param1", "value1", "param2", "x"));
@@ -90,7 +103,7 @@ void dataClassBindingWithConversionError() {
90103
}
91104

92105
@SuppressWarnings("SameParameterValue")
93-
private static DataBinder initDataBinder(Class<DataClass> targetType) {
106+
private static DataBinder initDataBinder(Class<?> targetType) {
94107
DataBinder binder = new DataBinder(null);
95108
binder.setTargetType(ResolvableType.forClass(targetType));
96109
binder.setConversionService(new DefaultFormattingConversionService());
@@ -137,17 +150,45 @@ public int param3() {
137150
}
138151

139152

153+
private static class NestedDataClass {
154+
155+
private final String param1;
156+
157+
@Nullable
158+
private final DataClass nestedParam2;
159+
160+
public NestedDataClass(String param1, @Nullable DataClass nestedParam2) {
161+
this.param1 = param1;
162+
this.nestedParam2 = nestedParam2;
163+
}
164+
165+
public String param1() {
166+
return this.param1;
167+
}
168+
169+
@Nullable
170+
public DataClass nestedParam2() {
171+
return this.nestedParam2;
172+
}
173+
}
174+
175+
140176
private static class MapValueResolver implements DataBinder.ValueResolver {
141177

142-
private final Map<String, Object> values;
178+
private final Map<String, Object> map;
143179

144-
private MapValueResolver(Map<String, Object> values) {
145-
this.values = values;
180+
private MapValueResolver(Map<String, Object> map) {
181+
this.map = map;
146182
}
147183

148184
@Override
149185
public Object resolveValue(String name, Class<?> type) {
150-
return values.get(name);
186+
return map.get(name);
187+
}
188+
189+
@Override
190+
public Set<String> getNames() {
191+
return this.map.keySet();
151192
}
152193
}
153194

Diff for: spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java

+23
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
package org.springframework.web.bind;
1818

1919
import java.lang.reflect.Array;
20+
import java.util.Enumeration;
21+
import java.util.LinkedHashSet;
2022
import java.util.List;
23+
import java.util.Set;
2124

2225
import jakarta.servlet.ServletRequest;
2326
import jakarta.servlet.http.HttpServletRequest;
@@ -213,6 +216,9 @@ protected static class ServletRequestValueResolver implements ValueResolver {
213216

214217
private final WebDataBinder dataBinder;
215218

219+
@Nullable
220+
private Set<String> parameterNames;
221+
216222
protected ServletRequestValueResolver(ServletRequest request, WebDataBinder dataBinder) {
217223
this.request = request;
218224
this.dataBinder = dataBinder;
@@ -261,6 +267,23 @@ else if (isFormDataPost(this.request)) {
261267
}
262268
return null;
263269
}
270+
271+
@Override
272+
public Set<String> getNames() {
273+
if (this.parameterNames == null) {
274+
this.parameterNames = initParameterNames(this.request);
275+
}
276+
return this.parameterNames;
277+
}
278+
279+
protected Set<String> initParameterNames(ServletRequest request) {
280+
Set<String> set = new LinkedHashSet<>();
281+
Enumeration<String> enumeration = request.getParameterNames();
282+
while (enumeration.hasMoreElements()) {
283+
set.add(enumeration.nextElement());
284+
}
285+
return set;
286+
}
264287
}
265288

266289
}

Diff for: spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.List;
2020
import java.util.Map;
21+
import java.util.Set;
2122
import java.util.TreeMap;
2223

2324
import reactor.core.publisher.Mono;
@@ -164,6 +165,11 @@ private record MapValueResolver(Map<String, Object> map) implements ValueResolve
164165
public Object resolveValue(String name, Class<?> type) {
165166
return this.map.get(name);
166167
}
168+
169+
@Override
170+
public Set<String> getNames() {
171+
return this.map.keySet();
172+
}
167173
}
168174

169175
}

Diff for: spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java

+11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.web.servlet.mvc.method.annotation;
1818

1919
import java.util.Map;
20+
import java.util.Set;
2021

2122
import jakarta.servlet.ServletRequest;
2223

@@ -121,6 +122,16 @@ protected Object getRequestParameter(String name, Class<?> type) {
121122
}
122123
return value;
123124
}
125+
126+
@Override
127+
protected Set<String> initParameterNames(ServletRequest request) {
128+
Set<String> set = super.initParameterNames(request);
129+
Map<String, String> uriVars = getUriVars(getRequest());
130+
if (uriVars != null) {
131+
set.addAll(uriVars.keySet());
132+
}
133+
return set;
134+
}
124135
}
125136

126137
}

0 commit comments

Comments
 (0)