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
This commit is contained in:
rstoyanchev 2023-06-30 14:22:41 +01:00
parent ba4d9a5230
commit a481c7649f
15 changed files with 429 additions and 434 deletions

View File

@ -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);
}
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.validation.beanvalidation;
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.MethodParameter;
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.MessageCodesResolver;
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}.
*
* <p>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<ParameterValidationResult> RESULT_COMPARATOR = new ResultComparator();
private static final MethodValidationResult EMPTY_RESULT = new EmptyMethodValidationResult();
private final Supplier<Validator> validator;
@ -158,13 +159,10 @@ public class MethodValidationAdapter {
}
/**
* 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.
* <p>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.
* <p>If not configured, {@link #createBindingResult} determines the name.
*/
public void setBindingResultNameResolver(BindingResultNameResolver nameResolver) {
this.objectNameResolver = nameResolver;
@ -172,18 +170,14 @@ public class MethodValidationAdapter {
/**
* 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}.
* <p>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 class MethodValidationAdapter {
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<ConstraintViolation<Object>> 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<ConstraintViolation<Object>> invokeValidatorForArguments(
Object target, Method method, Object[] arguments, Class<?>[] groups) {
ExecutableValidator execVal = this.validator.get().forExecutables();
Set<ConstraintViolation<Object>> result;
Set<ConstraintViolation<Object>> 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) {
ExecutableValidator execVal = this.validator.get().forExecutables();
Set<ConstraintViolation<Object>> 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));
Set<ConstraintViolation<Object>> 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);
}
private MethodValidationException createException(
/**
* Invoke the validator, and return the resulting violations.
*/
public final Set<ConstraintViolation<Object>> invokeValidatorForReturnValue(
Object target, Method method, @Nullable Object returnValue, Class<?>[] groups) {
ExecutableValidator execVal = this.validator.get().forExecutables();
return execVal.validateReturnValue(target, method, returnValue, groups);
}
private MethodValidationResult adaptViolations(
Object target, Method method, Set<ConstraintViolation<Object>> violations,
Function<Integer, MethodParameter> parameterFunction, Function<Integer, Object> argumentFunction,
boolean forReturnValue) {
Function<Integer, MethodParameter> parameterFunction,
Function<Integer, Object> argumentFunction) {
Map<MethodParameter, ValueResultBuilder> parameterViolations = new LinkedHashMap<>();
Map<Path.Node, BeanResultBuilder> cascadedViolations = new LinkedHashMap<>();
@ -309,7 +306,7 @@ public class MethodValidationAdapter {
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 @@ public class MethodValidationAdapter {
private final List<MessageSourceResolvable> resolvableErrors = new ArrayList<>();
private final List<ConstraintViolation<Object>> violations = new ArrayList<>();
public ValueResultBuilder(Object target, MethodParameter parameter, @Nullable Object argument) {
this.target = target;
this.parameter = parameter;
@ -422,12 +417,10 @@ public class MethodValidationAdapter {
public void addViolation(ConstraintViolation<Object> 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 class MethodValidationAdapter {
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 @@ public class MethodValidationAdapter {
}
}
/**
* 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<ParameterValidationResult> allValidationResults;
private final boolean forReturnValue;
DefaultMethodValidationResult(Object target, Method method, List<ParameterValidationResult> 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<ParameterValidationResult> 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<ParameterValidationResult> getAllValidationResults() {
return Collections.emptyList();
}
@Override
public String toString() {
return "0 validation errors";
}
}
}

View File

@ -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 {
public class MethodValidationException extends RuntimeException implements MethodValidationResult {
private final Object target;
private final Method method;
private final List<ParameterValidationResult> allValidationResults;
private final boolean forReturnValue;
private final MethodValidationResult validationResult;
/**
* Package private constructor for {@link MethodValidationAdapter}.
*/
MethodValidationException(
Object target, Method method, boolean forReturnValue,
Set<? extends ConstraintViolation<?>> violations, List<ParameterValidationResult> results) {
super(violations);
Assert.notNull(violations, "'violations' is required");
Assert.notNull(results, "'results' is required");
this.target = target;
this.method = method;
this.allValidationResults = results;
this.forReturnValue = forReturnValue;
public MethodValidationException(MethodValidationResult validationResult) {
super(validationResult.toString());
Assert.notNull(validationResult, "MethodValidationResult is required");
this.validationResult = validationResult;
}
/**
* Private constructor copying from another {@code MethodValidationResult}.
*/
private MethodValidationException(MethodValidationResult other) {
this(other.getTarget(), other.getMethod(), other.isForReturnValue(),
other.getConstraintViolations(), other.getAllValidationResults());
}
// re-declare getConstraintViolations as NonNull
@Override
public Set<ConstraintViolation<?>> 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<ParameterValidationResult> getAllValidationResults() {
return this.allValidationResults;
return this.validationResult.getAllValidationResults();
}
@Override
public List<ParameterValidationResult> getValueResults() {
return this.allValidationResults.stream()
.filter(result -> !(result instanceof ParameterErrors))
.toList();
}
@Override
public List<ParameterErrors> 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));
}
/**
* 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());
}
}

View File

@ -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 @@ import org.springframework.validation.annotation.Validated;
*
* <p>E.g.: {@code public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)}
*
* <p>In case of validation errors, the interceptor can raise
* {@link ConstraintViolationException}, or adapt the violations to
* {@link MethodValidationResult} and raise {@link MethodValidationException}.
*
* <p>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 @@ import org.springframework.validation.annotation.Validated;
* <p>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 class MethodValidationInterceptor implements MethodInterceptor {
* @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 class MethodValidationInterceptor implements MethodInterceptor {
* @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 class MethodValidationInterceptor implements MethodInterceptor {
* @since 6.0
*/
public MethodValidationInterceptor(Supplier<Validator> 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> 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 class MethodValidationInterceptor implements MethodInterceptor {
Object target = getTarget(invocation);
Method method = invocation.getMethod();
Object[] arguments = invocation.getArguments();
Class<?>[] groups = determineValidationGroups(invocation);
MethodValidationResult result;
Set<ConstraintViolation<Object>> 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 @@ public class MethodValidationInterceptor implements MethodInterceptor {
*/
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());
}
}

View File

@ -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 @@ package org.springframework.validation.beanvalidation;
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 @@ import org.springframework.validation.annotation.Validated;
* public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)
* </pre>
*
* <p>In case of validation errors, the interceptor can raise
* {@link ConstraintViolationException}, or adapt the violations to
* {@link MethodValidationResult} and raise {@link MethodValidationException}.
*
* <p>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> validator = SingletonSupplier.of(() ->
Validation.buildDefaultValidatorFactory().getValidator());
private boolean adaptConstraintViolations;
/**
* Set the 'validated' annotation type.
@ -109,6 +117,18 @@ public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvis
this.validator = validatorProvider::getObject;
}
/**
* Whether to adapt {@link ConstraintViolation}s to {@link MethodValidationResult}.
* <p>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 class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvis
* @since 6.0
*/
protected Advice createMethodValidationAdvice(Supplier<Validator> validator) {
return new MethodValidationInterceptor(validator);
return new MethodValidationInterceptor(validator, this.adaptConstraintViolations);
}
}

View File

@ -18,22 +18,16 @@ package org.springframework.validation.beanvalidation;
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.
*
* <p>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<ConstraintViolation<?>> getConstraintViolations();
default List<? extends MessageSourceResolvable> 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<ParameterValidationResult> 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<ParameterValidationResult> getValueResults();
default List<ParameterValidationResult> 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<ParameterErrors> getBeanResults();
default List<ParameterErrors> getBeanResults() {
return getAllValidationResults().stream()
.filter(result -> result instanceof ParameterErrors)
.map(result -> (ParameterErrors) result)
.toList();
}
}

View File

@ -22,44 +22,78 @@ import org.springframework.core.MethodParameter;
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 @@ public interface MethodValidator {
* @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);
}

View File

@ -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.FieldError;
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.
*
* <p>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.
* <p>The base class method {@link #getResolvableErrors()} returns
* {@link Errors#getAllErrors()}, but this subclass provides access to the same
* as {@link FieldError}s.
*
* <p>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.
* <p>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<ConstraintViolation<Object>> 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;

View File

@ -19,8 +19,6 @@ package org.springframework.validation.beanvalidation;
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.Assert;
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.
* <ul>
* <li>For a constraints directly on a method parameter, each
* {@link ConstraintViolation} is adapted to {@link MessageSourceResolvable}.
* <li>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.
* <li>Validation errors directly on method parameter values are exposed as a
* list of {@link MessageSourceResolvable}s.
* <li>Nested validation errors on an Object method parameter are exposed as
* {@link org.springframework.validation.Errors} by the subclass
* {@link ParameterErrors}.
* </ul>
*
* @author Rossen Stoyanchev
@ -51,24 +47,18 @@ public class ParameterValidationResult {
private final List<MessageSourceResolvable> resolvableErrors;
private final List<ConstraintViolation<Object>> violations;
/**
* Create a {@code ParameterValidationResult}.
*/
public ParameterValidationResult(
MethodParameter methodParameter, @Nullable Object argument,
Collection<? extends MessageSourceResolvable> resolvableErrors,
Collection<ConstraintViolation<Object>> violations) {
MethodParameter param, @Nullable Object arg, Collection<? extends MessageSourceResolvable> errors) {
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);
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 class ParameterValidationResult {
/**
* List of {@link MessageSourceResolvable} representations adapted from the
* underlying {@link #getViolations() violations}.
* validation errors of the validation library.
* <ul>
* <li>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 class ParameterValidationResult {
return this.resolvableErrors;
}
/**
* The violations associated with the method parameter, in the same order
* as {@link #getResolvableErrors()}.
*/
public List<ConstraintViolation<Object>> getViolations() {
return this.violations;
}
@Override
public boolean equals(@Nullable Object other) {
@ -129,8 +111,7 @@ public class ParameterValidationResult {
}
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 class ParameterValidationResult {
int hashCode = super.hashCode();
hashCode = 29 * hashCode + getMethodParameter().hashCode();
hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getArgument());
hashCode = 29 * hashCode + (getViolations().hashCode());
return hashCode;
}

View File

@ -69,9 +69,8 @@ public class MethodValidationAdapterTests {
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 @@ public class MethodValidationAdapterTests {
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 @@ public class MethodValidationAdapterTests {
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 @@ public class MethodValidationAdapterTests {
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 @@ public class MethodValidationAdapterTests {
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 @@ public class MethodValidationAdapterTests {
});
}
private void validateArguments(
Object target, Method method, Object[] arguments, Consumer<MethodValidationResult> assertions) {
assertions.accept(
this.validationAdapter.validateMethodArguments(target, method, null, arguments, new Class<?>[0]));
private void testArgs(Object target, Method method, Object[] args, Consumer<MethodValidationResult> consumer) {
consumer.accept(this.validationAdapter.validateArguments(target, method, null, args, new Class<?>[0]));
}
private void validateReturnValue(
Object target, Method method, @Nullable Object returnValue, Consumer<MethodValidationResult> assertions) {
assertions.accept(
this.validationAdapter.validateMethodReturnValue(target, method, null, returnValue, new Class<?>[0]));
private void testReturnValue(Object target, Method method, @Nullable Object value, Consumer<MethodValidationResult> consumer) {
consumer.accept(this.validationAdapter.validateReturnValue(target, method, null, value, new Class<?>[0]));
}
private static void assertBeanResult(

View File

@ -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.core.ParameterNameDiscoverer;
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 @@ import org.springframework.web.bind.support.WebBindingInitializer;
/**
* {@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.
*
* <p>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 HandlerMethodValidator(MethodValidationAdapter adapter) {
super(adapter);
private final MethodValidationAdapter validationAdapter;
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 @@ public final class HandlerMethodValidator extends DefaultMethodValidator {
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 final class HandlerMethodValidator extends DefaultMethodValidator {
adapter.setMessageCodesResolver(codesResolver);
}
HandlerMethodValidator methodValidator = new HandlerMethodValidator(adapter);
adapter.setBindingResultNameResolver(methodValidator::determineObjectName);
adapter.setBindingResultNameResolver(HandlerMethodValidator::determineObjectName);
return methodValidator;
}
}

View File

@ -174,14 +174,14 @@ public class InvocableHandlerMethod extends HandlerMethod {
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);
}

View File

@ -159,7 +159,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
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;

View File

@ -207,7 +207,6 @@ public class MethodValidationTests {
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(

View File

@ -51,6 +51,7 @@ import org.springframework.web.bind.annotation.RestController;
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 @@ public class MethodValidationTests {
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(