diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java index 41aafc07b61..e7e4d9bf262 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java @@ -70,9 +70,11 @@ import org.springframework.validation.annotation.Validated; */ public class MethodValidationAdapter implements MethodValidator { - private static final Comparator RESULT_COMPARATOR = new ResultComparator(); + private static final ObjectNameResolver defaultObjectNameResolver = new DefaultObjectNameResolver(); - private static final MethodValidationResult EMPTY_RESULT = new EmptyMethodValidationResult(); + private static final Comparator resultComparator = new ResultComparator(); + + private static final MethodValidationResult emptyResult = new EmptyMethodValidationResult(); private final Supplier validator; @@ -83,8 +85,7 @@ public class MethodValidationAdapter implements MethodValidator { private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); - @Nullable - private BindingResultNameResolver objectNameResolver; + private ObjectNameResolver objectNameResolver = defaultObjectNameResolver; /** @@ -142,8 +143,10 @@ public class MethodValidationAdapter implements MethodValidator { } /** - * Set the ParameterNameDiscoverer to use to resolve method parameter names - * that is in turn used to create error codes for {@link MessageSourceResolvable}. + * Set the {@code ParameterNameDiscoverer} to discover method parameter names + * with to create error codes for {@link MessageSourceResolvable}. Used only + * when {@link MethodParameter}s are not passed into + * {@link #validateArguments} or {@link #validateReturnValue}. *

Default is {@link org.springframework.core.DefaultParameterNameDiscoverer}. */ public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { @@ -151,7 +154,7 @@ public class MethodValidationAdapter implements MethodValidator { } /** - * Return the {@link #setParameterNameDiscoverer(ParameterNameDiscoverer) configured} + * Return the {@link #setParameterNameDiscoverer configured} * {@code ParameterNameDiscoverer}. */ public ParameterNameDiscoverer getParameterNameDiscoverer() { @@ -159,12 +162,24 @@ public class MethodValidationAdapter implements MethodValidator { } /** - * Configure a resolver for the name of Object parameters with nested errors - * to allow matching the name used in the higher level programming model, - * e.g. {@code @ModelAttribute} in Spring MVC. - *

If not configured, {@link #createBindingResult} determines the name. + * Configure a resolver to determine the name of an {@code @Valid} method + * parameter to use for its {@link BindingResult}. This allows aligning with + * a higher level programming model such as to resolve the name of an + * {@code @ModelAttribute} method parameter in Spring MVC. + *

By default, the object name is resolved through: + *

    + *
  • {@link MethodParameter#getParameterName()} for input parameters + *
  • {@link Conventions#getVariableNameForReturnType(Method, Class, Object)} + * for a return type + *
+ * If a name cannot be determined, e.g. a return value with insufficient + * type information, then it defaults to one of: + *
    + *
  • {@code "{methodName}.arg{index}"} for input parameters + *
  • {@code "{methodName}.returnValue"} for a return type + *
*/ - public void setBindingResultNameResolver(BindingResultNameResolver nameResolver) { + public void setObjectNameResolver(ObjectNameResolver nameResolver) { this.objectNameResolver = nameResolver; } @@ -204,11 +219,11 @@ public class MethodValidationAdapter implements MethodValidator { invokeValidatorForArguments(target, method, arguments, groups); if (violations.isEmpty()) { - return EMPTY_RESULT; + return emptyResult; } return adaptViolations(target, method, violations, - i -> parameters != null ? parameters[i] : new MethodParameter(method, i), + i -> parameters != null ? parameters[i] : initMethodParameter(method, i), i -> arguments[i]); } @@ -242,11 +257,11 @@ public class MethodValidationAdapter implements MethodValidator { invokeValidatorForReturnValue(target, method, returnValue, groups); if (violations.isEmpty()) { - return EMPTY_RESULT; + return emptyResult; } return adaptViolations(target, method, violations, - i -> returnType != null ? returnType : new MethodParameter(method, -1), + i -> returnType != null ? returnType : initMethodParameter(method, -1), i -> returnValue); } @@ -284,7 +299,6 @@ public class MethodValidationAdapter implements MethodValidator { else { continue; } - parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); Object argument = argumentFunction.apply(parameter.getParameterIndex()); if (!itr.hasNext()) { @@ -304,18 +318,17 @@ public class MethodValidationAdapter implements MethodValidator { List validatonResultList = new ArrayList<>(); parameterViolations.forEach((parameter, builder) -> validatonResultList.add(builder.build())); cascadedViolations.forEach((node, builder) -> validatonResultList.add(builder.build())); - validatonResultList.sort(RESULT_COMPARATOR); + validatonResultList.sort(resultComparator); return new DefaultMethodValidationResult(target, method, validatonResultList); } - /** - * Create a {@link MessageSourceResolvable} for the given violation. - * @param target target of the method invocation to which validation was applied - * @param parameter the method parameter associated with the violation - * @param violation the violation - * @return the created {@code MessageSourceResolvable} - */ + private MethodParameter initMethodParameter(Method method, int index) { + MethodParameter parameter = new MethodParameter(method, index); + parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); + return parameter; + } + private MessageSourceResolvable createMessageSourceResolvable( Object target, MethodParameter parameter, ConstraintViolation violation) { @@ -331,47 +344,8 @@ public class MethodValidationAdapter implements MethodValidator { return new DefaultMessageSourceResolvable(codes, arguments, violation.getMessage()); } - /** - * Select an object name and create a {@link BindingResult} for the argument. - * You can configure a {@link #setBindingResultNameResolver(BindingResultNameResolver) - * bindingResultNameResolver} to determine in a way that matches the specific - * programming model, e.g. {@code @ModelAttribute} or {@code @RequestBody} arguments - * in Spring MVC. - *

By default, the name is based on the parameter name, or for a return type on - * {@link Conventions#getVariableNameForReturnType(Method, Class, Object)}. - *

If a name cannot be determined for any reason, e.g. a return value with - * insufficient type information, then {@code "{methodName}.arg{index}"} is used. - * @param parameter the method parameter - * @param argument the argument value - * @return the determined name - */ private BindingResult createBindingResult(MethodParameter parameter, @Nullable Object argument) { - String objectName = null; - if (this.objectNameResolver != null) { - objectName = this.objectNameResolver.resolveName(parameter, argument); - } - else { - if (parameter.getParameterIndex() != -1) { - objectName = parameter.getParameterName(); - } - else { - try { - Method method = parameter.getMethod(); - if (method != null) { - Class containingClass = parameter.getContainingClass(); - Class resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass); - objectName = Conventions.getVariableNameForReturnType(method, resolvedType, argument); - } - } - catch (IllegalArgumentException ex) { - // insufficient type information - } - } - } - if (objectName == null) { - int index = parameter.getParameterIndex(); - objectName = (parameter.getExecutable().getName() + (index != -1 ? ".arg" + index : "")); - } + String objectName = this.objectNameResolver.resolveName(parameter, argument); BeanPropertyBindingResult result = new BeanPropertyBindingResult(argument, objectName); result.setMessageCodesResolver(this.messageCodesResolver); return result; @@ -379,14 +353,15 @@ public class MethodValidationAdapter implements MethodValidator { /** - * Contract to determine the object name of an {@code @Valid} method parameter. + * Strategy to resolve the name of an {@code @Valid} method parameter to + * use for its {@link BindingResult}. */ - public interface BindingResultNameResolver { + public interface ObjectNameResolver { /** - * Determine the name for the given method parameter. + * Determine the name for the given method argument. * @param parameter the method parameter - * @param value the argument or return value + * @param value the argument value or return value * @return the name to use */ String resolveName(MethodParameter parameter, @Nullable Object value); @@ -484,6 +459,40 @@ public class MethodValidationAdapter implements MethodValidator { } + /** + * Default algorithm to select an object name, as described in + * {@link #setObjectNameResolver(ObjectNameResolver)}. + */ + private static class DefaultObjectNameResolver implements ObjectNameResolver { + + @Override + public String resolveName(MethodParameter parameter, @Nullable Object value) { + String objectName = null; + if (parameter.getParameterIndex() != -1) { + objectName = parameter.getParameterName(); + } + else { + try { + Method method = parameter.getMethod(); + if (method != null) { + Class containingClass = parameter.getContainingClass(); + Class resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass); + objectName = Conventions.getVariableNameForReturnType(method, resolvedType, value); + } + } + catch (IllegalArgumentException ex) { + // insufficient type information + } + } + if (objectName == null) { + int index = parameter.getParameterIndex(); + objectName = (parameter.getExecutable().getName() + (index != -1 ? ".arg" + index : ".returnValue")); + } + return objectName; + } + } + + /** * Comparator for validation results, sorted by method parameter index first, * also falling back on container indexes if necessary for cascaded diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java index f62eec9db46..b835c1e0707 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java @@ -101,7 +101,7 @@ public class MethodValidationAdapterTests { MyService target = new MyService(); Method method = getMethod(target, "addStudent"); - this.validationAdapter.setBindingResultNameResolver((parameter, value) -> "studentToAdd"); + this.validationAdapter.setObjectNameResolver((param, value) -> "studentToAdd"); testArgs(target, method, new Object[] {faustino1234, new Person("Joe"), 1}, ex -> { diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidator.java b/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidator.java index 36605ff4a54..77e8c3a3cb9 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidator.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidator.java @@ -36,8 +36,8 @@ import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.bind.support.WebBindingInitializer; /** - * {@link org.springframework.validation.beanvalidation.MethodValidator} for - * {@code @RequestMapping} methods. + * {@link org.springframework.validation.beanvalidation.MethodValidator} that + * uses Bean Validation to validate {@code @RequestMapping} method arguments. * *

Handles validation results by populating {@link BindingResult} method * arguments with errors from {@link MethodValidationResult#getBeanResults() @@ -49,6 +49,9 @@ import org.springframework.web.bind.support.WebBindingInitializer; */ public final class HandlerMethodValidator implements MethodValidator { + private static final MethodValidationAdapter.ObjectNameResolver objectNameResolver = new WebObjectNameResolver(); + + private final MethodValidationAdapter validationAdapter; @@ -119,43 +122,51 @@ public final class HandlerMethodValidator implements MethodValidator { return this.validationAdapter.validateReturnValue(target, method, returnType, returnValue, groups); } - private static String determineObjectName(MethodParameter param, @Nullable Object argument) { - if (param.hasParameterAnnotation(RequestBody.class) || param.hasParameterAnnotation(RequestPart.class)) { - return Conventions.getVariableNameForParameter(param); - } - else { - return (param.getParameterIndex() != -1 ? - ModelFactory.getNameForParameter(param) : - ModelFactory.getNameForReturnValue(argument, param)); - } - } - /** - * Static factory method to create a {@link HandlerMethodValidator} if Bean - * Validation is enabled in Spring MVC or WebFlux. + * Static factory method to create a {@link HandlerMethodValidator} when Bean + * Validation is enabled for use via {@link ConfigurableWebBindingInitializer}, + * for example in Spring MVC or WebFlux config. */ @Nullable public static MethodValidator from( - @Nullable WebBindingInitializer bindingInitializer, - @Nullable ParameterNameDiscoverer parameterNameDiscoverer) { + @Nullable WebBindingInitializer initializer, @Nullable ParameterNameDiscoverer paramNameDiscoverer) { - if (bindingInitializer instanceof ConfigurableWebBindingInitializer configurableInitializer) { + if (initializer instanceof ConfigurableWebBindingInitializer configurableInitializer) { if (configurableInitializer.getValidator() instanceof Validator validator) { MethodValidationAdapter adapter = new MethodValidationAdapter(validator); - if (parameterNameDiscoverer != null) { - adapter.setParameterNameDiscoverer(parameterNameDiscoverer); + if (paramNameDiscoverer != null) { + adapter.setParameterNameDiscoverer(paramNameDiscoverer); } MessageCodesResolver codesResolver = configurableInitializer.getMessageCodesResolver(); if (codesResolver != null) { adapter.setMessageCodesResolver(codesResolver); } HandlerMethodValidator methodValidator = new HandlerMethodValidator(adapter); - adapter.setBindingResultNameResolver(HandlerMethodValidator::determineObjectName); + adapter.setObjectNameResolver(objectNameResolver); return methodValidator; } } return null; } + + /** + * ObjectNameResolver for web controller methods. + */ + private static class WebObjectNameResolver implements MethodValidationAdapter.ObjectNameResolver { + + @Override + public String resolveName(MethodParameter param, @Nullable Object value) { + if (param.hasParameterAnnotation(RequestBody.class) || param.hasParameterAnnotation(RequestPart.class)) { + return Conventions.getVariableNameForParameter(param); + } + else { + return (param.getParameterIndex() != -1 ? + ModelFactory.getNameForParameter(param) : + ModelFactory.getNameForReturnValue(value, param)); + } + } + } + }