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 1588056191f..00000000000
--- 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 2b349651dfb..41aafc07b61 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 @@ 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}.
- *
- *
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 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.
- * 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 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}.
*
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> 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 @@ 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 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 class MethodValidationAdapter {
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 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 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 a452e0f7724..470afa9d222 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 extends ConstraintViolation>> 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 da2658e7859..5b687087b82 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 @@ import org.springframework.validation.annotation.Validated;
*
* 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 @@ import org.springframework.validation.annotation.Validated;
*
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) {
- 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 class MethodValidationInterceptor implements MethodInterceptor {
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 @@ 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());
}
}
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 e0b14b865a4..4ba66ff5125 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 @@ 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)
*
*
+ * 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 class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvis
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 class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvis
* @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 33cc7b0433f..316dbc0a424 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 @@ 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.
- *
- * 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 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 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 d549422a5e2..1922383eded 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.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);
}
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 cae8ac5e713..5a3d91a90a9 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.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.
*
- * 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 d71dbdbf5d0..4091f0aec41 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 @@ 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.
*
- * 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 extends MessageSourceResolvable> 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 extends MessageSourceResolvable> 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 class ParameterValidationResult {
/**
* 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 class ParameterValidationResult {
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 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;
}
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 a9775e97779..f62eec9db46 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 @@ 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 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 41c5451fe17..3e7a1265fae 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.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.
+ *
+ * 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 @@ 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;
}
}
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 24dc1012fa6..21e4db3b6cb 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 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);
}
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 3547fd95d03..e730a07ad9b 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 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;
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 5b035cf23b2..a2649c6e527 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 @@ 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(
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 719ea21ae48..fb27dfad8b2 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.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(