From a481c7649fdd09c7a937df4d5cbe406cc2096c5a Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 30 Jun 2023 14:22:41 +0100 Subject: [PATCH] Refactor to prepare for method validation handling To handle method validation errors in ResponseEntityExceptionHandler, MethodValidationException and associated types should not depend on Bean Validation. To that effect: 1. MethodValidationResult and ParameterValidationResult no longer make the underlying ConstraintViolation set available, and instead expose only the adapted validation errors (MessageSourceResolvable, Errors), analogous to what SpringValidatorAdapter does. And likewise MethodValidationException no longer extends ConstraintViolationException. 2. MethodValidationPostProcessor has a new property adaptConstraintViolations to decide whether to simply raise ConstraintViolationException, or otherwise to adapt the ConstraintViolations and raise MethodValidationException instead, with the former is the default for compatibility. 3. As a result, the MethodValidator contract can now expose methods that return MethodValidationResult, which provided more flexibility for handling, and it allows MethodValidationAdapter to implement MethodValidator directly. 4. Update Javadoc in method validation classes to reflect this shift, and use terminology consistent with Spring validation in classes without an explicit dependency on Bean Validation. See gh-30644 --- .../DefaultMethodValidator.java | 98 -------- .../MethodValidationAdapter.java | 228 ++++++++++++------ .../MethodValidationException.java | 107 +------- .../MethodValidationInterceptor.java | 66 +++-- .../MethodValidationPostProcessor.java | 24 +- .../MethodValidationResult.java | 72 +++--- .../beanvalidation/MethodValidator.java | 83 +++++-- .../beanvalidation/ParameterErrors.java | 29 +-- .../ParameterValidationResult.java | 50 ++-- .../MethodValidationAdapterTests.java | 29 +-- .../annotation/HandlerMethodValidator.java | 68 ++++-- .../support/InvocableHandlerMethod.java | 4 +- .../result/method/InvocableHandlerMethod.java | 2 +- .../annotation/MethodValidationTests.java | 1 - .../annotation/MethodValidationTests.java | 2 +- 15 files changed, 429 insertions(+), 434 deletions(-) delete mode 100644 spring-context/src/main/java/org/springframework/validation/beanvalidation/DefaultMethodValidator.java diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/DefaultMethodValidator.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/DefaultMethodValidator.java deleted file mode 100644 index 1588056191f1..000000000000 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/DefaultMethodValidator.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.validation.beanvalidation; - -import java.lang.reflect.Method; - -import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; - -/** - * Default implementation of {@link MethodValidator} that delegates to a - * {@link MethodValidationAdapter}. Also, convenient as a base class that allows - * handling of the validation result. - * - * @author Rossen Stoyanchev - * @since 6.1 - */ -public class DefaultMethodValidator implements MethodValidator { - - private final MethodValidationAdapter adapter; - - - public DefaultMethodValidator(MethodValidationAdapter adapter) { - this.adapter = adapter; - } - - - @Override - public Class[] determineValidationGroups(Object bean, Method method) { - return MethodValidationAdapter.determineValidationGroups(bean, method); - } - - @Override - public void validateArguments( - Object target, Method method, @Nullable MethodParameter[] parameters, - Object[] arguments, Class[] groups) { - - MethodValidationResult validationResult = - this.adapter.validateMethodArguments(target, method, parameters, arguments, groups); - - handleArgumentsResult(arguments, groups, validationResult); - } - - /** - * Subclasses can override this to handle the result of argument validation. - * By default, throws {@link MethodValidationException} if there are errors. - * @param arguments the candidate argument values to validate - * @param groups groups for validation determined via - * @param validationResult the result from validation - */ - protected void handleArgumentsResult( - Object[] arguments, Class[] groups, MethodValidationResult validationResult) { - - if (validationResult.hasViolations()) { - throw MethodValidationException.forResult(validationResult); - } - } - - public void validateReturnValue( - Object target, Method method, @Nullable MethodParameter returnType, - @Nullable Object returnValue, Class[] groups) { - - MethodValidationResult validationResult = - this.adapter.validateMethodReturnValue(target, method, returnType, returnValue, groups); - - handleReturnValueResult(returnValue, groups, validationResult); - } - - /** - * Subclasses can override this to handle the result of return value validation. - * By default, throws {@link MethodValidationException} if there are errors. - * @param returnValue the return value to validate - * @param groups groups for validation determined via - * @param validationResult the result from validation - */ - protected void handleReturnValueResult( - @Nullable Object returnValue, Class[] groups, MethodValidationResult validationResult) { - - if (validationResult.hasViolations()) { - throw MethodValidationException.forResult(validationResult); - } - } - -} 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 2b349651dfb0..41aafc07b614 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 @@ -18,6 +18,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashMap; @@ -49,6 +50,7 @@ import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.function.SingletonSupplier; import org.springframework.validation.BeanPropertyBindingResult; @@ -59,20 +61,19 @@ import org.springframework.validation.annotation.Validated; /** - * Assist with applying method-level validation via - * {@link jakarta.validation.Validator}, adapt each resulting - * {@link ConstraintViolation} to {@link ParameterValidationResult}, and - * raise {@link MethodValidationException}. - * - *

Used by {@link MethodValidationInterceptor}. + * {@link MethodValidator} that uses a Bean Validation + * {@link jakarta.validation.Validator} for validation, and adapts + * {@link ConstraintViolation}s to {@link MethodValidationResult}. * * @author Rossen Stoyanchev * @since 6.1 */ -public class MethodValidationAdapter { +public class MethodValidationAdapter implements MethodValidator { private static final Comparator RESULT_COMPARATOR = new ResultComparator(); + private static final MethodValidationResult EMPTY_RESULT = new EmptyMethodValidationResult(); + private final Supplier validator; @@ -158,13 +159,10 @@ public ParameterNameDiscoverer getParameterNameDiscoverer() { } /** - * Configure a resolver for {@link BindingResult} method parameters to match - * the behavior of the higher level programming model, e.g. how the name of - * {@code @ModelAttribute} or {@code @RequestBody} is determined in Spring MVC. - *

If this is not configured, then {@link #createBindingResult} will apply - * default behavior to resolve the name to use. - * behavior applies. - * @param nameResolver the resolver to use + * 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. */ public void setBindingResultNameResolver(BindingResultNameResolver nameResolver) { this.objectNameResolver = nameResolver; @@ -172,18 +170,14 @@ public void setBindingResultNameResolver(BindingResultNameResolver nameResolver) /** - * Use this method determine the validation groups to pass into - * {@link #validateMethodArguments(Object, Method, MethodParameter[], Object[], Class[])} and - * {@link #validateMethodReturnValue(Object, Method, MethodParameter, Object, Class[])}. + * {@inheritDoc}. *

Default are the validation groups as specified in the {@link Validated} * annotation on the method, or on the containing target class of the method, * or for an AOP proxy without a target (with all behavior in advisors), also * check on proxied interfaces. - * @param target the target Object - * @param method the target method - * @return the applicable validation groups as a {@code Class} array */ - public static Class[] determineValidationGroups(Object target, Method method) { + @Override + public Class[] determineValidationGroups(Object target, Method method) { Validated validatedAnn = AnnotationUtils.findAnnotation(method, Validated.class); if (validatedAnn == null) { if (AopUtils.isAopProxy(target)) { @@ -201,72 +195,75 @@ public static Class[] determineValidationGroups(Object target, Method method) return (validatedAnn != null ? validatedAnn.value() : new Class[0]); } - /** - * Validate the given method arguments and return the result of validation. - * @param target the target Object - * @param method the target method - * @param parameters the parameters, if already created and available - * @param arguments the candidate argument values to validate - * @param groups groups for validation determined via - * {@link #determineValidationGroups(Object, Method)} - * @return a result with {@link ConstraintViolation violations} and - * {@link ParameterValidationResult validationResults}, both possibly empty - * in case there are no violations - */ - public MethodValidationResult validateMethodArguments( + @Override + public final MethodValidationResult validateArguments( Object target, Method method, @Nullable MethodParameter[] parameters, Object[] arguments, Class[] groups) { + Set> violations = + invokeValidatorForArguments(target, method, arguments, groups); + + if (violations.isEmpty()) { + return EMPTY_RESULT; + } + + return adaptViolations(target, method, violations, + i -> parameters != null ? parameters[i] : new MethodParameter(method, i), + i -> arguments[i]); + } + + /** + * Invoke the validator, and return the resulting violations. + */ + public final Set> invokeValidatorForArguments( + Object target, Method method, Object[] arguments, Class[] groups) { + ExecutableValidator execVal = this.validator.get().forExecutables(); - Set> result; + Set> violations; try { - result = execVal.validateParameters(target, method, arguments, groups); + violations = execVal.validateParameters(target, method, arguments, groups); } catch (IllegalArgumentException ex) { // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011 // Let's try to find the bridged method on the implementation class... Method mostSpecificMethod = ClassUtils.getMostSpecificMethod(method, target.getClass()); Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(mostSpecificMethod); - result = execVal.validateParameters(target, bridgedMethod, arguments, groups); + violations = execVal.validateParameters(target, bridgedMethod, arguments, groups); } - return (result.isEmpty() ? - MethodValidationException.forEmptyResult(target, method, true) : - createException(target, method, result, - i -> parameters != null ? parameters[i] : new MethodParameter(method, i), - i -> arguments[i], - false)); + return violations; } - /** - * Validate the given return value and return the result of validation. - * @param target the target Object - * @param method the target method - * @param returnType the return parameter, if already created and available - * @param returnValue the return value to validate - * @param groups groups for validation determined via - * {@link #determineValidationGroups(Object, Method)} - * @return a result with {@link ConstraintViolation violations} and - * {@link ParameterValidationResult validationResults}, both possibly empty - * in case there are no violations - */ - public MethodValidationResult validateMethodReturnValue( + @Override + public final MethodValidationResult validateReturnValue( Object target, Method method, @Nullable MethodParameter returnType, @Nullable Object returnValue, Class[] groups) { + Set> violations = + invokeValidatorForReturnValue(target, method, returnValue, groups); + + if (violations.isEmpty()) { + return EMPTY_RESULT; + } + + return adaptViolations(target, method, violations, + i -> returnType != null ? returnType : new MethodParameter(method, -1), + i -> returnValue); + } + + /** + * Invoke the validator, and return the resulting violations. + */ + public final Set> invokeValidatorForReturnValue( + Object target, Method method, @Nullable Object returnValue, Class[] groups) { + ExecutableValidator execVal = this.validator.get().forExecutables(); - Set> result = execVal.validateReturnValue(target, method, returnValue, groups); - return (result.isEmpty() ? - MethodValidationException.forEmptyResult(target, method, true) : - createException(target, method, result, - i -> returnType != null ? returnType : new MethodParameter(method, -1), - i -> returnValue, - true)); + return execVal.validateReturnValue(target, method, returnValue, groups); } - private MethodValidationException createException( + private MethodValidationResult adaptViolations( Object target, Method method, Set> violations, - Function parameterFunction, Function argumentFunction, - boolean forReturnValue) { + Function parameterFunction, + Function argumentFunction) { Map parameterViolations = new LinkedHashMap<>(); Map cascadedViolations = new LinkedHashMap<>(); @@ -309,7 +306,7 @@ else if (node.getKind().equals(ElementKind.RETURN_VALUE)) { cascadedViolations.forEach((node, builder) -> validatonResultList.add(builder.build())); validatonResultList.sort(RESULT_COMPARATOR); - return new MethodValidationException(target, method, forReturnValue, violations, validatonResultList); + return new DefaultMethodValidationResult(target, method, validatonResultList); } /** @@ -412,8 +409,6 @@ private final class ValueResultBuilder { private final List resolvableErrors = new ArrayList<>(); - private final List> violations = new ArrayList<>(); - public ValueResultBuilder(Object target, MethodParameter parameter, @Nullable Object argument) { this.target = target; this.parameter = parameter; @@ -422,12 +417,10 @@ public ValueResultBuilder(Object target, MethodParameter parameter, @Nullable Ob public void addViolation(ConstraintViolation violation) { this.resolvableErrors.add(createMessageSourceResolvable(this.target, this.parameter, violation)); - this.violations.add(violation); } public ParameterValidationResult build() { - return new ParameterValidationResult( - this.parameter, this.argument, this.resolvableErrors, this.violations); + return new ParameterValidationResult(this.parameter, this.argument, this.resolvableErrors); } } @@ -485,8 +478,8 @@ public void addViolation(ConstraintViolation violation) { public ParameterErrors build() { validatorAdapter.get().processConstraintViolations(this.violations, this.errors); return new ParameterErrors( - this.parameter, this.argument, this.errors, this.violations, - this.container, this.containerIndex, this.containerKey); + this.parameter, this.argument, this.errors, this.container, + this.containerIndex, this.containerKey); } } @@ -532,4 +525,91 @@ private int compareKeys(ParameterErrors errors1, ParameterErrors errors2) { } } + + /** + * Default {@link MethodValidationResult} implementation with non-zero errors. + */ + private static class DefaultMethodValidationResult implements MethodValidationResult { + + private final Object target; + + private final Method method; + + private final List allValidationResults; + + private final boolean forReturnValue; + + + DefaultMethodValidationResult(Object target, Method method, List results) { + Assert.notEmpty(results, "'results' is required and must not be empty"); + Assert.notNull(target, "'target' is required"); + Assert.notNull(method, "Method is required"); + this.target = target; + this.method = method; + this.allValidationResults = results; + this.forReturnValue = (results.get(0).getMethodParameter().getParameterIndex() == -1); + } + + + @Override + public Object getTarget() { + return this.target; + } + + @Override + public Method getMethod() { + return this.method; + } + + @Override + public boolean isForReturnValue() { + return this.forReturnValue; + } + + @Override + public List getAllValidationResults() { + return this.allValidationResults; + } + + + @Override + public String toString() { + return getAllErrors().size() + " validation errors " + + "for " + (isForReturnValue() ? "return value" : "arguments") + " of " + + this.method.toGenericString(); + } + } + + + /** + * {@link MethodValidationResult} for when there are no errors. + */ + private static class EmptyMethodValidationResult implements MethodValidationResult { + + @Override + public Object getTarget() { + throw new UnsupportedOperationException(); + } + + @Override + public Method getMethod() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isForReturnValue() { + throw new UnsupportedOperationException(); + } + + @Override + public List getAllValidationResults() { + return Collections.emptyList(); + } + + @Override + public String toString() { + return "0 validation errors"; + } + } + } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationException.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationException.java index a452e0f77248..470afa9d2221 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationException.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationException.java @@ -17,131 +17,48 @@ package org.springframework.validation.beanvalidation; import java.lang.reflect.Method; -import java.util.Collections; import java.util.List; -import java.util.Set; - -import jakarta.validation.ConstraintViolation; -import jakarta.validation.ConstraintViolationException; import org.springframework.util.Assert; /** - * Extension of {@link ConstraintViolationException} that implements - * {@link MethodValidationResult} exposing an additional list of - * {@link ParameterValidationResult} that represents violations adapted to - * {@link org.springframework.context.MessageSourceResolvable} and grouped by - * method parameter. + * Exception that is a {@link MethodValidationResult}. * * @author Rossen Stoyanchev * @since 6.1 - * @see ParameterValidationResult - * @see ParameterErrors - * @see MethodValidationAdapter + * @see MethodValidator */ @SuppressWarnings("serial") -public class MethodValidationException extends ConstraintViolationException implements MethodValidationResult { - - private final Object target; - - private final Method method; - - private final List allValidationResults; - - private final boolean forReturnValue; - - - /** - * Package private constructor for {@link MethodValidationAdapter}. - */ - MethodValidationException( - Object target, Method method, boolean forReturnValue, - Set> violations, List results) { +public class MethodValidationException extends RuntimeException implements MethodValidationResult { - super(violations); + private final MethodValidationResult validationResult; - Assert.notNull(violations, "'violations' is required"); - Assert.notNull(results, "'results' is required"); - this.target = target; - this.method = method; - this.allValidationResults = results; - this.forReturnValue = forReturnValue; - } - - /** - * Private constructor copying from another {@code MethodValidationResult}. - */ - private MethodValidationException(MethodValidationResult other) { - this(other.getTarget(), other.getMethod(), other.isForReturnValue(), - other.getConstraintViolations(), other.getAllValidationResults()); + public MethodValidationException(MethodValidationResult validationResult) { + super(validationResult.toString()); + Assert.notNull(validationResult, "MethodValidationResult is required"); + this.validationResult = validationResult; } - // re-declare getConstraintViolations as NonNull - - @Override - public Set> getConstraintViolations() { - return super.getConstraintViolations(); - } - @Override public Object getTarget() { - return this.target; + return this.validationResult.getTarget(); } @Override public Method getMethod() { - return this.method; + return this.validationResult.getMethod(); } @Override public boolean isForReturnValue() { - return this.forReturnValue; + return this.validationResult.isForReturnValue(); } @Override public List getAllValidationResults() { - return this.allValidationResults; - } - - @Override - public List getValueResults() { - return this.allValidationResults.stream() - .filter(result -> !(result instanceof ParameterErrors)) - .toList(); - } - - @Override - public List getBeanResults() { - return this.allValidationResults.stream() - .filter(result -> result instanceof ParameterErrors) - .map(result -> (ParameterErrors) result) - .toList(); - } - - @Override - public String toString() { - return "MethodValidationResult (" + getConstraintViolations().size() + " violations) " + - "for " + this.method.toGenericString(); - } - - - /** - * Create an exception copying from the given result, or return the same - * instance if it is a {@code MethodValidationException} already. - */ - public static MethodValidationException forResult(MethodValidationResult result) { - return (result instanceof MethodValidationException ex ? ex : new MethodValidationException(result)); + return this.validationResult.getAllValidationResults(); } - /** - * Create an exception for validation without errors. - */ - public static MethodValidationException forEmptyResult(Object target, Method method, boolean forReturnValue) { - return new MethodValidationException( - target, method, forReturnValue, Collections.emptySet(), Collections.emptyList()); - } - - } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java index da2658e78598..5b687087b829 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java @@ -17,8 +17,11 @@ package org.springframework.validation.beanvalidation; import java.lang.reflect.Method; +import java.util.Set; import java.util.function.Supplier; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; import org.aopalliance.intercept.MethodInterceptor; @@ -42,6 +45,10 @@ * *

E.g.: {@code public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)} * + *

In case of validation errors, the interceptor can raise + * {@link ConstraintViolationException}, or adapt the violations to + * {@link MethodValidationResult} and raise {@link MethodValidationException}. + * *

Validation groups can be specified through Spring's {@link Validated} annotation * at the type level of the containing target class, applying to all public service methods * of that class. By default, JSR-303 will validate against its default group only. @@ -49,20 +56,23 @@ *

As of Spring 5.0, this functionality requires a Bean Validation 1.1+ provider. * * @author Juergen Hoeller + * @author Rossen Stoyanchev * @since 3.1 * @see MethodValidationPostProcessor * @see jakarta.validation.executable.ExecutableValidator */ public class MethodValidationInterceptor implements MethodInterceptor { - private final MethodValidationAdapter delegate; + private final MethodValidationAdapter validationAdapter; + + private final boolean adaptViolations; /** * Create a new MethodValidationInterceptor using a default JSR-303 validator underneath. */ public MethodValidationInterceptor() { - this.delegate = new MethodValidationAdapter(); + this(new MethodValidationAdapter(), false); } /** @@ -70,7 +80,7 @@ public MethodValidationInterceptor() { * @param validatorFactory the JSR-303 ValidatorFactory to use */ public MethodValidationInterceptor(ValidatorFactory validatorFactory) { - this.delegate = new MethodValidationAdapter(validatorFactory); + this(new MethodValidationAdapter(validatorFactory), false); } /** @@ -78,7 +88,7 @@ public MethodValidationInterceptor(ValidatorFactory validatorFactory) { * @param validator the JSR-303 Validator to use */ public MethodValidationInterceptor(Validator validator) { - this.delegate = new MethodValidationAdapter(validator); + this(new MethodValidationAdapter(validator), false); } /** @@ -88,7 +98,25 @@ public MethodValidationInterceptor(Validator validator) { * @since 6.0 */ public MethodValidationInterceptor(Supplier validator) { - this.delegate = new MethodValidationAdapter(validator); + this(validator, false); + } + + /** + * Create a new MethodValidationInterceptor for the supplied + * (potentially lazily initialized) Validator. + * @param validator a Supplier for the Validator to use + * @param adaptViolations whether to adapt {@link ConstraintViolation}s, and + * if {@code true}, raise {@link MethodValidationException}, of if + * {@code false} raise {@link ConstraintViolationException} instead + * @since 6.1 + */ + public MethodValidationInterceptor(Supplier validator, boolean adaptViolations) { + this(new MethodValidationAdapter(validator), adaptViolations); + } + + private MethodValidationInterceptor(MethodValidationAdapter validationAdapter, boolean adaptViolations) { + this.validationAdapter = validationAdapter; + this.adaptViolations = adaptViolations; } @@ -102,20 +130,31 @@ public Object invoke(MethodInvocation invocation) throws Throwable { Object target = getTarget(invocation); Method method = invocation.getMethod(); + Object[] arguments = invocation.getArguments(); Class[] groups = determineValidationGroups(invocation); - MethodValidationResult result; + Set> violations; - result = this.delegate.validateMethodArguments(target, method, null, invocation.getArguments(), groups); - if (result.hasViolations()) { - throw MethodValidationException.forResult(result); + if (this.adaptViolations) { + this.validationAdapter.applyArgumentValidation(target, method, null, arguments, groups); + } + else { + violations = this.validationAdapter.invokeValidatorForArguments(target, method, arguments, groups); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } } Object returnValue = invocation.proceed(); - result = this.delegate.validateMethodReturnValue(target, method, null, returnValue, groups); - if (result.hasViolations()) { - throw MethodValidationException.forResult(result); + if (this.adaptViolations) { + this.validationAdapter.applyReturnValueValidation(target, method, null, arguments, groups); + } + else { + violations = this.validationAdapter.invokeValidatorForReturnValue(target, method, returnValue, groups); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } } return returnValue; @@ -162,8 +201,7 @@ else if (FactoryBean.class.isAssignableFrom(clazz)) { */ protected Class[] determineValidationGroups(MethodInvocation invocation) { Object target = getTarget(invocation); - Method method = invocation.getMethod(); - return MethodValidationAdapter.determineValidationGroups(target, method); + return this.validationAdapter.determineValidationGroups(target, invocation.getMethod()); } } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationPostProcessor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationPostProcessor.java index e0b14b865a44..4ba66ff51254 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import java.lang.annotation.Annotation; import java.util.function.Supplier; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; @@ -47,6 +49,10 @@ * public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2) * * + *

In case of validation errors, the interceptor can raise + * {@link ConstraintViolationException}, or adapt the violations to + * {@link MethodValidationResult} and raise {@link MethodValidationException}. + * *

Target classes with such annotated methods need to be annotated with Spring's * {@link Validated} annotation at the type level, for their methods to be searched for * inline constraint annotations. Validation groups can be specified through {@code @Validated} @@ -68,6 +74,8 @@ public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvis private Supplier validator = SingletonSupplier.of(() -> Validation.buildDefaultValidatorFactory().getValidator()); + private boolean adaptConstraintViolations; + /** * Set the 'validated' annotation type. @@ -109,6 +117,18 @@ public void setValidatorProvider(ObjectProvider validatorProvider) { this.validator = validatorProvider::getObject; } + /** + * Whether to adapt {@link ConstraintViolation}s to {@link MethodValidationResult}. + *

By default {@code false} in which case + * {@link jakarta.validation.ConstraintViolationException} is raised in case of + * violations. When set to {@code true}, {@link MethodValidationException} + * is raised instead with the method validation results. + * @since 6.1 + */ + public void setAdaptConstraintViolations(boolean adaptViolations) { + this.adaptConstraintViolations = adaptViolations; + } + @Override public void afterPropertiesSet() { @@ -125,7 +145,7 @@ public void afterPropertiesSet() { * @since 6.0 */ protected Advice createMethodValidationAdvice(Supplier validator) { - return new MethodValidationInterceptor(validator); + return new MethodValidationInterceptor(validator, this.adaptConstraintViolations); } } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationResult.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationResult.java index 33cc7b0433fa..316dbc0a4242 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationResult.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationResult.java @@ -18,22 +18,16 @@ import java.lang.reflect.Method; import java.util.List; -import java.util.Set; -import jakarta.validation.ConstraintViolation; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.validation.Errors; /** - * Container for method validation results where underlying - * {@link ConstraintViolation violations} have been adapted to - * {@link ParameterValidationResult} each containing a list of - * {@link org.springframework.context.MessageSourceResolvable} grouped by method - * parameter. - * - *

For {@link jakarta.validation.Valid @Valid}-annotated, Object method - * parameters or return types with cascaded violations, the {@link ParameterErrors} - * subclass of {@link ParameterValidationResult} implements - * {@link org.springframework.validation.Errors} and exposes - * {@link org.springframework.validation.FieldError field errors}. + * Container for method validation results with validation errors from the + * underlying library adapted to {@link MessageSourceResolvable}s and grouped + * by method parameter as {@link ParameterValidationResult}. For method parameters + * with nested validation errors, the validation result is of type + * {@link ParameterErrors} and implements {@link Errors}. * * @author Rossen Stoyanchev * @since 6.1 @@ -58,43 +52,53 @@ public interface MethodValidationResult { boolean isForReturnValue(); /** - * Whether the result contains any {@link ConstraintViolation}s. + * Whether the result contains any validation errors. */ - default boolean hasViolations() { - return !getConstraintViolations().isEmpty(); + default boolean hasErrors() { + return !getAllValidationResults().isEmpty(); } /** - * Returns the set of constraint violations reported during a validation. - * @return the {@code Set} of {@link ConstraintViolation}s, or an empty Set + * Return a single list with all errors from all validation results. + * @see #getAllValidationResults() + * @see ParameterValidationResult#getResolvableErrors() */ - Set> getConstraintViolations(); + default List getAllErrors() { + return getAllValidationResults().stream() + .flatMap(result -> result.getResolvableErrors().stream()) + .toList(); + } /** - * Return all validation results. This includes method parameters with - * constraints declared on them, as well as - * {@link jakarta.validation.Valid @Valid} method parameters with - * cascaded constraints. + * Return all validation results. This includes both method parameters with + * errors directly on them, and Object method parameters with nested errors + * on their fields and properties. * @see #getValueResults() * @see #getBeanResults() */ List getAllValidationResults(); /** - * Return only validation results for method parameters with constraints - * declared directly on them. This excludes - * {@link jakarta.validation.Valid @Valid} method parameters with cascaded - * constraints. - * @see #getAllValidationResults() + * Return only validation results for method parameters with errors directly + * on them. This does not include Object method parameters with nested + * errors on their fields and properties. */ - List getValueResults(); + default List getValueResults() { + return getAllValidationResults().stream() + .filter(result -> !(result instanceof ParameterErrors)) + .toList(); + } /** - * Return only validation results for {@link jakarta.validation.Valid @Valid} - * method parameters with cascaded constraints. This excludes method - * parameters with constraints declared directly on them. - * @see #getAllValidationResults() + * Return only validation results for Object method parameters with nested + * errors on their fields and properties. This excludes method parameters + * with errors directly on them. */ - List getBeanResults(); + default List getBeanResults() { + return getAllValidationResults().stream() + .filter(result -> result instanceof ParameterErrors) + .map(result -> (ParameterErrors) result) + .toList(); + } } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidator.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidator.java index d549422a5e2a..1922383ededb 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidator.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidator.java @@ -22,44 +22,78 @@ import org.springframework.lang.Nullable; /** - * Contract to apply method validation without directly using - * {@link MethodValidationAdapter}. For use in components where Jakarta Bean - * Validation is an optional dependency and may or may not be present on the - * classpath. If that's not a concern, use {@code MethodValidationAdapter} - * directly. + * Contract to apply method validation and handle the results. + * Exposes methods that return {@link MethodValidationResult}, and methods that + * handle the results, by default raising {@link MethodValidationException}. * * @author Rossen Stoyanchev * @since 6.1 - * @see DefaultMethodValidator */ public interface MethodValidator { /** - * Use this method determine the validation groups to pass into - * {@link #validateArguments(Object, Method, MethodParameter[], Object[], Class[])} and - * {@link #validateReturnValue(Object, Method, MethodParameter, Object, Class[])}. + * Use this method to determine the validation groups. * @param target the target Object * @param method the target method * @return the applicable validation groups as a {@code Class} array - * @see MethodValidationAdapter#determineValidationGroups(Object, Method) */ Class[] determineValidationGroups(Object target, Method method); /** - * Validate the given method arguments and return the result of validation. + * Validate the given method arguments and handle the result. * @param target the target Object * @param method the target method * @param parameters the parameters, if already created and available * @param arguments the candidate argument values to validate - * @param groups groups for validation determined via - * {@link #determineValidationGroups(Object, Method)} - * @throws MethodValidationException should be raised in case of validation - * errors unless the implementation handles those errors otherwise (e.g. - * by injecting {@code BindingResult} into the method). + * @param groups validation groups via {@link #determineValidationGroups} + * @throws MethodValidationException raised by default in case of validation errors. + * Implementations may provide alternative handling, possibly not raise an exception + * but for example inject errors into the method, or raise a different exception, + * one that also implements {@link MethodValidationResult}. */ - void validateArguments( - Object target, Method method, @Nullable MethodParameter[] parameters, Object[] arguments, - Class[] groups); + default void applyArgumentValidation( + Object target, Method method, @Nullable MethodParameter[] parameters, + Object[] arguments, Class[] groups) { + + MethodValidationResult result = validateArguments(target, method, parameters, arguments, groups); + if (result.hasErrors()) { + throw new MethodValidationException(result); + } + } + + /** + * Validate the given method arguments and return validation results. + * @param target the target Object + * @param method the target method + * @param parameters the parameters, if already created and available + * @param arguments the candidate argument values to validate + * @param groups validation groups from {@link #determineValidationGroups} + * @return the result of validation + */ + MethodValidationResult validateArguments( + Object target, Method method, @Nullable MethodParameter[] parameters, + Object[] arguments, Class[] groups); + + /** + * Validate the given return value and handle the results. + * @param target the target Object + * @param method the target method + * @param returnType the return parameter, if already created and available + * @param returnValue the return value to validate + * @param groups validation groups from {@link #determineValidationGroups} + * @throws MethodValidationException raised by default in case of validation errors. + * Implementations may provide alternative handling, or raise a different exception, + * one that also implements {@link MethodValidationResult}. + */ + default void applyReturnValueValidation( + Object target, Method method, @Nullable MethodParameter returnType, + @Nullable Object returnValue, Class[] groups) { + + MethodValidationResult result = validateReturnValue(target, method, returnType, returnValue, groups); + if (result.hasErrors()) { + throw new MethodValidationException(result); + } + } /** * Validate the given return value and return the result of validation. @@ -67,12 +101,11 @@ void validateArguments( * @param method the target method * @param returnType the return parameter, if already created and available * @param returnValue the return value to validate - * @param groups groups for validation determined via - * {@link #determineValidationGroups(Object, Method)} - * @throws MethodValidationException in case of validation errors + * @param groups validation groups from {@link #determineValidationGroups} + * @return the result of validation */ - void validateReturnValue( - Object target, Method method, @Nullable MethodParameter returnType, @Nullable Object returnValue, - Class[] groups); + MethodValidationResult validateReturnValue( + Object target, Method method, @Nullable MethodParameter returnType, + @Nullable Object returnValue, Class[] groups); } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/ParameterErrors.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/ParameterErrors.java index cae8ac5e713b..5a3d91a90a9e 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/ParameterErrors.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/ParameterErrors.java @@ -16,11 +16,8 @@ package org.springframework.validation.beanvalidation; -import java.util.Collection; import java.util.List; -import jakarta.validation.ConstraintViolation; - import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; import org.springframework.validation.Errors; @@ -28,20 +25,18 @@ import org.springframework.validation.ObjectError; /** - * Extension of {@link ParameterValidationResult} that's created for Object - * method arguments or return values with cascaded violations on their properties. - * Such method parameters are annotated with {@link jakarta.validation.Valid @Valid}, - * or in the case of return values, the annotation is on the method. + * Extension of {@link ParameterValidationResult} created for Object method + * parameters or return values with nested errors on their properties. * - *

In addition to the (generic) {@link #getResolvableErrors() - * MessageSourceResolvable errors} from the base class, this subclass implements - * {@link Errors} to expose convenient access to the same as {@link FieldError}s. + *

The base class method {@link #getResolvableErrors()} returns + * {@link Errors#getAllErrors()}, but this subclass provides access to the same + * as {@link FieldError}s. * - *

When {@code @Valid} is declared on a {@link List} or {@link java.util.Map} - * parameter, a separate {@link ParameterErrors} is created for each list or map - * value for which there are constraint violations. In such cases, the - * {@link #getContainer()} is the list or map, while {@link #getContainerIndex()} - * and {@link #getContainerKey()} reflect the index or key of the value. + *

When the method parameter is a {@link List} or {@link java.util.Map}, + * a separate {@link ParameterErrors} is created for each list or map value for + * which there are validation errors. In such cases, the {@link #getContainer()} + * method returns the list or map, while {@link #getContainerIndex()} + * and {@link #getContainerKey()} return the value index or key. * * @author Rossen Stoyanchev * @since 6.1 @@ -65,11 +60,9 @@ public class ParameterErrors extends ParameterValidationResult implements Errors */ public ParameterErrors( MethodParameter parameter, @Nullable Object argument, Errors errors, - Collection> violations, @Nullable Object container, @Nullable Integer index, @Nullable Object key) { - super(parameter, argument, errors.getAllErrors(), violations); - + super(parameter, argument, errors.getAllErrors()); this.errors = errors; this.container = container; this.containerIndex = index; diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/ParameterValidationResult.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/ParameterValidationResult.java index d71dbdbf5d0e..4091f0aec41a 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/ParameterValidationResult.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/ParameterValidationResult.java @@ -19,8 +19,6 @@ import java.util.Collection; import java.util.List; -import jakarta.validation.ConstraintViolation; - import org.springframework.context.MessageSourceResolvable; import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; @@ -28,15 +26,13 @@ import org.springframework.util.ObjectUtils; /** - * Store and expose the results of method validation via - * {@link jakarta.validation.Validator} for a specific method parameter. + * Store and expose the results of method validation for a method parameter. *

    - *
  • For a constraints directly on a method parameter, each - * {@link ConstraintViolation} is adapted to {@link MessageSourceResolvable}. - *
  • For cascaded constraints via {@link jakarta.validation.Validator @Valid} - * on a bean method parameter, {@link SpringValidatorAdapter} is used to initialize - * an {@link org.springframework.validation.Errors} with field errors, and create - * the {@link ParameterErrors} sub-class. + *
  • Validation errors directly on method parameter values are exposed as a + * list of {@link MessageSourceResolvable}s. + *
  • Nested validation errors on an Object method parameter are exposed as + * {@link org.springframework.validation.Errors} by the subclass + * {@link ParameterErrors}. *
* * @author Rossen Stoyanchev @@ -51,24 +47,18 @@ public class ParameterValidationResult { private final List resolvableErrors; - private final List> violations; - /** * Create a {@code ParameterValidationResult}. */ public ParameterValidationResult( - MethodParameter methodParameter, @Nullable Object argument, - Collection resolvableErrors, - Collection> violations) { - - Assert.notNull(methodParameter, "MethodParameter is required"); - Assert.notEmpty(resolvableErrors, "`resolvableErrors` must not be empty"); - Assert.notEmpty(violations, "'violations' must not be empty"); - this.methodParameter = methodParameter; - this.argument = argument; - this.resolvableErrors = List.copyOf(resolvableErrors); - this.violations = List.copyOf(violations); + MethodParameter param, @Nullable Object arg, Collection errors) { + + Assert.notNull(param, "MethodParameter is required"); + Assert.notEmpty(errors, "`resolvableErrors` must not be empty"); + this.methodParameter = param; + this.argument = arg; + this.resolvableErrors = List.copyOf(errors); } @@ -89,7 +79,7 @@ public Object getArgument() { /** * List of {@link MessageSourceResolvable} representations adapted from the - * underlying {@link #getViolations() violations}. + * validation errors of the validation library. *
    *
  • For a constraints directly on a method parameter, error codes are * based on the names of the constraint annotation, the object, the method, @@ -110,14 +100,6 @@ public List getResolvableErrors() { return this.resolvableErrors; } - /** - * The violations associated with the method parameter, in the same order - * as {@link #getResolvableErrors()}. - */ - public List> getViolations() { - return this.violations; - } - @Override public boolean equals(@Nullable Object other) { @@ -129,8 +111,7 @@ public boolean equals(@Nullable Object other) { } ParameterValidationResult otherResult = (ParameterValidationResult) other; return (getMethodParameter().equals(otherResult.getMethodParameter()) && - ObjectUtils.nullSafeEquals(getArgument(), otherResult.getArgument()) && - getViolations().equals(otherResult.getViolations())); + ObjectUtils.nullSafeEquals(getArgument(), otherResult.getArgument())); } @Override @@ -138,7 +119,6 @@ public int hashCode() { int hashCode = super.hashCode(); hashCode = 29 * hashCode + getMethodParameter().hashCode(); hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getArgument()); - hashCode = 29 * hashCode + (getViolations().hashCode()); return hashCode; } 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 a9775e977793..f62eec9db46d 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 @@ -69,9 +69,8 @@ void validateArguments() { MyService target = new MyService(); Method method = getMethod(target, "addStudent"); - validateArguments(target, method, new Object[] {faustino1234, cayetana6789, 3}, ex -> { + testArgs(target, method, new Object[] {faustino1234, cayetana6789, 3}, ex -> { - assertThat(ex.getConstraintViolations()).hasSize(3); assertThat(ex.getAllValidationResults()).hasSize(3); assertBeanResult(ex.getBeanResults().get(0), 0, "student", faustino1234, List.of(""" @@ -104,9 +103,8 @@ void validateArgumentWithCustomObjectName() { this.validationAdapter.setBindingResultNameResolver((parameter, value) -> "studentToAdd"); - validateArguments(target, method, new Object[] {faustino1234, new Person("Joe"), 1}, ex -> { + testArgs(target, method, new Object[] {faustino1234, new Person("Joe"), 1}, ex -> { - assertThat(ex.getConstraintViolations()).hasSize(1); assertThat(ex.getAllValidationResults()).hasSize(1); assertBeanResult(ex.getBeanResults().get(0), 0, "studentToAdd", faustino1234, List.of(""" @@ -122,9 +120,8 @@ void validateArgumentWithCustomObjectName() { void validateReturnValue() { MyService target = new MyService(); - validateReturnValue(target, getMethod(target, "getIntValue"), 4, ex -> { + testReturnValue(target, getMethod(target, "getIntValue"), 4, ex -> { - assertThat(ex.getConstraintViolations()).hasSize(1); assertThat(ex.getAllValidationResults()).hasSize(1); assertValueResult(ex.getValueResults().get(0), -1, 4, List.of(""" @@ -140,9 +137,8 @@ void validateReturnValue() { void validateReturnValueBean() { MyService target = new MyService(); - validateReturnValue(target, getMethod(target, "getPerson"), faustino1234, ex -> { + testReturnValue(target, getMethod(target, "getPerson"), faustino1234, ex -> { - assertThat(ex.getConstraintViolations()).hasSize(1); assertThat(ex.getAllValidationResults()).hasSize(1); assertBeanResult(ex.getBeanResults().get(0), -1, "person", faustino1234, List.of(""" @@ -159,9 +155,8 @@ void validateListArgument() { MyService target = new MyService(); Method method = getMethod(target, "addPeople"); - validateArguments(target, method, new Object[] {List.of(faustino1234, cayetana6789)}, ex -> { + testArgs(target, method, new Object[] {List.of(faustino1234, cayetana6789)}, ex -> { - assertThat(ex.getConstraintViolations()).hasSize(2); assertThat(ex.getAllValidationResults()).hasSize(2); int paramIndex = 0; @@ -184,18 +179,12 @@ void validateListArgument() { }); } - private void validateArguments( - Object target, Method method, Object[] arguments, Consumer assertions) { - - assertions.accept( - this.validationAdapter.validateMethodArguments(target, method, null, arguments, new Class[0])); + private void testArgs(Object target, Method method, Object[] args, Consumer consumer) { + consumer.accept(this.validationAdapter.validateArguments(target, method, null, args, new Class[0])); } - private void validateReturnValue( - Object target, Method method, @Nullable Object returnValue, Consumer assertions) { - - assertions.accept( - this.validationAdapter.validateMethodReturnValue(target, method, null, returnValue, new Class[0])); + private void testReturnValue(Object target, Method method, @Nullable Object value, Consumer consumer) { + consumer.accept(this.validationAdapter.validateReturnValue(target, method, null, value, new Class[0])); } private static void assertBeanResult( 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 41c5451fe170..3e7a1265fae0 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 @@ -16,6 +16,8 @@ package org.springframework.web.method.annotation; +import java.lang.reflect.Method; + import jakarta.validation.Validator; import org.springframework.core.Conventions; @@ -24,7 +26,6 @@ import org.springframework.lang.Nullable; import org.springframework.validation.BindingResult; import org.springframework.validation.MessageCodesResolver; -import org.springframework.validation.beanvalidation.DefaultMethodValidator; import org.springframework.validation.beanvalidation.MethodValidationAdapter; import org.springframework.validation.beanvalidation.MethodValidationException; import org.springframework.validation.beanvalidation.MethodValidationResult; @@ -37,27 +38,41 @@ /** * {@link org.springframework.validation.beanvalidation.MethodValidator} for - * use with {@code @RequestMapping} methods. Helps to determine object names - * and populates {@link BindingResult} method arguments with errors from - * {@link MethodValidationResult#getBeanResults() beanResults}. + * {@code @RequestMapping} methods. + * + *

    Handles validation results by populating {@link BindingResult} method + * arguments with errors from {@link MethodValidationResult#getBeanResults() + * beanResults}. Also, helps to determine parameter names for + * {@code @ModelAttribute} and {@code @RequestBody} parameters. * * @author Rossen Stoyanchev * @since 6.1 */ -public final class HandlerMethodValidator extends DefaultMethodValidator { +public final class HandlerMethodValidator implements MethodValidator { + + private final MethodValidationAdapter validationAdapter; - private HandlerMethodValidator(MethodValidationAdapter adapter) { - super(adapter); + + private HandlerMethodValidator(MethodValidationAdapter validationAdapter) { + this.validationAdapter = validationAdapter; } @Override - protected void handleArgumentsResult( - Object[] arguments, Class[] groups, MethodValidationResult result) { + public Class[] determineValidationGroups(Object target, Method method) { + return this.validationAdapter.determineValidationGroups(target, method); + } - if (result.getConstraintViolations().isEmpty()) { + @Override + public void applyArgumentValidation( + Object target, Method method, @Nullable MethodParameter[] parameters, + Object[] arguments, Class[] groups) { + + MethodValidationResult result = validateArguments(target, method, parameters, arguments, groups); + if (!result.hasErrors()) { return; } + if (!result.getBeanResults().isEmpty()) { int bindingResultCount = 0; for (ParameterErrors errors : result.getBeanResults()) { @@ -75,12 +90,37 @@ protected void handleArgumentsResult( return; } } - if (result.hasViolations()) { - throw MethodValidationException.forResult(result); + + throw new MethodValidationException(result); + } + + @Override + public MethodValidationResult validateArguments( + Object target, Method method, @Nullable MethodParameter[] parameters, + Object[] arguments, Class[] groups) { + + return this.validationAdapter.validateArguments(target, method, parameters, arguments, groups); + } + + @Override + public void applyReturnValueValidation( + Object target, Method method, @Nullable MethodParameter returnType, + @Nullable Object returnValue, Class[] groups) { + + MethodValidationResult result = validateReturnValue(target, method, returnType, returnValue, groups); + if (result.hasErrors()) { + throw new MethodValidationException(result); } } - private String determineObjectName(MethodParameter param, @Nullable Object argument) { + @Override + public MethodValidationResult validateReturnValue(Object target, Method method, + @Nullable MethodParameter returnType, @Nullable Object returnValue, Class[] groups) { + + 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); } @@ -112,7 +152,7 @@ public static MethodValidator from( adapter.setMessageCodesResolver(codesResolver); } HandlerMethodValidator methodValidator = new HandlerMethodValidator(adapter); - adapter.setBindingResultNameResolver(methodValidator::determineObjectName); + adapter.setBindingResultNameResolver(HandlerMethodValidator::determineObjectName); return methodValidator; } } diff --git a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java index 24dc1012fa6f..21e4db3b6cb8 100644 --- a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java @@ -174,14 +174,14 @@ public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewC Class[] groups = getValidationGroups(); if (shouldValidateArguments() && this.methodValidator != null) { - this.methodValidator.validateArguments( + this.methodValidator.applyArgumentValidation( getBean(), getBridgedMethod(), getMethodParameters(), args, groups); } Object returnValue = doInvoke(args); if (shouldValidateReturnValue() && this.methodValidator != null) { - this.methodValidator.validateReturnValue( + this.methodValidator.applyReturnValueValidation( getBean(), getBridgedMethod(), getReturnType(), returnValue, groups); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java index 3547fd95d03c..e730a07ad9ba 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java @@ -159,7 +159,7 @@ public Mono invoke( return getMethodArgumentValues(exchange, bindingContext, providedArgs).flatMap(args -> { Class[] groups = getValidationGroups(); if (shouldValidateArguments() && this.methodValidator != null) { - this.methodValidator.validateArguments( + this.methodValidator.applyArgumentValidation( getBean(), getBridgedMethod(), getMethodParameters(), args, groups); } Object value; diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MethodValidationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MethodValidationTests.java index 5b035cf23b23..a2649c6e5273 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MethodValidationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MethodValidationTests.java @@ -207,7 +207,6 @@ void modelAttributeWithBindingResultAndRequestHeader() { assertThat(this.jakartaValidator.getValidationCount()).isEqualTo(1); assertThat(this.jakartaValidator.getMethodValidationCount()).isEqualTo(1); - assertThat(ex.getConstraintViolations()).hasSize(2); assertThat(ex.getAllValidationResults()).hasSize(2); assertBeanResult(ex.getBeanResults().get(0), "student", Collections.singletonList( diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java index 719ea21ae482..fb27dfad8b20 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java @@ -51,6 +51,7 @@ import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.annotation.HandlerMethodValidationException; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.testfixture.method.ResolvableMethod; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; @@ -170,7 +171,6 @@ void modelAttributeWithBindingResultAndRequestHeader() { assertThat(this.jakartaValidator.getValidationCount()).isEqualTo(1); assertThat(this.jakartaValidator.getMethodValidationCount()).isEqualTo(1); - assertThat(ex.getConstraintViolations()).hasSize(2); assertThat(ex.getAllValidationResults()).hasSize(2); assertBeanResult(ex.getBeanResults().get(0), "student", Collections.singletonList(