ModelAttributeMethodProcessor.java 11.2 KB
Newer Older
1
/*
J
Juergen Hoeller 已提交
2
 * Copyright 2002-2017 the original author or authors.
3 4 5 6 7 8 9 10 11 12 13 14 15 16
 *
 * 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
 *
 *      http://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.
 */

17
package org.springframework.web.method.annotation;
18

19
import java.beans.ConstructorProperties;
20
import java.lang.annotation.Annotation;
21
import java.lang.reflect.Constructor;
22
import java.util.Map;
23

24 25
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
26

27
import org.springframework.beans.BeanUtils;
28
import org.springframework.core.DefaultParameterNameDiscoverer;
29
import org.springframework.core.MethodParameter;
30
import org.springframework.core.ParameterNameDiscoverer;
31
import org.springframework.core.annotation.AnnotationUtils;
32
import org.springframework.lang.Nullable;
33
import org.springframework.util.Assert;
34 35
import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
36
import org.springframework.validation.annotation.Validated;
37 38 39 40 41 42 43 44 45 46
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.bind.support.WebRequestDataBinder;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;

/**
R
Polish  
Rossen Stoyanchev 已提交
47 48
 * Resolve {@code @ModelAttribute} annotated method arguments and handle
 * return values from {@code @ModelAttribute} annotated methods.
49
 *
R
Polish  
Rossen Stoyanchev 已提交
50 51 52 53
 * <p>Model attributes are obtained from the model or created with a default
 * constructor (and then added to the model). Once created the attribute is
 * populated via data binding to Servlet request parameters. Validation may be
 * applied if the argument is annotated with {@code @javax.validation.Valid}.
54
 * or Spring's own {@code @org.springframework.validation.annotation.Validated}.
55
 *
R
Polish  
Rossen Stoyanchev 已提交
56
 * <p>When this handler is created with {@code annotationNotRequired=true}
57
 * any non-simple type argument and return value is regarded as a model
58
 * attribute with or without the presence of an {@code @ModelAttribute}.
59
 *
60
 * @author Rossen Stoyanchev
61
 * @author Juergen Hoeller
62
 * @author Sebastien Deleuze
63 64
 * @since 3.1
 */
J
Juergen Hoeller 已提交
65
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
66

67 68
	private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

69
	protected final Log logger = LogFactory.getLog(getClass());
70

71
	private final boolean annotationNotRequired;
72

73

74
	/**
R
Polish  
Rossen Stoyanchev 已提交
75
	 * Class constructor.
76
	 * @param annotationNotRequired if "true", non-simple method arguments and
77
	 * return values are considered model attributes with or without a
78
	 * {@code @ModelAttribute} annotation
79
	 */
80 81
	public ModelAttributeMethodProcessor(boolean annotationNotRequired) {
		this.annotationNotRequired = annotationNotRequired;
82 83
	}

84

85
	/**
R
Polish  
Rossen Stoyanchev 已提交
86 87 88
	 * Returns {@code true} if the parameter is annotated with
	 * {@link ModelAttribute} or, if in default resolution mode, for any
	 * method parameter that is not a simple type.
89
	 */
90
	@Override
91
	public boolean supportsParameter(MethodParameter parameter) {
R
Polish  
Rossen Stoyanchev 已提交
92 93
		return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
				(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
94 95 96
	}

	/**
97 98
	 * Resolve the argument from the model or if not found instantiate it with
	 * its default if it is available. The model attribute is then populated
99 100 101
	 * 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
102 103
	 * and the next method parameter is not of type {@link Errors}
	 * @throws Exception if WebDataBinder initialization fails
104
	 */
105
	@Override
106 107 108 109 110
	public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
		Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
111

112
		String name = ModelFactory.getNameForParameter(parameter);
R
Polish  
Rossen Stoyanchev 已提交
113 114
		Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) :
				createAttribute(name, parameter, binderFactory, webRequest));
115

116
		if (!mavContainer.isBindingDisabled(name)) {
117 118
			ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
			if (ann != null && !ann.binding()) {
119 120 121 122
				mavContainer.setBindingDisabled(name);
			}
		}

123
		WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
124
		if (binder.getTarget() != null) {
125 126 127
			if (!mavContainer.isBindingDisabled(name)) {
				bindRequestParameters(binder, webRequest);
			}
128
			validateIfApplicable(binder, parameter);
129 130
			if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
				throw new BindException(binder.getBindingResult());
131 132 133
			}
		}

134 135 136 137 138
		// Add resolved attribute and BindingResult at the end of the model
		Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
		mavContainer.removeAttributes(bindingResultModel);
		mavContainer.addAllAttributes(bindingResultModel);

139 140
		return (parameter.getParameterType().isInstance(attribute) ? attribute :
				binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter));
141 142 143
	}

	/**
144 145 146 147 148 149 150 151
	 * 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.
152
	 * @param attributeName the name of the attribute (never {@code null})
153
	 * @param parameter the method parameter declaration
154
	 * @param binderFactory for creating WebDataBinder instance
155
	 * @param webRequest the current request
156
	 * @return the created model attribute (never {@code null})
157
	 */
158 159 160
	protected Object createAttribute(String attributeName, MethodParameter parameter,
			WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {

161 162 163 164 165
		Class<?> type = parameter.getParameterType();
		
		Constructor<?> ctor = BeanUtils.findPrimaryConstructor(type);
		if (ctor == null) {
			throw new IllegalStateException("No primary constructor found for " + type.getName());
166
		}
167

168 169 170 171
		if (ctor.getParameterCount() == 0) {
			// A single default constructor -> clearly a standard JavaBeans arrangement.
			return BeanUtils.instantiateClass(ctor);
		}
172

173 174 175 176 177 178 179 180 181 182
		// 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++) {
183 184 185
			String[] parameterValues = webRequest.getParameterValues(paramNames[i]);
			args[i] = (parameterValues != null ? binder.convertIfNecessary(parameterValues, paramTypes[i],
					new MethodParameter(ctor, i)) : null);
186 187
		}
		return BeanUtils.instantiateClass(ctor, args);
188
	}
189

190
	/**
191 192
	 * Extension point to bind the request to the target object.
	 * @param binder the data binder instance to use for the binding
193 194
	 * @param request the current request
	 */
195
	protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
196 197 198 199
		((WebRequestDataBinder) binder).bind(request);
	}

	/**
200
	 * Validate the model attribute if applicable.
201 202 203
	 * <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".
204
	 * @param binder the DataBinder to be used
205
	 * @param parameter the method parameter declaration
206
	 */
207 208
	protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
		Annotation[] annotations = parameter.getParameterAnnotations();
209
		for (Annotation ann : annotations) {
210 211 212 213 214
			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});
				binder.validate(validationHints);
215
				break;
216 217 218 219 220
			}
		}
	}

	/**
221
	 * Whether to raise a fatal bind exception on validation errors.
222
	 * @param binder the data binder used to perform data binding
223
	 * @param parameter the method parameter declaration
224
	 * @return {@code true} if the next method argument is not of type {@link Errors}
225
	 */
226 227
	protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
		int i = parameter.getParameterIndex();
228
		Class<?>[] paramTypes = parameter.getExecutable().getParameterTypes();
229 230 231 232
		boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
		return !hasBindingResult;
	}

233
	/**
234
	 * Return {@code true} if there is a method-level {@code @ModelAttribute}
R
Polish  
Rossen Stoyanchev 已提交
235 236
	 * or, in default resolution mode, for any return value type that is not
	 * a simple type.
237
	 */
238
	@Override
239
	public boolean supportsReturnType(MethodParameter returnType) {
240 241
		return (returnType.hasMethodAnnotation(ModelAttribute.class) ||
				(this.annotationNotRequired && !BeanUtils.isSimpleProperty(returnType.getParameterType())));
242 243
	}

244 245 246
	/**
	 * Add non-null return values to the {@link ModelAndViewContainer}.
	 */
247
	@Override
248
	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
249
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
250

251 252
		if (returnValue != null) {
			String name = ModelFactory.getNameForReturnValue(returnValue, returnType);
253
			mavContainer.addAttribute(name, returnValue);
254
		}
255
	}
256

257
}