Skip to content

Commit 84e863f

Browse files
committed
Refactoring in MethodValidationAdapter
Extract the default logic for resolving the name of an @Valid parameter into an ObjectNameResolver, and use it when there isn't one configured. See gh-30644
1 parent 7a79da5 commit 84e863f

File tree

3 files changed

+111
-91
lines changed

3 files changed

+111
-91
lines changed

spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java

Lines changed: 78 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,11 @@
7070
*/
7171
public class MethodValidationAdapter implements MethodValidator {
7272

73-
private static final Comparator<ParameterValidationResult> RESULT_COMPARATOR = new ResultComparator();
73+
private static final ObjectNameResolver defaultObjectNameResolver = new DefaultObjectNameResolver();
7474

75-
private static final MethodValidationResult EMPTY_RESULT = new EmptyMethodValidationResult();
75+
private static final Comparator<ParameterValidationResult> resultComparator = new ResultComparator();
76+
77+
private static final MethodValidationResult emptyResult = new EmptyMethodValidationResult();
7678

7779

7880
private final Supplier<Validator> validator;
@@ -83,8 +85,7 @@ public class MethodValidationAdapter implements MethodValidator {
8385

8486
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
8587

86-
@Nullable
87-
private BindingResultNameResolver objectNameResolver;
88+
private ObjectNameResolver objectNameResolver = defaultObjectNameResolver;
8889

8990

9091
/**
@@ -142,29 +143,43 @@ public MessageCodesResolver getMessageCodesResolver() {
142143
}
143144

144145
/**
145-
* Set the ParameterNameDiscoverer to use to resolve method parameter names
146-
* that is in turn used to create error codes for {@link MessageSourceResolvable}.
146+
* Set the {@code ParameterNameDiscoverer} to discover method parameter names
147+
* with to create error codes for {@link MessageSourceResolvable}. Used only
148+
* when {@link MethodParameter}s are not passed into
149+
* {@link #validateArguments} or {@link #validateReturnValue}.
147150
* <p>Default is {@link org.springframework.core.DefaultParameterNameDiscoverer}.
148151
*/
149152
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
150153
this.parameterNameDiscoverer = parameterNameDiscoverer;
151154
}
152155

153156
/**
154-
* Return the {@link #setParameterNameDiscoverer(ParameterNameDiscoverer) configured}
157+
* Return the {@link #setParameterNameDiscoverer configured}
155158
* {@code ParameterNameDiscoverer}.
156159
*/
157160
public ParameterNameDiscoverer getParameterNameDiscoverer() {
158161
return this.parameterNameDiscoverer;
159162
}
160163

161164
/**
162-
* Configure a resolver for the name of Object parameters with nested errors
163-
* to allow matching the name used in the higher level programming model,
164-
* e.g. {@code @ModelAttribute} in Spring MVC.
165-
* <p>If not configured, {@link #createBindingResult} determines the name.
165+
* Configure a resolver to determine the name of an {@code @Valid} method
166+
* parameter to use for its {@link BindingResult}. This allows aligning with
167+
* a higher level programming model such as to resolve the name of an
168+
* {@code @ModelAttribute} method parameter in Spring MVC.
169+
* <p>By default, the object name is resolved through:
170+
* <ul>
171+
* <li>{@link MethodParameter#getParameterName()} for input parameters
172+
* <li>{@link Conventions#getVariableNameForReturnType(Method, Class, Object)}
173+
* for a return type
174+
* </ul>
175+
* If a name cannot be determined, e.g. a return value with insufficient
176+
* type information, then it defaults to one of:
177+
* <ul>
178+
* <li>{@code "{methodName}.arg{index}"} for input parameters
179+
* <li>{@code "{methodName}.returnValue"} for a return type
180+
* </ul>
166181
*/
167-
public void setBindingResultNameResolver(BindingResultNameResolver nameResolver) {
182+
public void setObjectNameResolver(ObjectNameResolver nameResolver) {
168183
this.objectNameResolver = nameResolver;
169184
}
170185

@@ -204,11 +219,11 @@ public final MethodValidationResult validateArguments(
204219
invokeValidatorForArguments(target, method, arguments, groups);
205220

206221
if (violations.isEmpty()) {
207-
return EMPTY_RESULT;
222+
return emptyResult;
208223
}
209224

210225
return adaptViolations(target, method, violations,
211-
i -> parameters != null ? parameters[i] : new MethodParameter(method, i),
226+
i -> parameters != null ? parameters[i] : initMethodParameter(method, i),
212227
i -> arguments[i]);
213228
}
214229

@@ -242,11 +257,11 @@ public final MethodValidationResult validateReturnValue(
242257
invokeValidatorForReturnValue(target, method, returnValue, groups);
243258

244259
if (violations.isEmpty()) {
245-
return EMPTY_RESULT;
260+
return emptyResult;
246261
}
247262

248263
return adaptViolations(target, method, violations,
249-
i -> returnType != null ? returnType : new MethodParameter(method, -1),
264+
i -> returnType != null ? returnType : initMethodParameter(method, -1),
250265
i -> returnValue);
251266
}
252267

@@ -284,7 +299,6 @@ else if (node.getKind().equals(ElementKind.RETURN_VALUE)) {
284299
else {
285300
continue;
286301
}
287-
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
288302

289303
Object argument = argumentFunction.apply(parameter.getParameterIndex());
290304
if (!itr.hasNext()) {
@@ -304,18 +318,17 @@ else if (node.getKind().equals(ElementKind.RETURN_VALUE)) {
304318
List<ParameterValidationResult> validatonResultList = new ArrayList<>();
305319
parameterViolations.forEach((parameter, builder) -> validatonResultList.add(builder.build()));
306320
cascadedViolations.forEach((node, builder) -> validatonResultList.add(builder.build()));
307-
validatonResultList.sort(RESULT_COMPARATOR);
321+
validatonResultList.sort(resultComparator);
308322

309323
return new DefaultMethodValidationResult(target, method, validatonResultList);
310324
}
311325

312-
/**
313-
* Create a {@link MessageSourceResolvable} for the given violation.
314-
* @param target target of the method invocation to which validation was applied
315-
* @param parameter the method parameter associated with the violation
316-
* @param violation the violation
317-
* @return the created {@code MessageSourceResolvable}
318-
*/
326+
private MethodParameter initMethodParameter(Method method, int index) {
327+
MethodParameter parameter = new MethodParameter(method, index);
328+
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
329+
return parameter;
330+
}
331+
319332
private MessageSourceResolvable createMessageSourceResolvable(
320333
Object target, MethodParameter parameter, ConstraintViolation<Object> violation) {
321334

@@ -331,62 +344,24 @@ private MessageSourceResolvable createMessageSourceResolvable(
331344
return new DefaultMessageSourceResolvable(codes, arguments, violation.getMessage());
332345
}
333346

334-
/**
335-
* Select an object name and create a {@link BindingResult} for the argument.
336-
* You can configure a {@link #setBindingResultNameResolver(BindingResultNameResolver)
337-
* bindingResultNameResolver} to determine in a way that matches the specific
338-
* programming model, e.g. {@code @ModelAttribute} or {@code @RequestBody} arguments
339-
* in Spring MVC.
340-
* <p>By default, the name is based on the parameter name, or for a return type on
341-
* {@link Conventions#getVariableNameForReturnType(Method, Class, Object)}.
342-
* <p>If a name cannot be determined for any reason, e.g. a return value with
343-
* insufficient type information, then {@code "{methodName}.arg{index}"} is used.
344-
* @param parameter the method parameter
345-
* @param argument the argument value
346-
* @return the determined name
347-
*/
348347
private BindingResult createBindingResult(MethodParameter parameter, @Nullable Object argument) {
349-
String objectName = null;
350-
if (this.objectNameResolver != null) {
351-
objectName = this.objectNameResolver.resolveName(parameter, argument);
352-
}
353-
else {
354-
if (parameter.getParameterIndex() != -1) {
355-
objectName = parameter.getParameterName();
356-
}
357-
else {
358-
try {
359-
Method method = parameter.getMethod();
360-
if (method != null) {
361-
Class<?> containingClass = parameter.getContainingClass();
362-
Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass);
363-
objectName = Conventions.getVariableNameForReturnType(method, resolvedType, argument);
364-
}
365-
}
366-
catch (IllegalArgumentException ex) {
367-
// insufficient type information
368-
}
369-
}
370-
}
371-
if (objectName == null) {
372-
int index = parameter.getParameterIndex();
373-
objectName = (parameter.getExecutable().getName() + (index != -1 ? ".arg" + index : ""));
374-
}
348+
String objectName = this.objectNameResolver.resolveName(parameter, argument);
375349
BeanPropertyBindingResult result = new BeanPropertyBindingResult(argument, objectName);
376350
result.setMessageCodesResolver(this.messageCodesResolver);
377351
return result;
378352
}
379353

380354

381355
/**
382-
* Contract to determine the object name of an {@code @Valid} method parameter.
356+
* Strategy to resolve the name of an {@code @Valid} method parameter to
357+
* use for its {@link BindingResult}.
383358
*/
384-
public interface BindingResultNameResolver {
359+
public interface ObjectNameResolver {
385360

386361
/**
387-
* Determine the name for the given method parameter.
362+
* Determine the name for the given method argument.
388363
* @param parameter the method parameter
389-
* @param value the argument or return value
364+
* @param value the argument value or return value
390365
* @return the name to use
391366
*/
392367
String resolveName(MethodParameter parameter, @Nullable Object value);
@@ -484,6 +459,40 @@ public ParameterErrors build() {
484459
}
485460

486461

462+
/**
463+
* Default algorithm to select an object name, as described in
464+
* {@link #setObjectNameResolver(ObjectNameResolver)}.
465+
*/
466+
private static class DefaultObjectNameResolver implements ObjectNameResolver {
467+
468+
@Override
469+
public String resolveName(MethodParameter parameter, @Nullable Object value) {
470+
String objectName = null;
471+
if (parameter.getParameterIndex() != -1) {
472+
objectName = parameter.getParameterName();
473+
}
474+
else {
475+
try {
476+
Method method = parameter.getMethod();
477+
if (method != null) {
478+
Class<?> containingClass = parameter.getContainingClass();
479+
Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass);
480+
objectName = Conventions.getVariableNameForReturnType(method, resolvedType, value);
481+
}
482+
}
483+
catch (IllegalArgumentException ex) {
484+
// insufficient type information
485+
}
486+
}
487+
if (objectName == null) {
488+
int index = parameter.getParameterIndex();
489+
objectName = (parameter.getExecutable().getName() + (index != -1 ? ".arg" + index : ".returnValue"));
490+
}
491+
return objectName;
492+
}
493+
}
494+
495+
487496
/**
488497
* Comparator for validation results, sorted by method parameter index first,
489498
* also falling back on container indexes if necessary for cascaded

spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ void validateArgumentWithCustomObjectName() {
101101
MyService target = new MyService();
102102
Method method = getMethod(target, "addStudent");
103103

104-
this.validationAdapter.setBindingResultNameResolver((parameter, value) -> "studentToAdd");
104+
this.validationAdapter.setObjectNameResolver((param, value) -> "studentToAdd");
105105

106106
testArgs(target, method, new Object[] {faustino1234, new Person("Joe"), 1}, ex -> {
107107

spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidator.java

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
import org.springframework.web.bind.support.WebBindingInitializer;
3737

3838
/**
39-
* {@link org.springframework.validation.beanvalidation.MethodValidator} for
40-
* {@code @RequestMapping} methods.
39+
* {@link org.springframework.validation.beanvalidation.MethodValidator} that
40+
* uses Bean Validation to validate {@code @RequestMapping} method arguments.
4141
*
4242
* <p>Handles validation results by populating {@link BindingResult} method
4343
* arguments with errors from {@link MethodValidationResult#getBeanResults()
@@ -49,6 +49,9 @@
4949
*/
5050
public final class HandlerMethodValidator implements MethodValidator {
5151

52+
private static final MethodValidationAdapter.ObjectNameResolver objectNameResolver = new WebObjectNameResolver();
53+
54+
5255
private final MethodValidationAdapter validationAdapter;
5356

5457

@@ -119,43 +122,51 @@ public MethodValidationResult validateReturnValue(Object target, Method method,
119122
return this.validationAdapter.validateReturnValue(target, method, returnType, returnValue, groups);
120123
}
121124

122-
private static String determineObjectName(MethodParameter param, @Nullable Object argument) {
123-
if (param.hasParameterAnnotation(RequestBody.class) || param.hasParameterAnnotation(RequestPart.class)) {
124-
return Conventions.getVariableNameForParameter(param);
125-
}
126-
else {
127-
return (param.getParameterIndex() != -1 ?
128-
ModelFactory.getNameForParameter(param) :
129-
ModelFactory.getNameForReturnValue(argument, param));
130-
}
131-
}
132-
133125

134126
/**
135-
* Static factory method to create a {@link HandlerMethodValidator} if Bean
136-
* Validation is enabled in Spring MVC or WebFlux.
127+
* Static factory method to create a {@link HandlerMethodValidator} when Bean
128+
* Validation is enabled for use via {@link ConfigurableWebBindingInitializer},
129+
* for example in Spring MVC or WebFlux config.
137130
*/
138131
@Nullable
139132
public static MethodValidator from(
140-
@Nullable WebBindingInitializer bindingInitializer,
141-
@Nullable ParameterNameDiscoverer parameterNameDiscoverer) {
133+
@Nullable WebBindingInitializer initializer, @Nullable ParameterNameDiscoverer paramNameDiscoverer) {
142134

143-
if (bindingInitializer instanceof ConfigurableWebBindingInitializer configurableInitializer) {
135+
if (initializer instanceof ConfigurableWebBindingInitializer configurableInitializer) {
144136
if (configurableInitializer.getValidator() instanceof Validator validator) {
145137
MethodValidationAdapter adapter = new MethodValidationAdapter(validator);
146-
if (parameterNameDiscoverer != null) {
147-
adapter.setParameterNameDiscoverer(parameterNameDiscoverer);
138+
if (paramNameDiscoverer != null) {
139+
adapter.setParameterNameDiscoverer(paramNameDiscoverer);
148140
}
149141
MessageCodesResolver codesResolver = configurableInitializer.getMessageCodesResolver();
150142
if (codesResolver != null) {
151143
adapter.setMessageCodesResolver(codesResolver);
152144
}
153145
HandlerMethodValidator methodValidator = new HandlerMethodValidator(adapter);
154-
adapter.setBindingResultNameResolver(HandlerMethodValidator::determineObjectName);
146+
adapter.setObjectNameResolver(objectNameResolver);
155147
return methodValidator;
156148
}
157149
}
158150
return null;
159151
}
160152

153+
154+
/**
155+
* ObjectNameResolver for web controller methods.
156+
*/
157+
private static class WebObjectNameResolver implements MethodValidationAdapter.ObjectNameResolver {
158+
159+
@Override
160+
public String resolveName(MethodParameter param, @Nullable Object value) {
161+
if (param.hasParameterAnnotation(RequestBody.class) || param.hasParameterAnnotation(RequestPart.class)) {
162+
return Conventions.getVariableNameForParameter(param);
163+
}
164+
else {
165+
return (param.getParameterIndex() != -1 ?
166+
ModelFactory.getNameForParameter(param) :
167+
ModelFactory.getNameForReturnValue(value, param));
168+
}
169+
}
170+
}
171+
161172
}

0 commit comments

Comments
 (0)