Add MethodParameter[] input to MethodValidationAdapter

This allows re-use of existing MethodParameter instances from controller
methods with cached metadata, and also ensures additional capabilities
such as looking up parameter annotations on interfaces.

See gh-29825
This commit is contained in:
rstoyanchev 2023-06-13 17:40:32 +01:00
parent e7c3e1c516
commit 85d81024a4
8 changed files with 94 additions and 57 deletions

View File

@ -18,6 +18,7 @@ package org.springframework.validation.beanvalidation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
@ -44,31 +45,45 @@ public class DefaultMethodValidator implements MethodValidator {
} }
@Override @Override
public void validateArguments(Object target, Method method, Object[] arguments, Class<?>[] groups) { public void validateArguments(
MethodValidationResult result = this.adapter.validateMethodArguments(target, method, arguments, groups); Object target, Method method, @Nullable MethodParameter[] parameters, Object[] arguments,
handleArgumentsResult(target, method, arguments, groups, result); Class<?>[] groups) {
handleArgumentsValidationResult(target, method, arguments, groups,
this.adapter.validateMethodArguments(target, method, parameters, arguments, groups));
}
public void validateReturnValue(
Object target, Method method, @Nullable MethodParameter returnType, @Nullable Object returnValue,
Class<?>[] groups) {
handleReturnValueValidationResult(target, method, returnValue, groups,
this.adapter.validateMethodReturnValue(target, method, returnType, returnValue, groups));
} }
/** /**
* Subclasses can override this to handle the result of argument validation. * Subclasses can override this to handle the result of argument validation.
* By default, {@link MethodValidationResult#throwIfViolationsPresent()} is called. * By default, {@link MethodValidationResult#throwIfViolationsPresent()} is called.
* @param bean the target Object for method invocation
* @param method the target method
* @param arguments the candidate argument values to validate
* @param groups groups for validation determined via
*/ */
protected void handleArgumentsResult( protected void handleArgumentsValidationResult(
Object bean, Method method, Object[] arguments, Class<?>[] groups, MethodValidationResult result) { Object bean, Method method, Object[] arguments, Class<?>[] groups, MethodValidationResult result) {
result.throwIfViolationsPresent(); result.throwIfViolationsPresent();
} }
public void validateReturnValue(Object target, Method method, @Nullable Object returnValue, Class<?>[] groups) {
MethodValidationResult result = this.adapter.validateMethodReturnValue(target, method, returnValue, groups);
handleReturnValueResult(target, method, returnValue, groups, result);
}
/** /**
* Subclasses can override this to handle the result of return value validation. * Subclasses can override this to handle the result of return value validation.
* By default, {@link MethodValidationResult#throwIfViolationsPresent()} is called. * By default, {@link MethodValidationResult#throwIfViolationsPresent()} is called.
* @param bean the target Object for method invocation
* @param method the target method
* @param returnValue the return value to validate
* @param groups groups for validation determined via
*/ */
protected void handleReturnValueResult( protected void handleReturnValueValidationResult(
Object bean, Method method, @Nullable Object returnValue, Class<?>[] groups, MethodValidationResult result) { Object bean, Method method, @Nullable Object returnValue, Class<?>[] groups, MethodValidationResult result) {
result.throwIfViolationsPresent(); result.throwIfViolationsPresent();

View File

@ -176,8 +176,8 @@ public class MethodValidationAdapter {
/** /**
* Use this method determine the validation groups to pass into * Use this method determine the validation groups to pass into
* {@link #validateMethodArguments(Object, Method, Object[], Class[])} and * {@link #validateMethodArguments(Object, Method, MethodParameter[], Object[], Class[])} and
* {@link #validateMethodReturnValue(Object, Method, Object, Class[])}. * {@link #validateMethodReturnValue(Object, Method, MethodParameter, Object, Class[])}.
* <p>Default are the validation groups as specified in the {@link Validated} * <p>Default are the validation groups as specified in the {@link Validated}
* annotation on the method, or on the containing target class of the method, * annotation on the method, or on the containing target class of the method,
* or for an AOP proxy without a target (with all behavior in advisors), also * or for an AOP proxy without a target (with all behavior in advisors), also
@ -208,7 +208,8 @@ public class MethodValidationAdapter {
* Validate the given method arguments and return the result of validation. * Validate the given method arguments and return the result of validation.
* @param target the target Object * @param target the target Object
* @param method the target method * @param method the target method
* @param arguments candidate arguments for a method invocation * @param parameters the parameters, if already created and available
* @param arguments the candidate argument values to validate
* @param groups groups for validation determined via * @param groups groups for validation determined via
* {@link #determineValidationGroups(Object, Method)} * {@link #determineValidationGroups(Object, Method)}
* @return a result with {@link ConstraintViolation violations} and * @return a result with {@link ConstraintViolation violations} and
@ -216,7 +217,8 @@ public class MethodValidationAdapter {
* in case there are no violations * in case there are no violations
*/ */
public MethodValidationResult validateMethodArguments( public MethodValidationResult validateMethodArguments(
Object target, Method method, Object[] arguments, Class<?>[] groups) { Object target, Method method, @Nullable MethodParameter[] parameters, Object[] arguments,
Class<?>[] groups) {
ExecutableValidator execVal = this.validator.get().forExecutables(); ExecutableValidator execVal = this.validator.get().forExecutables();
Set<ConstraintViolation<Object>> result; Set<ConstraintViolation<Object>> result;
@ -231,14 +233,18 @@ public class MethodValidationAdapter {
result = execVal.validateParameters(target, bridgedMethod, arguments, groups); result = execVal.validateParameters(target, bridgedMethod, arguments, groups);
} }
return (result.isEmpty() ? EMPTY_RESULT : return (result.isEmpty() ? EMPTY_RESULT :
createException(target, method, result, i -> arguments[i], false)); createException(target, method, result,
i -> parameters != null ? parameters[i] : new MethodParameter(method, i),
i -> arguments[i],
false));
} }
/** /**
* Validate the given return value and return the result of validation. * Validate the given return value and return the result of validation.
* @param target the target Object * @param target the target Object
* @param method the target method * @param method the target method
* @param returnValue value returned from invoking the target method * @param returnType the return parameter, if already created and available
* @param returnValue the return value to validate
* @param groups groups for validation determined via * @param groups groups for validation determined via
* {@link #determineValidationGroups(Object, Method)} * {@link #determineValidationGroups(Object, Method)}
* @return a result with {@link ConstraintViolation violations} and * @return a result with {@link ConstraintViolation violations} and
@ -246,16 +252,22 @@ public class MethodValidationAdapter {
* in case there are no violations * in case there are no violations
*/ */
public MethodValidationResult validateMethodReturnValue( public MethodValidationResult validateMethodReturnValue(
Object target, Method method, @Nullable Object returnValue, Class<?>[] groups) { Object target, Method method, @Nullable MethodParameter returnType, @Nullable Object returnValue,
Class<?>[] groups) {
ExecutableValidator execVal = this.validator.get().forExecutables(); ExecutableValidator execVal = this.validator.get().forExecutables();
Set<ConstraintViolation<Object>> result = execVal.validateReturnValue(target, method, returnValue, groups); Set<ConstraintViolation<Object>> result = execVal.validateReturnValue(target, method, returnValue, groups);
return (result.isEmpty() ? EMPTY_RESULT : createException(target, method, result, i -> returnValue, true)); return (result.isEmpty() ? EMPTY_RESULT :
createException(target, method, result,
i -> returnType != null ? returnType : new MethodParameter(method, -1),
i -> returnValue,
true));
} }
private MethodValidationException createException( private MethodValidationException createException(
Object target, Method method, Set<ConstraintViolation<Object>> violations, Object target, Method method, Set<ConstraintViolation<Object>> violations,
Function<Integer, Object> argumentFunction, boolean forReturnValue) { Function<Integer, MethodParameter> parameterFunction, Function<Integer, Object> argumentFunction,
boolean forReturnValue) {
Map<MethodParameter, ValueResultBuilder> parameterViolations = new LinkedHashMap<>(); Map<MethodParameter, ValueResultBuilder> parameterViolations = new LinkedHashMap<>();
Map<Path.Node, BeanResultBuilder> cascadedViolations = new LinkedHashMap<>(); Map<Path.Node, BeanResultBuilder> cascadedViolations = new LinkedHashMap<>();
@ -267,10 +279,11 @@ public class MethodValidationAdapter {
MethodParameter parameter; MethodParameter parameter;
if (node.getKind().equals(ElementKind.PARAMETER)) { if (node.getKind().equals(ElementKind.PARAMETER)) {
parameter = new MethodParameter(method, node.as(Path.ParameterNode.class).getParameterIndex()); int index = node.as(Path.ParameterNode.class).getParameterIndex();
parameter = parameterFunction.apply(index);
} }
else if (node.getKind().equals(ElementKind.RETURN_VALUE)) { else if (node.getKind().equals(ElementKind.RETURN_VALUE)) {
parameter = new MethodParameter(method, -1); parameter = parameterFunction.apply(-1);
} }
else { else {
continue; continue;

View File

@ -104,12 +104,12 @@ public class MethodValidationInterceptor implements MethodInterceptor {
Method method = invocation.getMethod(); Method method = invocation.getMethod();
Class<?>[] groups = determineValidationGroups(invocation); Class<?>[] groups = determineValidationGroups(invocation);
this.delegate.validateMethodArguments(target, method, invocation.getArguments(), groups) this.delegate.validateMethodArguments(target, method, null, invocation.getArguments(), groups)
.throwIfViolationsPresent(); .throwIfViolationsPresent();
Object returnValue = invocation.proceed(); Object returnValue = invocation.proceed();
this.delegate.validateMethodReturnValue(target, method, returnValue, groups) this.delegate.validateMethodReturnValue(target, method, null, returnValue, groups)
.throwIfViolationsPresent(); .throwIfViolationsPresent();
return returnValue; return returnValue;

View File

@ -18,6 +18,7 @@ package org.springframework.validation.beanvalidation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
@ -35,8 +36,8 @@ public interface MethodValidator {
/** /**
* Use this method determine the validation groups to pass into * Use this method determine the validation groups to pass into
* {@link #validateArguments(Object, Method, Object[], Class[])} and * {@link #validateArguments(Object, Method, MethodParameter[], Object[], Class[])} and
* {@link #validateReturnValue(Object, Method, Object, Class[])}. * {@link #validateReturnValue(Object, Method, MethodParameter, Object, Class[])}.
* @param target the target Object * @param target the target Object
* @param method the target method * @param method the target method
* @return the applicable validation groups as a {@code Class} array * @return the applicable validation groups as a {@code Class} array
@ -48,24 +49,30 @@ public interface MethodValidator {
* Validate the given method arguments and return the result of validation. * Validate the given method arguments and return the result of validation.
* @param target the target Object * @param target the target Object
* @param method the target method * @param method the target method
* @param arguments candidate arguments for a method invocation * @param parameters the parameters, if already created and available
* @param arguments the candidate argument values to validate
* @param groups groups for validation determined via * @param groups groups for validation determined via
* {@link #determineValidationGroups(Object, Method)} * {@link #determineValidationGroups(Object, Method)}
* @throws MethodValidationException should be raised in case of validation * @throws MethodValidationException should be raised in case of validation
* errors unless the implementation handles those errors otherwise (e.g. * errors unless the implementation handles those errors otherwise (e.g.
* by injecting {@code BindingResult} into the method). * by injecting {@code BindingResult} into the method).
*/ */
void validateArguments(Object target, Method method, Object[] arguments, Class<?>[] groups); void validateArguments(
Object target, Method method, @Nullable MethodParameter[] parameters, Object[] arguments,
Class<?>[] groups);
/** /**
* Validate the given return value and return the result of validation. * Validate the given return value and return the result of validation.
* @param target the target Object * @param target the target Object
* @param method the target method * @param method the target method
* @param returnValue value returned from invoking the target method * @param returnType the return parameter, if already created and available
* @param returnValue the return value to validate
* @param groups groups for validation determined via * @param groups groups for validation determined via
* {@link #determineValidationGroups(Object, Method)} * {@link #determineValidationGroups(Object, Method)}
* @throws MethodValidationException in case of validation errors * @throws MethodValidationException in case of validation errors
*/ */
void validateReturnValue(Object target, Method method, @Nullable Object returnValue, Class<?>[] groups); void validateReturnValue(
Object target, Method method, @Nullable MethodParameter returnType, @Nullable Object returnValue,
Class<?>[] groups);
} }

View File

@ -181,14 +181,14 @@ public class MethodValidationAdapterTests {
Object target, Method method, Object[] arguments, Consumer<MethodValidationResult> assertions) { Object target, Method method, Object[] arguments, Consumer<MethodValidationResult> assertions) {
assertions.accept( assertions.accept(
this.validationAdapter.validateMethodArguments(target, method, arguments, new Class<?>[0])); this.validationAdapter.validateMethodArguments(target, method, null, arguments, new Class<?>[0]));
} }
private void validateReturnValue( private void validateReturnValue(
Object target, Method method, @Nullable Object returnValue, Consumer<MethodValidationResult> assertions) { Object target, Method method, @Nullable Object returnValue, Consumer<MethodValidationResult> assertions) {
assertions.accept( assertions.accept(
this.validationAdapter.validateMethodReturnValue(target, method, returnValue, new Class<?>[0])); this.validationAdapter.validateMethodReturnValue(target, method, null, returnValue, new Class<?>[0]));
} }
private static void assertBeanResult( private static void assertBeanResult(

View File

@ -46,28 +46,16 @@ import org.springframework.web.method.annotation.ModelFactory;
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 6.1 * @since 6.1
*/ */
public class HandlerMethodValidator extends DefaultMethodValidator { public final class HandlerMethodValidator extends DefaultMethodValidator {
public HandlerMethodValidator(MethodValidationAdapter adapter) { private HandlerMethodValidator(MethodValidationAdapter adapter) {
super(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 @Override
protected void handleArgumentsResult( protected void handleArgumentsValidationResult(
Object bean, Method method, Object[] arguments, Class<?>[] groups, MethodValidationResult result) { Object bean, Method method, Object[] arguments, Class<?>[] groups, MethodValidationResult result) {
if (result.getConstraintViolations().isEmpty()) { if (result.getConstraintViolations().isEmpty()) {
@ -93,12 +81,21 @@ public class HandlerMethodValidator extends DefaultMethodValidator {
result.throwIfViolationsPresent(); result.throwIfViolationsPresent();
} }
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));
}
}
/** /**
* Create a {@link MethodValidator} if Bean Validation is enabled in Spring MVC or WebFlux. * Static factory method to create a {@link HandlerMethodValidator} if Bean
* @param bindingInitializer for the configured Validator and MessageCodesResolver * Validation is enabled in Spring MVC or WebFlux.
* @param parameterNameDiscoverer the {@code ParameterNameDiscoverer} to use
* for {@link MethodValidationAdapter#setParameterNameDiscoverer}
*/ */
@Nullable @Nullable
public static MethodValidator from( public static MethodValidator from(
@ -107,15 +104,17 @@ public class HandlerMethodValidator extends DefaultMethodValidator {
if (bindingInitializer instanceof ConfigurableWebBindingInitializer configurableInitializer) { if (bindingInitializer instanceof ConfigurableWebBindingInitializer configurableInitializer) {
if (configurableInitializer.getValidator() instanceof Validator validator) { if (configurableInitializer.getValidator() instanceof Validator validator) {
MethodValidationAdapter validationAdapter = new MethodValidationAdapter(validator); MethodValidationAdapter adapter = new MethodValidationAdapter(validator);
if (parameterNameDiscoverer != null) { if (parameterNameDiscoverer != null) {
validationAdapter.setParameterNameDiscoverer(parameterNameDiscoverer); adapter.setParameterNameDiscoverer(parameterNameDiscoverer);
} }
MessageCodesResolver codesResolver = configurableInitializer.getMessageCodesResolver(); MessageCodesResolver codesResolver = configurableInitializer.getMessageCodesResolver();
if (codesResolver != null) { if (codesResolver != null) {
validationAdapter.setMessageCodesResolver(codesResolver); adapter.setMessageCodesResolver(codesResolver);
} }
return new HandlerMethodValidator(validationAdapter); HandlerMethodValidator methodValidator = new HandlerMethodValidator(adapter);
adapter.setBindingResultNameResolver(methodValidator::determineObjectName);
return methodValidator;
} }
} }
return null; return null;

View File

@ -168,13 +168,15 @@ public class InvocableHandlerMethod extends HandlerMethod {
Class<?>[] groups = getValidationGroups(); Class<?>[] groups = getValidationGroups();
if (shouldValidateArguments() && this.methodValidator != null) { if (shouldValidateArguments() && this.methodValidator != null) {
this.methodValidator.validateArguments(getBean(), getBridgedMethod(), args, groups); this.methodValidator.validateArguments(
getBean(), getBridgedMethod(), getMethodParameters(), args, groups);
} }
Object returnValue = doInvoke(args); Object returnValue = doInvoke(args);
if (shouldValidateReturnValue() && this.methodValidator != null) { if (shouldValidateReturnValue() && this.methodValidator != null) {
this.methodValidator.validateReturnValue(getBean(), getBridgedMethod(), returnValue, groups); this.methodValidator.validateReturnValue(
getBean(), getBridgedMethod(), getReturnType(), returnValue, groups);
} }
return returnValue; return returnValue;

View File

@ -152,7 +152,8 @@ public class InvocableHandlerMethod extends HandlerMethod {
return getMethodArgumentValues(exchange, bindingContext, providedArgs).flatMap(args -> { return getMethodArgumentValues(exchange, bindingContext, providedArgs).flatMap(args -> {
Class<?>[] groups = getValidationGroups(); Class<?>[] groups = getValidationGroups();
if (shouldValidateArguments() && this.methodValidator != null) { if (shouldValidateArguments() && this.methodValidator != null) {
this.methodValidator.validateArguments(getBean(), getBridgedMethod(), args, groups); this.methodValidator.validateArguments(
getBean(), getBridgedMethod(), getMethodParameters(), args, groups);
} }
Object value; Object value;
Method method = getBridgedMethod(); Method method = getBridgedMethod();