diff --git a/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java b/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java index 45474877ab..015ecd14ba 100644 --- a/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java +++ b/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java @@ -79,9 +79,10 @@ public interface MethodValidationResult { List getAllValidationResults(); /** - * Return only validation results for method parameters with errors directly - * on them. This does not include Object method parameters with nested - * errors on their fields and properties. + * Return the subset of {@link #getAllValidationResults() allValidationResults} + * that includes method parameters with validation errors directly on method + * argument values. This excludes {@link #getBeanResults() beanResults} with + * nested errors on their fields and properties. */ default List getValueResults() { return getAllValidationResults().stream() @@ -90,9 +91,10 @@ public interface MethodValidationResult { } /** - * Return only validation results for Object method parameters with nested - * errors on their fields and properties. This excludes method parameters - * with errors directly on them. + * Return the subset of {@link #getAllValidationResults() allValidationResults} + * that includes Object method parameters with nested errors on their fields + * and properties. This excludes {@link #getValueResults() valueResults} with + * validation errors directly on method arguments. */ default List getBeanResults() { return getAllValidationResults().stream() diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidationException.java b/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidationException.java index 749ab35a38..10f7281c02 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidationException.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidationException.java @@ -19,11 +19,24 @@ package org.springframework.web.method.annotation; import java.lang.reflect.Method; import java.util.List; import java.util.Locale; +import java.util.function.Predicate; import org.springframework.context.MessageSource; +import org.springframework.core.MethodParameter; import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.validation.method.MethodValidationResult; +import org.springframework.validation.method.ParameterErrors; import org.springframework.validation.method.ParameterValidationResult; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.util.BindErrorUtils; @@ -43,10 +56,24 @@ public class HandlerMethodValidationException extends ResponseStatusException im private final MethodValidationResult validationResult; + private final Predicate modelAttribitePredicate; + + private final Predicate requestParamPredicate; + public HandlerMethodValidationException(MethodValidationResult validationResult) { + this(validationResult, + param -> param.hasParameterAnnotation(ModelAttribute.class), + param -> param.hasParameterAnnotation(RequestParam.class)); + } + + public HandlerMethodValidationException(MethodValidationResult validationResult, + Predicate modelAttribitePredicate, Predicate requestParamPredicate) { + super(initHttpStatus(validationResult), "Validation failure", null, null, null); this.validationResult = validationResult; + this.modelAttribitePredicate = modelAttribitePredicate; + this.requestParamPredicate = requestParamPredicate; } private static HttpStatus initHttpStatus(MethodValidationResult validationResult) { @@ -84,4 +111,133 @@ public class HandlerMethodValidationException extends ResponseStatusException im return this.validationResult.getAllValidationResults(); } + /** + * Provide a {@link Visitor Visitor} to handle {@link ParameterValidationResult}s + * through callback methods organized by controller method parameter type. + */ + public void visitResults(Visitor visitor) { + for (ParameterValidationResult result : getAllValidationResults()) { + MethodParameter param = result.getMethodParameter(); + CookieValue cookieValue = param.getParameterAnnotation(CookieValue.class); + if (cookieValue != null) { + visitor.cookieValue(cookieValue, result); + continue; + } + MatrixVariable matrixVariable = param.getParameterAnnotation(MatrixVariable.class); + if (matrixVariable != null) { + visitor.matrixVariable(matrixVariable, result); + continue; + } + if (this.modelAttribitePredicate.test(param)) { + ModelAttribute modelAttribute = param.getParameterAnnotation(ModelAttribute.class); + visitor.modelAttribute(modelAttribute, asErrors(result)); + continue; + } + PathVariable pathVariable = param.getParameterAnnotation(PathVariable.class); + if (pathVariable != null) { + visitor.pathVariable(pathVariable, result); + continue; + } + RequestBody requestBody = param.getParameterAnnotation(RequestBody.class); + if (requestBody != null) { + visitor.requestBody(requestBody, asErrors(result)); + continue; + } + RequestHeader requestHeader = param.getParameterAnnotation(RequestHeader.class); + if (requestHeader != null) { + visitor.requestHeader(requestHeader, result); + continue; + } + if (this.requestParamPredicate.test(param)) { + RequestParam requestParam = param.getParameterAnnotation(RequestParam.class); + visitor.requestParam(requestParam, result); + continue; + } + RequestPart requestPart = param.getParameterAnnotation(RequestPart.class); + if (requestPart != null) { + visitor.requestPart(requestPart, asErrors(result)); + continue; + } + visitor.other(result); + } + } + + private static ParameterErrors asErrors(ParameterValidationResult result) { + Assert.state(result instanceof ParameterErrors, "Expected ParameterErrors"); + return (ParameterErrors) result; + } + + + /** + * Contract to handle validation results with callbacks by controller method + * parameter type, with {@link #other} serving as the fallthrough. + */ + public interface Visitor { + + /** + * Handle results for {@code @CookieValue} method parameters. + * @param cookieValue the annotation declared on the parameter + * @param result the validation result + */ + void cookieValue(CookieValue cookieValue, ParameterValidationResult result); + + /** + * Handle results for {@code @MatrixVariable} method parameters. + * @param matrixVariable the annotation declared on the parameter + * @param result the validation result + */ + void matrixVariable(MatrixVariable matrixVariable, ParameterValidationResult result); + + /** + * Handle results for {@code @ModelAttribute} method parameters. + * @param modelAttribute the optional {@code ModelAttribute} annotation, + * possibly {@code null} if the method parameter is declared without it. + * @param errors the validation errors + */ + void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors); + + /** + * Handle results for {@code @PathVariable} method parameters. + * @param pathVariable the annotation declared on the parameter + * @param result the validation result + */ + void pathVariable(PathVariable pathVariable, ParameterValidationResult result); + + /** + * Handle results for {@code @RequestBody} method parameters. + * @param requestBody the annotation declared on the parameter + * @param errors the validation error + */ + void requestBody(RequestBody requestBody, ParameterErrors errors); + + /** + * Handle results for {@code @RequestHeader} method parameters. + * @param requestHeader the annotation declared on the parameter + * @param result the validation result + */ + void requestHeader(RequestHeader requestHeader, ParameterValidationResult result); + + /** + * Handle results for {@code @RequestParam} method parameters. + * @param requestParam the optional {@code RequestParam} annotation, + * possibly {@code null} if the method parameter is declared without it. + * @param result the validation result + */ + void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result); + + /** + * Handle results for {@code @RequestPart} method parameters. + * @param requestPart the annotation declared on the parameter + * @param errors the validation errors + */ + void requestPart(RequestPart requestPart, ParameterErrors errors); + + /** + * Handle other results that aren't any of the above. + * @param result the validation result + */ + void other(ParameterValidationResult result); + + } + } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidator.java b/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidator.java index e8a08cd46f..830254788b 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidator.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidator.java @@ -17,6 +17,7 @@ package org.springframework.web.method.annotation; import java.lang.reflect.Method; +import java.util.function.Predicate; import jakarta.validation.Validator; @@ -54,9 +55,17 @@ public final class HandlerMethodValidator implements MethodValidator { private final MethodValidationAdapter validationAdapter; + private final Predicate modelAttribitePredicate; + + private final Predicate requestParamPredicate; + + + private HandlerMethodValidator(MethodValidationAdapter validationAdapter, + Predicate modelAttribitePredicate, Predicate requestParamPredicate) { - private HandlerMethodValidator(MethodValidationAdapter validationAdapter) { this.validationAdapter = validationAdapter; + this.modelAttribitePredicate = modelAttribitePredicate; + this.requestParamPredicate = requestParamPredicate; } @@ -93,7 +102,8 @@ public final class HandlerMethodValidator implements MethodValidator { } } - throw new HandlerMethodValidationException(result); + throw new HandlerMethodValidationException( + result, this.modelAttribitePredicate, this.requestParamPredicate); } @Override @@ -130,11 +140,13 @@ public final class HandlerMethodValidator implements MethodValidator { */ @Nullable public static MethodValidator from( - @Nullable WebBindingInitializer initializer, @Nullable ParameterNameDiscoverer paramNameDiscoverer) { + @Nullable WebBindingInitializer initializer, @Nullable ParameterNameDiscoverer paramNameDiscoverer, + Predicate modelAttribitePredicate, Predicate requestParamPredicate) { if (initializer instanceof ConfigurableWebBindingInitializer configurableInitializer) { if (configurableInitializer.getValidator() instanceof Validator validator) { MethodValidationAdapter adapter = new MethodValidationAdapter(validator); + adapter.setObjectNameResolver(objectNameResolver); if (paramNameDiscoverer != null) { adapter.setParameterNameDiscoverer(paramNameDiscoverer); } @@ -142,9 +154,7 @@ public final class HandlerMethodValidator implements MethodValidator { if (codesResolver != null) { adapter.setMessageCodesResolver(codesResolver); } - HandlerMethodValidator methodValidator = new HandlerMethodValidator(adapter); - adapter.setObjectNameResolver(objectNameResolver); - return methodValidator; + return new HandlerMethodValidator(adapter, modelAttribitePredicate, requestParamPredicate); } } return null; diff --git a/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java b/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java index e9b5f6056c..4ca80dd52c 100644 --- a/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java +++ b/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -127,7 +127,7 @@ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgu * the given method parameter. */ @Nullable - private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + public HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { diff --git a/spring-web/src/test/java/org/springframework/web/method/support/HandlerMethodValidationExceptionTests.java b/spring-web/src/test/java/org/springframework/web/method/support/HandlerMethodValidationExceptionTests.java new file mode 100644 index 0000000000..59bf93402b --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/method/support/HandlerMethodValidationExceptionTests.java @@ -0,0 +1,253 @@ +/* + * 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.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.StringJoiner; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Size; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.BeanUtils; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; +import org.springframework.validation.method.MethodValidationResult; +import org.springframework.validation.method.ParameterErrors; +import org.springframework.validation.method.ParameterValidationResult; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.annotation.HandlerMethodValidationException; +import org.springframework.web.testfixture.method.ResolvableMethod; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link HandlerMethodValidationException}. + * + * @author Rossen Stoyanchev + */ +public class HandlerMethodValidationExceptionTests { + + private static final Person person = new Person("Faustino1234"); + + private static final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + + private final HandlerMethod handlerMethod = handlerMethod(new ValidController(), + controller -> controller.handle(person, person, person, person, "", "", "", "", "", "")); + + private final TestVisitor visitor = new TestVisitor(); + + + @Test + void traverse() { + + HandlerMethodValidationException ex = + new HandlerMethodValidationException(createMethodValidationResult(this.handlerMethod), + new MvcParamPredicate(ModelAttribute.class), + new MvcParamPredicate(RequestParam.class)); + + ex.visitResults(this.visitor); + + assertThat(this.visitor.getOutput()).isEqualTo(""" + @ModelAttribute: modelAttribute1, @ModelAttribute: modelAttribute2, \ + @RequestBody: requestBody, @RequestPart: requestPart, \ + @RequestParam: requestParam1, @RequestParam: requestParam2, \ + @RequestHeader: header, @PathVariable: pathVariable, \ + @CookieValue: cookie, @MatrixVariable: matrixVariable"""); + } + + @Test + void traverseRemaining() { + + HandlerMethodValidationException ex = + new HandlerMethodValidationException(createMethodValidationResult(this.handlerMethod)); + + ex.visitResults(this.visitor); + + assertThat(this.visitor.getOutput()).isEqualTo(""" + Other: modelAttribute1, @ModelAttribute: modelAttribute2, \ + @RequestBody: requestBody, @RequestPart: requestPart, \ + Other: requestParam1, @RequestParam: requestParam2, \ + @RequestHeader: header, @PathVariable: pathVariable, \ + @CookieValue: cookie, @MatrixVariable: matrixVariable"""); + } + + @SuppressWarnings("unchecked") + private static HandlerMethod handlerMethod(T controller, Consumer mockCallConsumer) { + Method method = ResolvableMethod.on((Class) controller.getClass()).mockCall(mockCallConsumer).method(); + HandlerMethod hm = new HandlerMethod(controller, method); + for (MethodParameter parameter : hm.getMethodParameters()) { + parameter.initParameterNameDiscovery(parameterNameDiscoverer); + } + return hm; + } + + private static MethodValidationResult createMethodValidationResult(HandlerMethod handlerMethod) { + return MethodValidationResult.create( + handlerMethod.getBean(), handlerMethod.getMethod(), + Arrays.stream(handlerMethod.getMethodParameters()) + .map(param -> { + if (param.hasParameterAnnotation(Valid.class)) { + Errors errors = new BeanPropertyBindingResult(person, param.getParameterName()); + errors.rejectValue("name", "Size.person.name"); + return new ParameterErrors(param, person, errors, null, null, null); + } + else { + MessageSourceResolvable error = new DefaultMessageSourceResolvable("Size"); + return new ParameterValidationResult(param, "123", List.of(error)); + } + }) + .toList()); + } + + + + @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 Person modelAttribute1, + @Valid @ModelAttribute Person modelAttribute2, + @Valid @RequestBody Person requestBody, + @Valid @RequestPart Person requestPart, + @Size(min = 5) String requestParam1, + @Size(min = 5) @RequestParam String requestParam2, + @Size(min = 5) @RequestHeader String header, + @Size(min = 5) @PathVariable String pathVariable, + @Size(min = 5) @CookieValue String cookie, + @Size(min = 5) @MatrixVariable String matrixVariable) { + } + + } + + + private record MvcParamPredicate(Class type) implements Predicate { + + @Override + public boolean test(MethodParameter param) { + return (param.hasParameterAnnotation(this.type) || + (isDefaultParameter(param) && !hasMvcAnnotation(param))); + } + + private boolean isDefaultParameter(MethodParameter param) { + boolean simpleType = BeanUtils.isSimpleValueType(param.getParameterType()); + return ((this.type.equals(RequestParam.class) && simpleType) || + (this.type.equals(ModelAttribute.class) && !simpleType)); + } + + private boolean hasMvcAnnotation(MethodParameter param) { + return Arrays.stream(param.getParameterAnnotations()) + .map(Annotation::annotationType) + .anyMatch(type -> type.getPackage().equals(RequestParam.class.getPackage())); + } + } + + + private static class TestVisitor implements HandlerMethodValidationException.Visitor { + + private final StringJoiner joiner = new StringJoiner(", "); + + public String getOutput() { + return this.joiner.toString(); + } + + @Override + public void cookieValue(CookieValue cookieValue, ParameterValidationResult result) { + handle(cookieValue, result); + } + + @Override + public void matrixVariable(MatrixVariable matrixVariable, ParameterValidationResult result) { + handle(matrixVariable, result); + } + + @Override + public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors) { + handle("@ModelAttribute", errors); + } + + @Override + public void pathVariable(PathVariable pathVariable, ParameterValidationResult result) { + handle(pathVariable, result); + } + + @Override + public void requestBody(RequestBody requestBody, ParameterErrors errors) { + handle(requestBody, errors); + } + + @Override + public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) { + handle(requestHeader, result); + } + + @Override + public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) { + handle("@RequestParam", result); + } + + @Override + public void requestPart(RequestPart requestPart, ParameterErrors errors) { + handle(requestPart, errors); + } + + @Override + public void other(ParameterValidationResult result) { + handle("Other", result); + } + + private void handle(Annotation annotation, ParameterValidationResult result) { + handle("@" + annotation.annotationType().getSimpleName(), result); + } + + private void handle(String tag, ParameterValidationResult result) { + this.joiner.add(tag + ": " + result.getMethodParameter().getParameterName()); + } + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java index f248bfcc36..9e80b55093 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -33,23 +34,28 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodIntrospector; +import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.codec.HttpMessageReader; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.validation.method.MethodValidator; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.method.ControllerAdviceBean; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; +import org.springframework.web.method.annotation.HandlerMethodValidator; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.reactive.result.method.InvocableHandlerMethod; import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver; import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod; +import org.springframework.web.service.invoker.RequestParamArgumentResolver; /** * Package-private class to assist {@link RequestMappingHandlerAdapter} with @@ -82,9 +88,12 @@ class ControllerMethodResolver { (!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()); private static final Log logger = LogFactory.getLog(ControllerMethodResolver.class); + private final List initBinderResolvers; private final List modelAttributeResolvers; @@ -117,7 +126,7 @@ class ControllerMethodResolver { ControllerMethodResolver( ArgumentResolverConfigurer customResolvers, ReactiveAdapterRegistry adapterRegistry, ConfigurableApplicationContext context, List> readers, - @Nullable MethodValidator methodValidator) { + @Nullable WebBindingInitializer webBindingInitializer) { Assert.notNull(customResolvers, "ArgumentResolverConfigurer is required"); Assert.notNull(adapterRegistry, "ReactiveAdapterRegistry is required"); @@ -129,7 +138,15 @@ class ControllerMethodResolver { this.requestMappingResolvers = requestMappingResolvers(customResolvers, adapterRegistry, context, readers); this.exceptionHandlerResolvers = exceptionHandlerResolvers(customResolvers, adapterRegistry, context); this.reactiveAdapterRegistry = adapterRegistry; - this.methodValidator = methodValidator; + + if (BEAN_VALIDATION_PRESENT) { + this.methodValidator = HandlerMethodValidator.from(webBindingInitializer, null, + methodParamPredicate(this.requestMappingResolvers, ModelAttributeMethodArgumentResolver.class), + methodParamPredicate(this.requestMappingResolvers, RequestParamArgumentResolver.class)); + } + else { + this.methodValidator = null; + } initControllerAdviceCaches(context); } @@ -258,6 +275,19 @@ class ControllerMethodResolver { } } + private static Predicate methodParamPredicate( + List resolvers, Class resolverType) { + + return parameter -> { + for (HandlerMethodArgumentResolver resolver : resolvers) { + if (resolver.supportsParameter(parameter)) { + return resolverType.isInstance(resolver); + } + } + return false; + }; + } + /** * Return an {@link InvocableHandlerMethod} for the given @@ -383,6 +413,10 @@ class ControllerMethodResolver { return invocable; } + public boolean hasMethodValidator() { + return (this.methodValidator != null); + } + /** * Return the handler for the type-level {@code @SessionAttributes} annotation * based on the given controller method. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index 3793f1d9a1..53579c9fdd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -33,12 +33,9 @@ import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; -import org.springframework.validation.method.MethodValidator; import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.method.annotation.HandlerMethodValidator; import org.springframework.web.reactive.BindingContext; import org.springframework.web.reactive.DispatchExceptionHandler; import org.springframework.web.reactive.HandlerAdapter; @@ -60,9 +57,6 @@ public class RequestMappingHandlerAdapter private static final Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class); - private final static boolean BEAN_VALIDATION_PRESENT = - ClassUtils.isPresent("jakarta.validation.Validator", HandlerMethod.class.getClassLoader()); - private List> messageReaders = Collections.emptyList(); @@ -75,9 +69,6 @@ public class RequestMappingHandlerAdapter @Nullable private ReactiveAdapterRegistry reactiveAdapterRegistry; - @Nullable - private MethodValidator methodValidator; - @Nullable private ConfigurableApplicationContext applicationContext; @@ -179,12 +170,10 @@ public class RequestMappingHandlerAdapter if (this.reactiveAdapterRegistry == null) { this.reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); } - if (BEAN_VALIDATION_PRESENT) { - this.methodValidator = HandlerMethodValidator.from(this.webBindingInitializer, null); - } - this.methodResolver = new ControllerMethodResolver(this.argumentResolverConfigurer, - this.reactiveAdapterRegistry, this.applicationContext, this.messageReaders, this.methodValidator); + this.methodResolver = new ControllerMethodResolver( + this.argumentResolverConfigurer, this.reactiveAdapterRegistry, this.applicationContext, + this.messageReaders, this.webBindingInitializer); this.modelInitializer = new ModelInitializer(this.methodResolver, this.reactiveAdapterRegistry); } @@ -202,7 +191,7 @@ public class RequestMappingHandlerAdapter InitBinderBindingContext bindingContext = new InitBinderBindingContext( this.webBindingInitializer, this.methodResolver.getInitBinderMethods(handlerMethod), - this.methodValidator != null && handlerMethod.shouldValidateArguments()); + this.methodResolver.hasMethodValidator() && handlerMethod.shouldValidateArguments()); InvocableHandlerMethod invocableMethod = this.methodResolver.getRequestMappingMethod(handlerMethod); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MethodValidationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MethodValidationTests.java index 72335a6024..8f36d836e8 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MethodValidationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/MethodValidationTests.java @@ -37,7 +37,6 @@ import reactor.test.StepVerifier; import org.springframework.context.MessageSourceResolvable; import org.springframework.context.i18n.LocaleContextHolder; 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; @@ -300,7 +299,6 @@ public class MethodValidationTests { @SuppressWarnings("unchecked") private static HandlerMethod handlerMethod(T controller, Consumer mockCallConsumer) { - Assert.isTrue(!(controller instanceof Class), "Expected controller instance"); Method method = ResolvableMethod.on((Class) controller.getClass()).mockCall(mockCallConsumer).method(); return new HandlerMethod(controller, method); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index a50dc4a942..17420c5bff 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -36,6 +37,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodIntrospector; +import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.annotation.AnnotatedElementUtils; @@ -77,6 +79,7 @@ import org.springframework.web.method.annotation.ExpressionValueMethodArgumentRe import org.springframework.web.method.annotation.HandlerMethodValidator; import org.springframework.web.method.annotation.InitBinderDataBinderFactory; import org.springframework.web.method.annotation.MapMethodProcessor; +import org.springframework.web.method.annotation.ModelAttributeMethodProcessor; import org.springframework.web.method.annotation.ModelFactory; import org.springframework.web.method.annotation.ModelMethodProcessor; import org.springframework.web.method.annotation.RequestHeaderMapMethodArgumentResolver; @@ -91,6 +94,7 @@ import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite; import org.springframework.web.method.support.InvocableHandlerMethod; import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.service.invoker.RequestParamArgumentResolver; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.View; import org.springframework.web.servlet.mvc.annotation.ModelAndViewResolver; @@ -569,7 +573,11 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } if (BEAN_VALIDATION_PRESENT) { - this.methodValidator = HandlerMethodValidator.from(this.webBindingInitializer, this.parameterNameDiscoverer); + List resolvers = this.argumentResolvers.getResolvers(); + this.methodValidator = HandlerMethodValidator.from( + this.webBindingInitializer, this.parameterNameDiscoverer, + methodParamPredicate(resolvers, ModelAttributeMethodProcessor.class), + methodParamPredicate(resolvers, RequestParamArgumentResolver.class)); } } @@ -769,6 +777,19 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter return handlers; } + private static Predicate methodParamPredicate( + List resolvers, Class resolverType) { + + return parameter -> { + for (HandlerMethodArgumentResolver resolver : resolvers) { + if (resolver.supportsParameter(parameter)) { + return resolverType.isInstance(resolver); + } + } + return false; + }; + } + /** * Always return {@code true} since any method argument and return value diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java index 2ec4208fae..b67080b62c 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MethodValidationTests.java @@ -33,7 +33,6 @@ 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; @@ -259,7 +258,6 @@ public class MethodValidationTests { @SuppressWarnings("unchecked") private static HandlerMethod handlerMethod(T controller, Consumer mockCallConsumer) { - Assert.isTrue(!(controller instanceof Class), "Expected controller instance"); Method method = ResolvableMethod.on((Class) controller.getClass()).mockCall(mockCallConsumer).method(); return new HandlerMethod(controller, method); }