Add method validation to Spring MVC

See gh-29825
This commit is contained in:
Rossen Stoyanchev 2023-06-08 15:39:22 +01:00 committed by rstoyanchev
parent cb04c3b335
commit bd054a4918
15 changed files with 1000 additions and 27 deletions

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -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;
}
}
}

View File

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

View File

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

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -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.
*/

View File

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

View File

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

View File

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

View File

@ -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")

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -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) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -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)) {

View File

@ -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)) {

View File

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