Consistent processing of binding/validation failures for data classes
Includes an extension of SmartValidator for candidate value validation, as well as nullability refinements in Validator and BindingResult. Issue: SPR-16840 Issue: SPR-16841 Issue: SPR-16854
This commit is contained in:
@@ -261,7 +261,7 @@ public class WebExchangeBindException extends ServerWebInputException implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordFieldValue(String field, Class<?> type, Object value) {
|
||||
public void recordFieldValue(String field, Class<?> type, @Nullable Object value) {
|
||||
this.bindingResult.recordFieldValue(field, type, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,14 @@ package org.springframework.web.method.annotation;
|
||||
import java.beans.ConstructorProperties;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -33,10 +39,11 @@ import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.validation.AbstractBindingResult;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.SmartValidator;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
@@ -189,7 +196,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||
* @return the created model attribute (never {@code null})
|
||||
* @throws BindException in case of constructor argument binding failure
|
||||
* @throws Exception in case of constructor invocation failure
|
||||
* @see #constructAttribute(Constructor, String, WebDataBinderFactory, NativeWebRequest)
|
||||
* @see #constructAttribute(Constructor, String, MethodParameter, WebDataBinderFactory, NativeWebRequest)
|
||||
* @see BeanUtils#findPrimaryConstructor(Class)
|
||||
*/
|
||||
protected Object createAttribute(String attributeName, MethodParameter parameter,
|
||||
@@ -214,7 +221,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||
}
|
||||
}
|
||||
|
||||
Object attribute = constructAttribute(ctor, attributeName, binderFactory, webRequest);
|
||||
Object attribute = constructAttribute(ctor, attributeName, parameter, binderFactory, webRequest);
|
||||
if (parameter != nestedParameter) {
|
||||
attribute = Optional.of(attribute);
|
||||
}
|
||||
@@ -233,11 +240,17 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||
* @return the created model attribute (never {@code null})
|
||||
* @throws BindException in case of constructor argument binding failure
|
||||
* @throws Exception in case of constructor invocation failure
|
||||
* @since 5.0
|
||||
* @since 5.1
|
||||
*/
|
||||
protected Object constructAttribute(Constructor<?> ctor, String attributeName,
|
||||
@SuppressWarnings("deprecation")
|
||||
protected Object constructAttribute(Constructor<?> ctor, String attributeName, MethodParameter parameter,
|
||||
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
|
||||
|
||||
Object constructed = constructAttribute(ctor, attributeName, binderFactory, webRequest);
|
||||
if (constructed != null) {
|
||||
return constructed;
|
||||
}
|
||||
|
||||
if (ctor.getParameterCount() == 0) {
|
||||
// A single default constructor -> clearly a standard JavaBeans arrangement.
|
||||
return BeanUtils.instantiateClass(ctor);
|
||||
@@ -256,6 +269,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||
String fieldDefaultPrefix = binder.getFieldDefaultPrefix();
|
||||
String fieldMarkerPrefix = binder.getFieldMarkerPrefix();
|
||||
boolean bindingFailure = false;
|
||||
Set<String> failedParams = new HashSet<>(4);
|
||||
|
||||
for (int i = 0; i < paramNames.length; i++) {
|
||||
String paramName = paramNames[i];
|
||||
@@ -272,7 +286,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||
}
|
||||
}
|
||||
try {
|
||||
MethodParameter methodParam = new MethodParameter(ctor, i);
|
||||
MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName);
|
||||
if (value == null && methodParam.isOptional()) {
|
||||
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
|
||||
}
|
||||
@@ -282,25 +296,43 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||
}
|
||||
catch (TypeMismatchException ex) {
|
||||
ex.initPropertyName(paramName);
|
||||
args[i] = value;
|
||||
failedParams.add(paramName);
|
||||
binder.getBindingResult().recordFieldValue(paramName, paramType, value);
|
||||
binder.getBindingErrorProcessor().processPropertyAccessException(ex, binder.getBindingResult());
|
||||
bindingFailure = true;
|
||||
args[i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (bindingFailure) {
|
||||
if (binder.getBindingResult() instanceof AbstractBindingResult) {
|
||||
AbstractBindingResult result = (AbstractBindingResult) binder.getBindingResult();
|
||||
for (int i = 0; i < paramNames.length; i++) {
|
||||
result.recordFieldValue(paramNames[i], paramTypes[i], args[i]);
|
||||
BindingResult result = binder.getBindingResult();
|
||||
for (int i = 0; i < paramNames.length; i++) {
|
||||
String paramName = paramNames[i];
|
||||
if (!failedParams.contains(paramName)) {
|
||||
result.recordFieldValue(paramName, paramTypes[i], args[i]);
|
||||
validateValueIfApplicable(binder, parameter, ctor.getDeclaringClass(), paramName, args[i]);
|
||||
}
|
||||
}
|
||||
throw new BindException(binder.getBindingResult());
|
||||
throw new BindException(result);
|
||||
}
|
||||
|
||||
return BeanUtils.instantiateClass(ctor, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new attribute instance with the given constructor.
|
||||
* @since 5.0
|
||||
* @deprecated as of 5.1, in favor of
|
||||
* {@link #constructAttribute(Constructor, String, MethodParameter, WebDataBinderFactory, NativeWebRequest)}
|
||||
*/
|
||||
@Deprecated
|
||||
@Nullable
|
||||
protected Object constructAttribute(Constructor<?> ctor, String attributeName,
|
||||
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension point to bind the request to the target object.
|
||||
* @param binder the data binder instance to use for the binding
|
||||
@@ -317,20 +349,72 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||
* and custom annotations whose name starts with "Valid".
|
||||
* @param binder the DataBinder to be used
|
||||
* @param parameter the method parameter declaration
|
||||
* @see WebDataBinder#validate(Object...)
|
||||
* @see SmartValidator#validate(Object, Errors, Object...)
|
||||
*/
|
||||
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
|
||||
Annotation[] annotations = parameter.getParameterAnnotations();
|
||||
for (Annotation ann : annotations) {
|
||||
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
|
||||
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
|
||||
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
|
||||
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
|
||||
for (Annotation ann : parameter.getParameterAnnotations()) {
|
||||
Object[] validationHints = determineValidationHints(ann);
|
||||
if (validationHints != null) {
|
||||
binder.validate(validationHints);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the specified candidate value if applicable.
|
||||
* <p>The default implementation checks for {@code @javax.validation.Valid},
|
||||
* Spring's {@link org.springframework.validation.annotation.Validated},
|
||||
* and custom annotations whose name starts with "Valid".
|
||||
* @param binder the DataBinder to be used
|
||||
* @param parameter the method parameter declaration
|
||||
* @param targetType the target type
|
||||
* @param fieldName the name of the field
|
||||
* @param value the candidate value
|
||||
* @since 5.1
|
||||
* @see #validateIfApplicable(WebDataBinder, MethodParameter)
|
||||
* @see SmartValidator#validateValue(Class, String, Object, Errors, Object...)
|
||||
*/
|
||||
protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter parameter,
|
||||
Class<?> targetType, String fieldName, @Nullable Object value) {
|
||||
|
||||
for (Annotation ann : parameter.getParameterAnnotations()) {
|
||||
Object[] validationHints = determineValidationHints(ann);
|
||||
if (validationHints != null) {
|
||||
for (Validator validator : binder.getValidators()) {
|
||||
if (validator instanceof SmartValidator) {
|
||||
try {
|
||||
((SmartValidator) validator).validateValue(targetType, fieldName, value,
|
||||
binder.getBindingResult(), validationHints);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// No corresponding field on the target class...
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine any validation triggered by the given annotation.
|
||||
* @param ann the annotation (potentially a validation annotation)
|
||||
* @return the validation hints to apply (possibly an empty array),
|
||||
* or {@code null} if this annotation does not trigger any validation
|
||||
* @since 5.1
|
||||
*/
|
||||
@Nullable
|
||||
private Object[] determineValidationHints(Annotation ann) {
|
||||
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
|
||||
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
|
||||
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
|
||||
return (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to raise a fatal bind exception on validation errors.
|
||||
* <p>The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}.
|
||||
@@ -380,4 +464,61 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link MethodParameter} subclass which detects field annotations as well.
|
||||
* @since 5.1
|
||||
*/
|
||||
private static class FieldAwareConstructorParameter extends MethodParameter {
|
||||
|
||||
private final String parameterName;
|
||||
|
||||
@Nullable
|
||||
private volatile Annotation[] combinedAnnotations;
|
||||
|
||||
public FieldAwareConstructorParameter(Constructor<?> constructor, int parameterIndex, String parameterName) {
|
||||
super(constructor, parameterIndex);
|
||||
this.parameterName = parameterName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Annotation[] getParameterAnnotations() {
|
||||
Annotation[] anns = this.combinedAnnotations;
|
||||
if (anns == null) {
|
||||
anns = super.getParameterAnnotations();
|
||||
try {
|
||||
Field field = getDeclaringClass().getDeclaredField(this.parameterName);
|
||||
Annotation[] fieldAnns = field.getAnnotations();
|
||||
if (fieldAnns.length > 0) {
|
||||
List<Annotation> merged = new ArrayList<>(anns.length + fieldAnns.length);
|
||||
merged.addAll(Arrays.asList(anns));
|
||||
for (Annotation fieldAnn : fieldAnns) {
|
||||
boolean existingType = false;
|
||||
for (Annotation ann : anns) {
|
||||
if (ann.annotationType() == fieldAnn.annotationType()) {
|
||||
existingType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!existingType) {
|
||||
merged.add(fieldAnn);
|
||||
}
|
||||
}
|
||||
anns = merged.toArray(new Annotation[0]);
|
||||
}
|
||||
}
|
||||
catch (NoSuchFieldException | SecurityException ex) {
|
||||
// ignore
|
||||
}
|
||||
this.combinedAnnotations = anns;
|
||||
}
|
||||
return anns;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParameterName() {
|
||||
return this.parameterName;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -160,9 +160,9 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Leave stack trace for later, e.g. AbstractHandlerExceptionResolver
|
||||
String message = ex.getMessage();
|
||||
if (!message.contains(parameter.getExecutable().toGenericString())) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
String message = ex.getMessage();
|
||||
if (message != null && !message.contains(parameter.getExecutable().toGenericString())) {
|
||||
logger.debug(formatArgumentError(parameter, message));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user