Support for populating model attributes through data class constructors
Includes a new overloaded ModelAndView constructor with an HttpStatus argument, as well as a HandlerMethodArgumentResolverSupport refactoring (revised checkParameterType signature, actually implementing the HandlerMethodArgumentResolver interface). Issue: SPR-15199
This commit is contained in:
@@ -16,15 +16,20 @@
|
||||
|
||||
package org.springframework.web.method.annotation;
|
||||
|
||||
import java.beans.ConstructorProperties;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -52,10 +57,13 @@ import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
* attribute with or without the presence of an {@code @ModelAttribute}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @since 3.1
|
||||
*/
|
||||
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
|
||||
|
||||
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final boolean annotationNotRequired;
|
||||
@@ -65,7 +73,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||
* Class constructor.
|
||||
* @param annotationNotRequired if "true", non-simple method arguments and
|
||||
* return values are considered model attributes with or without a
|
||||
* {@code @ModelAttribute} annotation.
|
||||
* {@code @ModelAttribute} annotation
|
||||
*/
|
||||
public ModelAttributeMethodProcessor(boolean annotationNotRequired) {
|
||||
this.annotationNotRequired = annotationNotRequired;
|
||||
@@ -89,8 +97,8 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||
* with request values via data binding and optionally validated
|
||||
* if {@code @java.validation.Valid} is present on the argument.
|
||||
* @throws BindException if data binding and validation result in an error
|
||||
* and the next method parameter is not of type {@link Errors}.
|
||||
* @throws Exception if WebDataBinder initialization fails.
|
||||
* and the next method parameter is not of type {@link Errors}
|
||||
* @throws Exception if WebDataBinder initialization fails
|
||||
*/
|
||||
@Override
|
||||
public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
|
||||
@@ -123,22 +131,54 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||
mavContainer.removeAttributes(bindingResultModel);
|
||||
mavContainer.addAllAttributes(bindingResultModel);
|
||||
|
||||
return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
|
||||
return (parameter.getParameterType().isInstance(attribute) ? attribute :
|
||||
binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension point to create the model attribute if not found in the model.
|
||||
* The default implementation uses the default constructor.
|
||||
* Extension point to create the model attribute if not found in the model,
|
||||
* with subsequent parameter binding through bean properties (unless suppressed).
|
||||
* <p>The default implementation uses the unique public no-arg constructor, if any,
|
||||
* which may have arguments: It understands the JavaBeans {@link ConstructorProperties}
|
||||
* annotation as well as runtime-retained parameter names in the bytecode,
|
||||
* associating request parameters with constructor arguments by name. If no such
|
||||
* constructor is found, the default constructor will be used (even if not public),
|
||||
* assuming subsequent bean property bindings through setter methods.
|
||||
* @param attributeName the name of the attribute (never {@code null})
|
||||
* @param methodParam the method parameter
|
||||
* @param parameter the method parameter declaration
|
||||
* @param binderFactory for creating WebDataBinder instance
|
||||
* @param request the current request
|
||||
* @param webRequest the current request
|
||||
* @return the created model attribute (never {@code null})
|
||||
*/
|
||||
protected Object createAttribute(String attributeName, MethodParameter methodParam,
|
||||
WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
|
||||
protected Object createAttribute(String attributeName, MethodParameter parameter,
|
||||
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
|
||||
|
||||
return BeanUtils.instantiateClass(methodParam.getParameterType());
|
||||
Constructor<?>[] ctors = parameter.getParameterType().getConstructors();
|
||||
if (ctors.length != 1) {
|
||||
// No standard data class or standard JavaBeans arrangement ->
|
||||
// defensively go with default constructor, expecting regular bean property bindings.
|
||||
return BeanUtils.instantiateClass(parameter.getParameterType());
|
||||
}
|
||||
Constructor<?> ctor = ctors[0];
|
||||
if (ctor.getParameterCount() == 0) {
|
||||
// A single default constructor -> clearly a standard JavaBeans arrangement.
|
||||
return BeanUtils.instantiateClass(ctor);
|
||||
}
|
||||
|
||||
// A single data class constructor -> resolve constructor arguments from request parameters.
|
||||
ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class);
|
||||
String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor));
|
||||
Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor);
|
||||
Class<?>[] paramTypes = ctor.getParameterTypes();
|
||||
Assert.state(paramNames.length == paramTypes.length,
|
||||
() -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor);
|
||||
Object[] args = new Object[paramTypes.length];
|
||||
WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName);
|
||||
for (int i = 0; i < paramNames.length; i++) {
|
||||
args[i] = binder.convertIfNecessary(
|
||||
webRequest.getParameterValues(paramNames[i]), paramTypes[i], new MethodParameter(ctor, i));
|
||||
}
|
||||
return BeanUtils.instantiateClass(ctor, args);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,10 +196,10 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||
* Spring's {@link org.springframework.validation.annotation.Validated},
|
||||
* and custom annotations whose name starts with "Valid".
|
||||
* @param binder the DataBinder to be used
|
||||
* @param methodParam the method parameter
|
||||
* @param parameter the method parameter declaration
|
||||
*/
|
||||
protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) {
|
||||
Annotation[] annotations = methodParam.getParameterAnnotations();
|
||||
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")) {
|
||||
@@ -174,12 +214,12 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||
/**
|
||||
* Whether to raise a fatal bind exception on validation errors.
|
||||
* @param binder the data binder used to perform data binding
|
||||
* @param methodParam the method argument
|
||||
* @param parameter the method parameter declaration
|
||||
* @return {@code true} if the next method argument is not of type {@link Errors}
|
||||
*/
|
||||
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
|
||||
int i = methodParam.getParameterIndex();
|
||||
Class<?>[] paramTypes = methodParam.getMethod().getParameterTypes();
|
||||
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
|
||||
int i = parameter.getParameterIndex();
|
||||
Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
|
||||
boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
|
||||
return !hasBindingResult;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user