parent
cb04c3b335
commit
bd054a4918
|
|
@ -24,6 +24,7 @@ import java.util.Collections;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
|
@ -164,6 +165,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
|
||||
private final List<Validator> validators = new ArrayList<>();
|
||||
|
||||
@Nullable
|
||||
private Predicate<Validator> excludedValidators;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new DataBinder instance, with default object name.
|
||||
|
|
@ -580,6 +584,14 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a predicate to exclude validators.
|
||||
* @since 6.1
|
||||
*/
|
||||
public void setExcludedValidators(Predicate<Validator> predicate) {
|
||||
this.excludedValidators = predicate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Validators to apply after each binding step.
|
||||
* @see #setValidator(Validator)
|
||||
|
|
@ -616,6 +628,18 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
return Collections.unmodifiableList(this.validators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Validators to apply after data binding. This includes the
|
||||
* configured {@link #getValidators() validators} filtered by the
|
||||
* {@link #setExcludedValidators(Predicate) exclude predicate}.
|
||||
* @since 6.1
|
||||
*/
|
||||
public List<Validator> getValidatorsToApply() {
|
||||
return (this.excludedValidators != null ?
|
||||
this.validators.stream().filter(validator -> !this.excludedValidators.test(validator)).toList() :
|
||||
Collections.unmodifiableList(this.validators));
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Implementation of PropertyEditorRegistry/TypeConverter interface
|
||||
|
|
@ -906,7 +930,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
Assert.state(target != null, "No target to validate");
|
||||
BindingResult bindingResult = getBindingResult();
|
||||
// Call each validator with the same binding result
|
||||
for (Validator validator : getValidators()) {
|
||||
for (Validator validator : getValidatorsToApply()) {
|
||||
validator.validate(target, bindingResult);
|
||||
}
|
||||
}
|
||||
|
|
@ -924,7 +948,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
|||
Assert.state(target != null, "No target to validate");
|
||||
BindingResult bindingResult = getBindingResult();
|
||||
// Call each validator with the same binding result
|
||||
for (Validator validator : getValidators()) {
|
||||
for (Validator validator : getValidatorsToApply()) {
|
||||
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator smartValidator) {
|
||||
smartValidator.validate(target, bindingResult, validationHints);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2021 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.
|
||||
|
|
@ -16,7 +16,11 @@
|
|||
|
||||
package org.springframework.web.bind.support;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.validation.DataBinder;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
|
||||
|
|
@ -32,6 +36,8 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory {
|
|||
@Nullable
|
||||
private final WebBindingInitializer initializer;
|
||||
|
||||
private boolean methodValidationApplicable;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@code DefaultDataBinderFactory} instance.
|
||||
|
|
@ -43,6 +49,17 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configure flag to signal whether validation will be applied to handler
|
||||
* method arguments, which is the case if Bean Validation is enabled in
|
||||
* Spring MVC, and method parameters have {@code @Constraint} annotations.
|
||||
* @since 6.1
|
||||
*/
|
||||
public void setMethodValidationApplicable(boolean methodValidationApplicable) {
|
||||
this.methodValidationApplicable = methodValidationApplicable;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@link WebDataBinder} for the given target object and
|
||||
* initialize it through a {@link WebBindingInitializer}.
|
||||
|
|
@ -87,4 +104,36 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}.
|
||||
* <p>By default, if the parameter has {@code @Valid}, Bean Validation is
|
||||
* excluded, deferring to method validation.
|
||||
*/
|
||||
@Override
|
||||
public WebDataBinder createBinder(
|
||||
NativeWebRequest webRequest, @Nullable Object target, String objectName,
|
||||
MethodParameter parameter) throws Exception {
|
||||
|
||||
WebDataBinder dataBinder = createBinder(webRequest, target, objectName);
|
||||
if (this.methodValidationApplicable) {
|
||||
MethodValidationInitializer.updateBinder(dataBinder, parameter);
|
||||
}
|
||||
return dataBinder;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Excludes Bean Validation if the method parameter has {@code @Valid}.
|
||||
*/
|
||||
private static class MethodValidationInitializer {
|
||||
|
||||
public static void updateBinder(DataBinder binder, MethodParameter parameter) {
|
||||
for (Annotation annotation : parameter.getParameterAnnotations()) {
|
||||
if (annotation.annotationType().getName().equals("jakarta.validation.Valid")) {
|
||||
binder.setExcludedValidators(validator -> validator instanceof jakarta.validation.Validator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2017 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.
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.springframework.web.bind.support;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
|
|
@ -24,6 +25,7 @@ import org.springframework.web.context.request.NativeWebRequest;
|
|||
* A factory for creating a {@link WebDataBinder} instance for a named target object.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 3.1
|
||||
*/
|
||||
public interface WebDataBinderFactory {
|
||||
|
|
@ -40,4 +42,18 @@ public interface WebDataBinderFactory {
|
|||
WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName)
|
||||
throws Exception;
|
||||
|
||||
/**
|
||||
* Variant of {@link #createBinder(NativeWebRequest, Object, String)} with a
|
||||
* {@link MethodParameter} for which the {@code DataBinder} is created. This
|
||||
* may provide more insight to initialize the {@link WebDataBinder}.
|
||||
* @since 6.1
|
||||
*/
|
||||
default WebDataBinder createBinder(
|
||||
NativeWebRequest webRequest, @Nullable Object target, String objectName,
|
||||
MethodParameter parameter) throws Exception {
|
||||
|
||||
return createBinder(webRequest, target, objectName);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -22,6 +22,7 @@ import java.util.ArrayList;
|
|||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
|
|
@ -35,6 +36,10 @@ import org.springframework.core.BridgeMethodResolver;
|
|||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotationPredicates;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.SynthesizingMethodParameter;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
|
@ -44,6 +49,7 @@ import org.springframework.util.ClassUtils;
|
|||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
/**
|
||||
|
|
@ -84,6 +90,10 @@ public class HandlerMethod {
|
|||
|
||||
private final MethodParameter[] parameters;
|
||||
|
||||
private final boolean validateArguments;
|
||||
|
||||
private final boolean validateReturnValue;
|
||||
|
||||
@Nullable
|
||||
private HttpStatusCode responseStatus;
|
||||
|
||||
|
|
@ -122,6 +132,8 @@ public class HandlerMethod {
|
|||
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
|
||||
ReflectionUtils.makeAccessible(this.bridgedMethod);
|
||||
this.parameters = initMethodParameters();
|
||||
this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, this.parameters);
|
||||
this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, this.bridgedMethod);
|
||||
evaluateResponseStatus();
|
||||
this.description = initDescription(this.beanType, this.method);
|
||||
}
|
||||
|
|
@ -141,6 +153,8 @@ public class HandlerMethod {
|
|||
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(this.method);
|
||||
ReflectionUtils.makeAccessible(this.bridgedMethod);
|
||||
this.parameters = initMethodParameters();
|
||||
this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, this.parameters);
|
||||
this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, this.bridgedMethod);
|
||||
evaluateResponseStatus();
|
||||
this.description = initDescription(this.beanType, this.method);
|
||||
}
|
||||
|
|
@ -177,6 +191,8 @@ public class HandlerMethod {
|
|||
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
|
||||
ReflectionUtils.makeAccessible(this.bridgedMethod);
|
||||
this.parameters = initMethodParameters();
|
||||
this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, this.parameters);
|
||||
this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, this.bridgedMethod);
|
||||
evaluateResponseStatus();
|
||||
this.description = initDescription(this.beanType, this.method);
|
||||
}
|
||||
|
|
@ -193,6 +209,8 @@ public class HandlerMethod {
|
|||
this.method = handlerMethod.method;
|
||||
this.bridgedMethod = handlerMethod.bridgedMethod;
|
||||
this.parameters = handlerMethod.parameters;
|
||||
this.validateArguments = handlerMethod.validateArguments;
|
||||
this.validateReturnValue = handlerMethod.validateReturnValue;
|
||||
this.responseStatus = handlerMethod.responseStatus;
|
||||
this.responseStatusReason = handlerMethod.responseStatusReason;
|
||||
this.description = handlerMethod.description;
|
||||
|
|
@ -212,6 +230,8 @@ public class HandlerMethod {
|
|||
this.method = handlerMethod.method;
|
||||
this.bridgedMethod = handlerMethod.bridgedMethod;
|
||||
this.parameters = handlerMethod.parameters;
|
||||
this.validateArguments = handlerMethod.validateArguments;
|
||||
this.validateReturnValue = handlerMethod.validateReturnValue;
|
||||
this.responseStatus = handlerMethod.responseStatus;
|
||||
this.responseStatusReason = handlerMethod.responseStatusReason;
|
||||
this.resolvedFromHandlerMethod = handlerMethod;
|
||||
|
|
@ -290,6 +310,33 @@ public class HandlerMethod {
|
|||
return this.parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the method arguments are a candidate for method validation, which
|
||||
* is the case when there are parameter {@code jakarta.validation.Constraint}
|
||||
* annotations.
|
||||
* <p>The presence of {@code jakarta.validation.Valid} by itself does not
|
||||
* trigger method validation since such parameters are already validated at
|
||||
* the level of argument resolvers.
|
||||
* <p><strong>Note:</strong> if the class is annotated with {@link Validated},
|
||||
* this method returns false, deferring to method validation via AOP proxy.
|
||||
* @since 6.1
|
||||
*/
|
||||
public boolean shouldValidateArguments() {
|
||||
return this.validateArguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the method return value is a candidate for method validation, which
|
||||
* is the case when there are method {@code jakarta.validation.Constraint}
|
||||
* or {@code jakarta.validation.Valid} annotations.
|
||||
* <p><strong>Note:</strong> if the class is annotated with {@link Validated},
|
||||
* this method returns false, deferring to method validation via AOP proxy.
|
||||
* @since 6.1
|
||||
*/
|
||||
public boolean shouldValidateReturnValue() {
|
||||
return this.validateReturnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the specified response status, if any.
|
||||
* @since 4.3.8
|
||||
|
|
@ -603,4 +650,38 @@ public class HandlerMethod {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks for the presence of {@code @Constraint} and {@code @Valid}
|
||||
* annotations on the method and method parameters.
|
||||
*/
|
||||
private static class MethodValidationInitializer {
|
||||
|
||||
private static final Predicate<MergedAnnotation<? extends Annotation>> INPUT_PREDICATE =
|
||||
MergedAnnotationPredicates.typeIn("jakarta.validation.Constraint");
|
||||
|
||||
private static final Predicate<MergedAnnotation<? extends Annotation>> OUTPUT_PREDICATE =
|
||||
MergedAnnotationPredicates.typeIn("jakarta.validation.Valid", "jakarta.validation.Constraint");
|
||||
|
||||
public static boolean checkArguments(Class<?> beanType, MethodParameter[] parameters) {
|
||||
if (AnnotationUtils.findAnnotation(beanType, Validated.class) == null) {
|
||||
for (MethodParameter parameter : parameters) {
|
||||
MergedAnnotations merged = MergedAnnotations.from(parameter.getParameterAnnotations());
|
||||
if (merged.stream().anyMatch(INPUT_PREDICATE)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean checkReturnValue(Class<?> beanType, Method method) {
|
||||
if (AnnotationUtils.findAnnotation(beanType, Validated.class) == null) {
|
||||
MergedAnnotations merged = MergedAnnotations.from(method, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY);
|
||||
return merged.stream().anyMatch(OUTPUT_PREDICATE);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
|||
if (bindingResult == null) {
|
||||
// Bean property binding and validation;
|
||||
// skipped in case of binding failure on construction.
|
||||
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
|
||||
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name, parameter);
|
||||
if (binder.getTarget() != null) {
|
||||
if (!mavContainer.isBindingDisabled(name)) {
|
||||
bindRequestParameters(binder, webRequest);
|
||||
|
|
@ -251,7 +251,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
|||
String[] paramNames = BeanUtils.getParameterNames(ctor);
|
||||
Class<?>[] paramTypes = ctor.getParameterTypes();
|
||||
Object[] args = new Object[paramTypes.length];
|
||||
WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName);
|
||||
WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName, parameter);
|
||||
String fieldDefaultPrefix = binder.getFieldDefaultPrefix();
|
||||
String fieldMarkerPrefix = binder.getFieldMarkerPrefix();
|
||||
boolean bindingFailure = false;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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.web.method.support;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import jakarta.validation.Validator;
|
||||
|
||||
import org.springframework.core.Conventions;
|
||||
import org.springframework.core.MethodParameter;
|
||||
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.MethodValidationResult;
|
||||
import org.springframework.validation.beanvalidation.MethodValidator;
|
||||
import org.springframework.validation.beanvalidation.ParameterErrors;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
||||
import org.springframework.web.bind.support.WebBindingInitializer;
|
||||
import org.springframework.web.method.annotation.ModelFactory;
|
||||
|
||||
/**
|
||||
* {@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}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 6.1
|
||||
*/
|
||||
public class HandlerMethodValidator extends DefaultMethodValidator {
|
||||
|
||||
|
||||
public HandlerMethodValidator(MethodValidationAdapter adapter) {
|
||||
super(adapter);
|
||||
adapter.setBindingResultNameResolver(this::determineObjectName);
|
||||
}
|
||||
|
||||
private String determineObjectName(MethodParameter param, @Nullable Object argument) {
|
||||
if (param.hasParameterAnnotation(RequestBody.class) || param.hasParameterAnnotation(RequestPart.class)) {
|
||||
return Conventions.getVariableNameForParameter(param);
|
||||
}
|
||||
else {
|
||||
return ((param.getParameterIndex() != -1) ?
|
||||
ModelFactory.getNameForParameter(param) :
|
||||
ModelFactory.getNameForReturnValue(argument, param));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void handleArgumentsResult(
|
||||
Object bean, Method method, Object[] arguments, Class<?>[] groups, MethodValidationResult result) {
|
||||
|
||||
if (result.getConstraintViolations().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!result.getBeanResults().isEmpty()) {
|
||||
int bindingResultCount = 0;
|
||||
for (ParameterErrors errors : result.getBeanResults()) {
|
||||
for (Object arg : arguments) {
|
||||
if (arg instanceof BindingResult bindingResult) {
|
||||
if (bindingResult.getObjectName().equals(errors.getObjectName())) {
|
||||
bindingResult.addAllErrors(errors);
|
||||
bindingResultCount++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.getAllValidationResults().size() == bindingResultCount) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
result.throwIfViolationsPresent();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@link MethodValidator} if Bean Validation is enabled in Spring MVC or WebFlux.
|
||||
* @param bindingInitializer for the configured Validator and MessageCodesResolver
|
||||
* @param parameterNameDiscoverer the {@code ParameterNameDiscoverer} to use
|
||||
* for {@link MethodValidationAdapter#setParameterNameDiscoverer}
|
||||
*/
|
||||
@Nullable
|
||||
public static MethodValidator from(
|
||||
@Nullable WebBindingInitializer bindingInitializer,
|
||||
@Nullable ParameterNameDiscoverer parameterNameDiscoverer) {
|
||||
|
||||
if (bindingInitializer instanceof ConfigurableWebBindingInitializer configurableInitializer) {
|
||||
if (configurableInitializer.getValidator() instanceof Validator validator) {
|
||||
MethodValidationAdapter validationAdapter = new MethodValidationAdapter(validator);
|
||||
if (parameterNameDiscoverer != null) {
|
||||
validationAdapter.setParameterNameDiscoverer(parameterNameDiscoverer);
|
||||
}
|
||||
MessageCodesResolver codesResolver = configurableInitializer.getMessageCodesResolver();
|
||||
if (codesResolver != null) {
|
||||
validationAdapter.setMessageCodesResolver(codesResolver);
|
||||
}
|
||||
return new HandlerMethodValidator(validationAdapter);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -30,6 +30,7 @@ import org.springframework.core.MethodParameter;
|
|||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.validation.beanvalidation.MethodValidator;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.support.SessionStatus;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
|
|
@ -50,6 +51,8 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||
|
||||
private static final Object[] EMPTY_ARGS = new Object[0];
|
||||
|
||||
private static final Class<?>[] EMPTY_GROUPS = new Class<?>[0];
|
||||
|
||||
|
||||
private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
|
||||
|
||||
|
|
@ -58,6 +61,9 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||
@Nullable
|
||||
private WebDataBinderFactory dataBinderFactory;
|
||||
|
||||
@Nullable
|
||||
private MethodValidator methodValidator;
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance from a {@code HandlerMethod}.
|
||||
|
|
@ -121,6 +127,16 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||
this.dataBinderFactory = dataBinderFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link MethodValidator} to perform method validation with if the
|
||||
* controller method {@link #shouldValidateArguments()} or
|
||||
* {@link #shouldValidateReturnValue()}.
|
||||
* @since 6.1
|
||||
*/
|
||||
public void setMethodValidator(@Nullable MethodValidator methodValidator) {
|
||||
this.methodValidator = methodValidator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invoke the method after resolving its argument values in the context of the given request.
|
||||
|
|
@ -149,7 +165,19 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Arguments: " + Arrays.toString(args));
|
||||
}
|
||||
return doInvoke(args);
|
||||
|
||||
Class<?>[] groups = getValidationGroups();
|
||||
if (shouldValidateArguments() && this.methodValidator != null) {
|
||||
this.methodValidator.validateArguments(getBean(), getBridgedMethod(), args, groups);
|
||||
}
|
||||
|
||||
Object returnValue = doInvoke(args);
|
||||
|
||||
if (shouldValidateReturnValue() && this.methodValidator != null) {
|
||||
this.methodValidator.validateReturnValue(getBean(), getBridgedMethod(), returnValue, groups);
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -194,6 +222,11 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||
return args;
|
||||
}
|
||||
|
||||
private Class<?>[] getValidationGroups() {
|
||||
return ((shouldValidateArguments() || shouldValidateReturnValue()) && this.methodValidator != null ?
|
||||
this.methodValidator.determineValidationGroups(getBean(), getBridgedMethod()) : EMPTY_GROUPS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the handler method with the given argument values.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* 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.web.method;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link HandlerMethod}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class HandlerMethodTests {
|
||||
|
||||
@Test
|
||||
void shouldValidateArgsWithConstraintsDirectlyOnClass() {
|
||||
Object target = new MyClass();
|
||||
testShouldValidateArguments(target, List.of("addIntValue", "addPersonAndIntValue"), true);
|
||||
testShouldValidateArguments(target, List.of("addPerson", "getPerson", "getIntValue", "addPersonNotValidated"), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateArgsWithConstraintsOnInterface() {
|
||||
Object target = new MyInterfaceImpl();
|
||||
testShouldValidateArguments(target, List.of("addIntValue", "addPersonAndIntValue"), true);
|
||||
testShouldValidateArguments(target, List.of("addPerson", "addPersonNotValidated", "getPerson", "getIntValue"), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateReturnValueWithConstraintsDirectlyOnClass() {
|
||||
Object target = new MyClass();
|
||||
testShouldValidateReturnValue(target, List.of("getPerson", "getIntValue"), true);
|
||||
testShouldValidateReturnValue(target, List.of("addPerson", "addIntValue", "addPersonNotValidated"), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateReturnValueWithConstraintsOnInterface() {
|
||||
Object target = new MyInterfaceImpl();
|
||||
testShouldValidateReturnValue(target, List.of("getPerson", "getIntValue"), true);
|
||||
testShouldValidateReturnValue(target, List.of("addPerson", "addIntValue", "addPersonNotValidated"), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void classLevelValidatedAnnotation() {
|
||||
Object target = new MyValidatedClass();
|
||||
testShouldValidateArguments(target, List.of("addPerson"), false);
|
||||
testShouldValidateReturnValue(target, List.of("getPerson"), false);
|
||||
}
|
||||
|
||||
private static void testShouldValidateArguments(Object target, List<String> methodNames, boolean expected) {
|
||||
for (String methodName : methodNames) {
|
||||
assertThat(getHandlerMethod(target, methodName).shouldValidateArguments()).isEqualTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
private static void testShouldValidateReturnValue(Object target, List<String> methodNames, boolean expected) {
|
||||
for (String methodName : methodNames) {
|
||||
assertThat(getHandlerMethod(target, methodName).shouldValidateReturnValue()).isEqualTo(expected);
|
||||
}
|
||||
}
|
||||
|
||||
private static HandlerMethod getHandlerMethod(Object target, String methodName) {
|
||||
Method method = ClassUtils.getMethod(target.getClass(), methodName, (Class<?>[]) null);
|
||||
return new HandlerMethod(target, method);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private record Person(@Size(min = 1, max = 10) String name) {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class MyClass {
|
||||
|
||||
public void addPerson(@Valid Person person) {
|
||||
}
|
||||
|
||||
public void addIntValue(@Max(10) int value) {
|
||||
}
|
||||
|
||||
public void addPersonAndIntValue(@Valid Person person, @Max(10) int value) {
|
||||
}
|
||||
|
||||
public void addPersonNotValidated(Person person) {
|
||||
}
|
||||
|
||||
@Valid
|
||||
public Person getPerson() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Max(10)
|
||||
public int getIntValue() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private interface MyInterface {
|
||||
|
||||
void addPerson(@Valid Person person);
|
||||
|
||||
void addIntValue(@Max(10) int value);
|
||||
|
||||
void addPersonAndIntValue(@Valid Person person, @Max(10) int value);
|
||||
|
||||
void addPersonNotValidated(Person person);
|
||||
|
||||
@Valid
|
||||
Person getPerson();
|
||||
|
||||
@Max(10)
|
||||
int getIntValue();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class MyInterfaceImpl implements MyInterface {
|
||||
|
||||
@Override
|
||||
public void addPerson(Person person) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addIntValue(int value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPersonAndIntValue(Person person, int value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPersonNotValidated(Person person) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Person getPerson() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntValue() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Validated
|
||||
private static class MyValidatedClass {
|
||||
|
||||
public void addPerson(@Valid Person person) {
|
||||
}
|
||||
|
||||
@Valid
|
||||
public Person getPerson() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -162,10 +162,10 @@ public class ModelAttributeMethodProcessorTests {
|
|||
public void resolveArgumentViaDefaultConstructor() throws Exception {
|
||||
WebDataBinder dataBinder = new WebRequestDataBinder(null);
|
||||
WebDataBinderFactory factory = mock();
|
||||
given(factory.createBinder(any(), notNull(), eq("attrName"))).willReturn(dataBinder);
|
||||
given(factory.createBinder(any(), notNull(), eq("attrName"), any())).willReturn(dataBinder);
|
||||
|
||||
this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory);
|
||||
verify(factory).createBinder(any(), notNull(), eq("attrName"));
|
||||
verify(factory).createBinder(any(), notNull(), eq("attrName"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -176,7 +176,7 @@ public class ModelAttributeMethodProcessorTests {
|
|||
|
||||
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
|
||||
WebDataBinderFactory factory = mock();
|
||||
given(factory.createBinder(this.request, target, name)).willReturn(dataBinder);
|
||||
given(factory.createBinder(this.request, target, name, this.paramNamedValidModelAttr)).willReturn(dataBinder);
|
||||
|
||||
this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory);
|
||||
|
||||
|
|
@ -195,7 +195,7 @@ public class ModelAttributeMethodProcessorTests {
|
|||
|
||||
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
|
||||
WebDataBinderFactory factory = mock();
|
||||
given(factory.createBinder(this.request, target, name)).willReturn(dataBinder);
|
||||
given(factory.createBinder(this.request, target, name, this.paramNamedValidModelAttr)).willReturn(dataBinder);
|
||||
|
||||
this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory);
|
||||
|
||||
|
|
@ -211,7 +211,7 @@ public class ModelAttributeMethodProcessorTests {
|
|||
|
||||
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
|
||||
WebDataBinderFactory factory = mock();
|
||||
given(factory.createBinder(this.request, target, name)).willReturn(dataBinder);
|
||||
given(factory.createBinder(this.request, target, name, this.paramBindingDisabledAttr)).willReturn(dataBinder);
|
||||
|
||||
this.processor.resolveArgument(this.paramBindingDisabledAttr, this.container, this.request, factory);
|
||||
|
||||
|
|
@ -229,12 +229,12 @@ public class ModelAttributeMethodProcessorTests {
|
|||
dataBinder.getBindingResult().reject("error");
|
||||
|
||||
WebDataBinderFactory binderFactory = mock();
|
||||
given(binderFactory.createBinder(this.request, target, name)).willReturn(dataBinder);
|
||||
given(binderFactory.createBinder(this.request, target, name, this.paramNonSimpleType)).willReturn(dataBinder);
|
||||
|
||||
assertThatExceptionOfType(MethodArgumentNotValidException.class).isThrownBy(() ->
|
||||
this.processor.resolveArgument(this.paramNonSimpleType, this.container, this.request, binderFactory));
|
||||
|
||||
verify(binderFactory).createBinder(this.request, target, name);
|
||||
verify(binderFactory).createBinder(this.request, target, name, this.paramNonSimpleType);
|
||||
}
|
||||
|
||||
@Test // SPR-9378
|
||||
|
|
@ -249,7 +249,7 @@ public class ModelAttributeMethodProcessorTests {
|
|||
|
||||
StubRequestDataBinder dataBinder = new StubRequestDataBinder(testBean, name);
|
||||
WebDataBinderFactory binderFactory = mock();
|
||||
given(binderFactory.createBinder(this.request, testBean, name)).willReturn(dataBinder);
|
||||
given(binderFactory.createBinder(this.request, testBean, name, this.paramModelAttr)).willReturn(dataBinder);
|
||||
|
||||
this.processor.resolveArgument(this.paramModelAttr, this.container, this.request, binderFactory);
|
||||
|
||||
|
|
@ -278,7 +278,7 @@ public class ModelAttributeMethodProcessorTests {
|
|||
ServletWebRequest requestWithParam = new ServletWebRequest(mockRequest);
|
||||
|
||||
WebDataBinderFactory factory = mock();
|
||||
given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs")))
|
||||
given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"), any()))
|
||||
.willAnswer(invocation -> {
|
||||
WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1));
|
||||
// Add conversion service which will convert "1,2" to a list
|
||||
|
|
@ -297,10 +297,10 @@ public class ModelAttributeMethodProcessorTests {
|
|||
|
||||
WebDataBinder dataBinder = new WebRequestDataBinder(target);
|
||||
WebDataBinderFactory factory = mock();
|
||||
given(factory.createBinder(this.request, target, expectedAttrName)).willReturn(dataBinder);
|
||||
given(factory.createBinder(this.request, target, expectedAttrName, param)).willReturn(dataBinder);
|
||||
|
||||
this.processor.resolveArgument(param, this.container, this.request, factory);
|
||||
verify(factory).createBinder(this.request, target, expectedAttrName);
|
||||
verify(factory).createBinder(this.request, target, expectedAttrName, param);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class ModelAttributeMethodProcessorKotlinTests {
|
|||
val mockRequest = MockHttpServletRequest().apply { addParameter("a", "b") }
|
||||
val requestWithParam = ServletWebRequest(mockRequest)
|
||||
val factory = mock<WebDataBinderFactory>()
|
||||
given(factory.createBinder(any(), any(), eq("param")))
|
||||
given(factory.createBinder(any(), any(), eq("param"), any()))
|
||||
.willAnswer { WebRequestDataBinder(it.getArgument(1)) }
|
||||
assertThat(processor.resolveArgument(this.param, container, requestWithParam, factory)).isEqualTo(Param("b"))
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ class ModelAttributeMethodProcessorKotlinTests {
|
|||
val mockRequest = MockHttpServletRequest().apply { addParameter("a", null) }
|
||||
val requestWithParam = ServletWebRequest(mockRequest)
|
||||
val factory = mock<WebDataBinderFactory>()
|
||||
given(factory.createBinder(any(), any(), eq("param")))
|
||||
given(factory.createBinder(any(), any(), eq("param"), any()))
|
||||
.willAnswer { WebRequestDataBinder(it.getArgument(1)) }
|
||||
assertThatThrownBy {
|
||||
processor.resolveArgument(this.param, container, requestWithParam, factory)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ dependencies {
|
|||
optional("jakarta.servlet.jsp:jakarta.servlet.jsp-api")
|
||||
optional("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api")
|
||||
optional("jakarta.el:jakarta.el-api")
|
||||
optional("jakarta.validation:jakarta.validation-api")
|
||||
optional("jakarta.xml.bind:jakarta.xml.bind-api")
|
||||
optional('io.micrometer:context-propagation')
|
||||
optional("org.webjars:webjars-locator-core")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -48,8 +48,10 @@ import org.springframework.http.converter.StringHttpMessageConverter;
|
|||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||
import org.springframework.validation.beanvalidation.MethodValidator;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
|
@ -86,6 +88,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
|||
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
|
||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
|
||||
import org.springframework.web.method.support.HandlerMethodValidator;
|
||||
import org.springframework.web.method.support.InvocableHandlerMethod;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
|
@ -128,6 +131,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
|||
(!AnnotatedElementUtils.hasAnnotation(method, RequestMapping.class) &&
|
||||
AnnotatedElementUtils.hasAnnotation(method, ModelAttribute.class));
|
||||
|
||||
private final static boolean BEAN_VALIDATION_PRESENT =
|
||||
ClassUtils.isPresent("jakarta.validation.Validator", HandlerMethod.class.getClassLoader());
|
||||
|
||||
|
||||
@Nullable
|
||||
private List<HandlerMethodArgumentResolver> customArgumentResolvers;
|
||||
|
|
@ -156,6 +162,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
|||
@Nullable
|
||||
private WebBindingInitializer webBindingInitializer;
|
||||
|
||||
@Nullable
|
||||
private MethodValidator methodValidator;
|
||||
|
||||
private AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("MvcAsync");
|
||||
|
||||
@Nullable
|
||||
|
|
@ -559,6 +568,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
|||
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
|
||||
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
|
||||
}
|
||||
if (BEAN_VALIDATION_PRESENT) {
|
||||
this.methodValidator = HandlerMethodValidator.from(this.webBindingInitializer, this.parameterNameDiscoverer);
|
||||
}
|
||||
}
|
||||
|
||||
private void initMessageConverters() {
|
||||
|
|
@ -855,6 +867,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
|||
}
|
||||
invocableMethod.setDataBinderFactory(binderFactory);
|
||||
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
|
||||
invocableMethod.setMethodValidator(this.methodValidator);
|
||||
|
||||
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
|
||||
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
|
||||
|
|
@ -955,7 +968,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
|||
Object bean = handlerMethod.getBean();
|
||||
initBinderMethods.add(createInitBinderMethod(bean, method));
|
||||
}
|
||||
return createDataBinderFactory(initBinderMethods);
|
||||
DefaultDataBinderFactory factory = createDataBinderFactory(initBinderMethods);
|
||||
factory.setMethodValidationApplicable(this.methodValidator != null && handlerMethod.shouldValidateArguments());
|
||||
return factory;
|
||||
}
|
||||
|
||||
private InvocableHandlerMethod createInitBinderMethod(Object bean, Method method) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -139,7 +139,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
|
|||
HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, name);
|
||||
arg = readWithMessageConverters(inputMessage, parameter, parameter.getNestedGenericParameterType());
|
||||
if (binderFactory != null) {
|
||||
WebDataBinder binder = binderFactory.createBinder(request, arg, name);
|
||||
WebDataBinder binder = binderFactory.createBinder(request, arg, name, parameter);
|
||||
if (arg != null) {
|
||||
validateIfApplicable(binder, parameter);
|
||||
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
|
|||
String name = Conventions.getVariableNameForParameter(parameter);
|
||||
|
||||
if (binderFactory != null) {
|
||||
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
|
||||
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name, parameter);
|
||||
if (arg != null) {
|
||||
validateIfApplicable(binder, parameter);
|
||||
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,439 @@
|
|||
/*
|
||||
* 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.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import jakarta.validation.executable.ExecutableValidator;
|
||||
import jakarta.validation.metadata.BeanDescriptor;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
import org.springframework.validation.beanvalidation.MethodValidationException;
|
||||
import org.springframework.validation.beanvalidation.ParameterValidationResult;
|
||||
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
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.servlet.HandlerMapping;
|
||||
import org.springframework.web.testfixture.method.ResolvableMethod;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowableOfType;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Method validation tests for Spring MVC controller methods.
|
||||
* <p>When adding tests, consider the following others:
|
||||
* <ul>
|
||||
* <li>{@code HandlerMethodTests} -- detection if methods need validation
|
||||
* <li>{@code MethodValidationAdapterTests} -- method validation independent of Spring MVC
|
||||
* <li>{@code MethodValidationProxyTests} -- method validation with proxy scenarios
|
||||
* </ul>
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class MethodValidationTests {
|
||||
|
||||
private static final Person mockPerson = mock(Person.class);
|
||||
|
||||
private static final Errors mockErrors = mock(Errors.class);
|
||||
|
||||
|
||||
private final MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
private final MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
private RequestMappingHandlerAdapter handlerAdapter;
|
||||
|
||||
private InvocationCountingValidator jakartaValidator;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
LocalValidatorFactoryBean validatorBean = new LocalValidatorFactoryBean();
|
||||
validatorBean.afterPropertiesSet();
|
||||
this.jakartaValidator = new InvocationCountingValidator(validatorBean);
|
||||
|
||||
this.handlerAdapter = initHandlerAdapter(this.jakartaValidator);
|
||||
|
||||
this.request.setMethod("POST");
|
||||
this.request.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||
this.request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, new HashMap<String, String>(0));
|
||||
}
|
||||
|
||||
private static RequestMappingHandlerAdapter initHandlerAdapter(Validator validator) {
|
||||
ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer();
|
||||
bindingInitializer.setValidator(validator);
|
||||
|
||||
GenericWebApplicationContext context = new GenericWebApplicationContext();
|
||||
context.refresh();
|
||||
|
||||
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
|
||||
handlerAdapter.setWebBindingInitializer(bindingInitializer);
|
||||
handlerAdapter.setApplicationContext(context);
|
||||
handlerAdapter.setBeanFactory(context.getBeanFactory());
|
||||
handlerAdapter.afterPropertiesSet();
|
||||
return handlerAdapter;
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void modelAttribute() {
|
||||
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson));
|
||||
this.request.addParameter("name", "name=Faustino1234");
|
||||
|
||||
MethodArgumentNotValidException ex = catchThrowableOfType(
|
||||
() -> this.handlerAdapter.handle(this.request, this.response, hm),
|
||||
MethodArgumentNotValidException.class);
|
||||
|
||||
assertThat(this.jakartaValidator.getValidationCount()).isEqualTo(1);
|
||||
assertThat(this.jakartaValidator.getMethodValidationCount()).as("Method validation unexpected").isEqualTo(0);
|
||||
|
||||
assertBeanResult(ex.getBindingResult(), "student", Collections.singletonList(
|
||||
"""
|
||||
Field error in object 'student' on field 'name': rejected value [name=Faustino1234]; \
|
||||
codes [Size.student.name,Size.name,Size.java.lang.String,Size]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [student.name,name]; arguments []; default message [name],10,1]; \
|
||||
default message [size must be between 1 and 10]"""));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void modelAttributeWithBindingResult() throws Exception {
|
||||
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson, mockErrors));
|
||||
this.request.addParameter("name", "name=Faustino1234");
|
||||
|
||||
this.handlerAdapter.handle(this.request, this.response, hm);
|
||||
|
||||
assertThat(this.jakartaValidator.getValidationCount()).isEqualTo(1);
|
||||
assertThat(this.jakartaValidator.getMethodValidationCount()).as("Method validation unexpected").isEqualTo(0);
|
||||
|
||||
assertThat(response.getContentAsString()).isEqualTo(
|
||||
"""
|
||||
org.springframework.validation.BeanPropertyBindingResult: 1 errors
|
||||
Field error in object 'student' on field 'name': rejected value [name=Faustino1234]; \
|
||||
codes [Size.student.name,Size.name,Size.java.lang.String,Size]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [student.name,name]; arguments []; default message [name],10,1]; \
|
||||
default message [size must be between 1 and 10]""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void modelAttributeWithBindingResultAndRequestHeader() {
|
||||
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson, mockErrors, ""));
|
||||
this.request.addParameter("name", "name=Faustino1234");
|
||||
this.request.addHeader("myHeader", "123");
|
||||
|
||||
MethodValidationException ex = catchThrowableOfType(
|
||||
() -> this.handlerAdapter.handle(this.request, this.response, hm),
|
||||
MethodValidationException.class);
|
||||
|
||||
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(
|
||||
"""
|
||||
Field error in object 'student' on field 'name': rejected value [name=Faustino1234]; \
|
||||
codes [Size.student.name,Size.name,Size.java.lang.String,Size]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [student.name,name]; arguments []; default message [name],10,1]; \
|
||||
default message [size must be between 1 and 10]"""));
|
||||
|
||||
assertValueResult(ex.getValueResults().get(0), 2, "123", Collections.singletonList(
|
||||
"""
|
||||
org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [Size.validController#handle.myHeader,Size.myHeader,Size.java.lang.String,Size]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [validController#handle.myHeader,myHeader]; arguments []; default message [myHeader],10,5]; \
|
||||
default message [size must be between 5 and 10]"""
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatedWithMethodValidation() throws Exception {
|
||||
|
||||
// 1 for @Validated argument validation + 1 for method validation of @RequestHeader
|
||||
this.jakartaValidator.setMaxInvocationsExpected(2);
|
||||
|
||||
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handleValidated(mockPerson, mockErrors, ""));
|
||||
this.request.addParameter("name", "name=Faustino1234");
|
||||
this.request.addHeader("myHeader", "12345");
|
||||
|
||||
this.handlerAdapter.handle(this.request, this.response, hm);
|
||||
|
||||
assertThat(jakartaValidator.getValidationCount()).isEqualTo(2);
|
||||
assertThat(jakartaValidator.getMethodValidationCount()).isEqualTo(1);
|
||||
|
||||
assertThat(response.getContentAsString()).isEqualTo(
|
||||
"""
|
||||
org.springframework.validation.BeanPropertyBindingResult: 1 errors
|
||||
Field error in object 'person' on field 'name': rejected value [name=Faustino1234]; \
|
||||
codes [Size.person.name,Size.name,Size.java.lang.String,Size]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [person.name,name]; arguments []; default message [name],10,1]; \
|
||||
default message [size must be between 1 and 10]""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void jakartaAndSpringValidator() throws Exception {
|
||||
HandlerMethod hm = handlerMethod(new InitBinderController(), ibc -> ibc.handle(mockPerson, mockErrors, ""));
|
||||
this.request.addParameter("name", "name=Faustino1234");
|
||||
this.request.addHeader("myHeader", "12345");
|
||||
|
||||
this.handlerAdapter.handle(this.request, this.response, hm);
|
||||
|
||||
assertThat(jakartaValidator.getValidationCount()).isEqualTo(1);
|
||||
assertThat(jakartaValidator.getMethodValidationCount()).isEqualTo(1);
|
||||
|
||||
assertThat(response.getContentAsString()).isEqualTo(
|
||||
"""
|
||||
org.springframework.validation.BeanPropertyBindingResult: 2 errors
|
||||
Field error in object 'person' on field 'name': rejected value [name=Faustino1234]; \
|
||||
codes [TOO_LONG.person.name,TOO_LONG.name,TOO_LONG.java.lang.String,TOO_LONG]; \
|
||||
arguments []; default message [length must be 10 or under]
|
||||
Field error in object 'person' on field 'name': rejected value [name=Faustino1234]; \
|
||||
codes [Size.person.name,Size.name,Size.java.lang.String,Size]; \
|
||||
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||
codes [person.name,name]; arguments []; default message [name],10,1]; \
|
||||
default message [size must be between 1 and 10]""");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void springValidator() throws Exception {
|
||||
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson, mockErrors));
|
||||
this.request.addParameter("name", "name=Faustino1234");
|
||||
|
||||
RequestMappingHandlerAdapter springValidatorHandlerAdapter = initHandlerAdapter(new PersonValidator());
|
||||
springValidatorHandlerAdapter.handle(this.request, this.response, hm);
|
||||
|
||||
assertThat(response.getContentAsString()).isEqualTo(
|
||||
"""
|
||||
org.springframework.validation.BeanPropertyBindingResult: 1 errors
|
||||
Field error in object 'student' on field 'name': rejected value [name=Faustino1234]; \
|
||||
codes [TOO_LONG.student.name,TOO_LONG.name,TOO_LONG.java.lang.String,TOO_LONG]; \
|
||||
arguments []; default message [length must be 10 or under]""");
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> HandlerMethod handlerMethod(T controller, Consumer<T> mockCallConsumer) {
|
||||
Assert.isTrue(!(controller instanceof Class<?>), "Expected controller instance");
|
||||
Method method = ResolvableMethod.on((Class<T>) controller.getClass()).mockCall(mockCallConsumer).method();
|
||||
return new HandlerMethod(controller, method);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static void assertBeanResult(Errors errors, String objectName, List<String> fieldErrors) {
|
||||
assertThat(errors.getObjectName()).isEqualTo(objectName);
|
||||
assertThat(errors.getFieldErrors())
|
||||
.extracting(FieldError::toString)
|
||||
.containsExactlyInAnyOrderElementsOf(fieldErrors);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static void assertValueResult(
|
||||
ParameterValidationResult result, int parameterIndex, Object argument, List<String> errors) {
|
||||
|
||||
assertThat(result.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex);
|
||||
assertThat(result.getArgument()).isEqualTo(argument);
|
||||
assertThat(result.getResolvableErrors())
|
||||
.extracting(MessageSourceResolvable::toString)
|
||||
.containsExactlyInAnyOrderElementsOf(errors);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private record Person(@Size(min = 1, max = 10) String name) {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings({"unused", "SameParameterValue", "UnusedReturnValue"})
|
||||
@RestController
|
||||
static class ValidController {
|
||||
|
||||
void handle(@Valid @ModelAttribute("student") Person person) {
|
||||
}
|
||||
|
||||
String handle(@Valid @ModelAttribute("student") Person person, Errors errors) {
|
||||
return errors.toString();
|
||||
}
|
||||
|
||||
void handle(@Valid @ModelAttribute("student") Person person, Errors errors,
|
||||
@RequestHeader @Size(min = 5, max = 10) String myHeader) {
|
||||
}
|
||||
|
||||
String handleValidated(@Validated Person person, Errors errors,
|
||||
@RequestHeader @Size(min = 5, max = 10) String myHeader) {
|
||||
|
||||
return errors.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings({"unused", "UnusedReturnValue", "SameParameterValue"})
|
||||
@RestController
|
||||
static class InitBinderController {
|
||||
|
||||
@InitBinder
|
||||
void initBinder(WebDataBinder dataBinder) {
|
||||
dataBinder.addValidators(new PersonValidator());
|
||||
}
|
||||
|
||||
String handle(@Valid Person person, Errors errors, @RequestHeader @Size(min = 5, max = 10) String myHeader) {
|
||||
return errors.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class PersonValidator implements Validator {
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> clazz) {
|
||||
return (clazz == Person.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Object target, Errors errors) {
|
||||
Person person = (Person) target;
|
||||
if (person.name().length() > 10) {
|
||||
errors.rejectValue("name", "TOO_LONG", "length must be 10 or under");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Intercept and count number of method validation calls.
|
||||
*/
|
||||
private static class InvocationCountingValidator implements jakarta.validation.Validator, Validator {
|
||||
|
||||
private final SpringValidatorAdapter delegate;
|
||||
|
||||
private int maxInvocationsExpected = 1;
|
||||
|
||||
private int validationCount;
|
||||
|
||||
private int methodValidationCount;
|
||||
|
||||
/**
|
||||
* Constructor with maxCount=1.
|
||||
*/
|
||||
private InvocationCountingValidator(SpringValidatorAdapter delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public void setMaxInvocationsExpected(int maxInvocationsExpected) {
|
||||
this.maxInvocationsExpected = maxInvocationsExpected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total number of times Bean Validation was invoked.
|
||||
*/
|
||||
public int getValidationCount() {
|
||||
return this.validationCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of times method level Bean Validation was invoked.
|
||||
*/
|
||||
public int getMethodValidationCount() {
|
||||
return this.methodValidationCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType, String propertyName, Object value, Class<?>... groups) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BeanDescriptor getConstraintsForClass(Class<?> clazz) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T unwrap(Class<T> type) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecutableValidator forExecutables() {
|
||||
this.methodValidationCount++;
|
||||
assertCountAndIncrement();
|
||||
return this.delegate.forExecutables();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> clazz) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Object target, Errors errors) {
|
||||
assertCountAndIncrement();
|
||||
this.delegate.validate(target, errors);
|
||||
}
|
||||
|
||||
private void assertCountAndIncrement() {
|
||||
assertThat(this.validationCount++).as("Too many calls to Bean Validation").isLessThan(this.maxInvocationsExpected);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue