ModelAttributeMethodProcessor.java 15.3 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
import java.util.Optional;
24

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

28
import org.springframework.beans.BeanUtils;
29
import org.springframework.beans.TypeMismatchException;
30
import org.springframework.core.DefaultParameterNameDiscoverer;
31
import org.springframework.core.MethodParameter;
32
import org.springframework.core.ParameterNameDiscoverer;
33
import org.springframework.core.annotation.AnnotationUtils;
34
import org.springframework.lang.Nullable;
35
import org.springframework.util.Assert;
36
import org.springframework.validation.BindException;
37
import org.springframework.validation.BindingResult;
38
import org.springframework.validation.Errors;
39
import org.springframework.validation.FieldError;
40
import org.springframework.validation.annotation.Validated;
41 42 43 44 45 46 47 48 49 50
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 已提交
51 52
 * Resolve {@code @ModelAttribute} annotated method arguments and handle
 * return values from {@code @ModelAttribute} annotated methods.
53
 *
R
Polish  
Rossen Stoyanchev 已提交
54 55 56 57
 * <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}.
58
 * or Spring's own {@code @org.springframework.validation.annotation.Validated}.
59
 *
R
Polish  
Rossen Stoyanchev 已提交
60
 * <p>When this handler is created with {@code annotationNotRequired=true}
61
 * any non-simple type argument and return value is regarded as a model
62
 * attribute with or without the presence of an {@code @ModelAttribute}.
63
 *
64
 * @author Rossen Stoyanchev
65
 * @author Juergen Hoeller
66
 * @author Sebastien Deleuze
67 68
 * @since 3.1
 */
J
Juergen Hoeller 已提交
69
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
70

71 72
	private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

73
	protected final Log logger = LogFactory.getLog(getClass());
74

75
	private final boolean annotationNotRequired;
76

77

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

88

89
	/**
R
Polish  
Rossen Stoyanchev 已提交
90 91 92
	 * 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.
93
	 */
94
	@Override
95
	public boolean supportsParameter(MethodParameter parameter) {
R
Polish  
Rossen Stoyanchev 已提交
96 97
		return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
				(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
98 99 100
	}

	/**
101 102
	 * 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
103 104 105
	 * 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
106 107
	 * and the next method parameter is not of type {@link Errors}
	 * @throws Exception if WebDataBinder initialization fails
108
	 */
109
	@Override
110
	@Nullable
111 112 113 114 115
	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");
116

117
		String name = ModelFactory.getNameForParameter(parameter);
118 119 120
		ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
		if (ann != null) {
			mavContainer.setBinding(name, ann.binding());
121 122
		}

123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
		Object attribute = null;
		BindingResult bindingResult = null;

		if (mavContainer.containsAttribute(name)) {
			attribute = mavContainer.getModel().get(name);
		}
		else {
			// Create attribute instance
			try {
				attribute = createAttribute(name, parameter, binderFactory, webRequest);
			}
			catch (BindException ex) {
				if (isBindExceptionRequired(parameter)) {
					// No BindingResult parameter -> fail with BindException
					throw ex;
				}
				// Otherwise, expose null/empty value and associated BindingResult
				if (parameter.getParameterType() == Optional.class) {
					attribute = Optional.empty();
				}
				bindingResult = ex.getBindingResult();
			}
		}

		if (bindingResult == null) {
			// Bean property binding and validation;
			// skipped in case of binding failure on construction.
			WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
			if (binder.getTarget() != null) {
				if (!mavContainer.isBindingDisabled(name)) {
					bindRequestParameters(binder, webRequest);
				}
				validateIfApplicable(binder, parameter);
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new BindException(binder.getBindingResult());
				}
159
			}
160 161 162
			// Value type adaptation, also covering java.util.Optional
			if (!parameter.getParameterType().isInstance(attribute)) {
				attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
163
			}
164
			bindingResult = binder.getBindingResult();
165 166
		}

167
		// Add resolved attribute and BindingResult at the end of the model
168
		Map<String, Object> bindingResultModel = bindingResult.getModel();
169 170 171
		mavContainer.removeAttributes(bindingResultModel);
		mavContainer.addAllAttributes(bindingResultModel);

172
		return attribute;
173 174 175
	}

	/**
176 177
	 * Extension point to create the model attribute if not found in the model,
	 * with subsequent parameter binding through bean properties (unless suppressed).
178 179 180 181 182 183 184
	 * <p>The default implementation typically uses the unique public no-arg constructor
	 * if available but also handles a "primary constructor" approach for data classes:
	 * 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.
185
	 * @param attributeName the name of the attribute (never {@code null})
186
	 * @param parameter the method parameter declaration
187
	 * @param binderFactory for creating WebDataBinder instance
188
	 * @param webRequest the current request
189
	 * @return the created model attribute (never {@code null})
190 191 192 193
	 * @throws BindException in case of constructor argument binding failure
	 * @throws Exception in case of constructor invocation failure
	 * @see #constructAttribute(Constructor, String, WebDataBinderFactory, NativeWebRequest)
	 * @see BeanUtils#findPrimaryConstructor(Class)
194
	 */
195 196 197
	protected Object createAttribute(String attributeName, MethodParameter parameter,
			WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {

198
		MethodParameter nestedParameter = parameter.nestedIfOptional();
199
		Class<?> clazz = nestedParameter.getNestedParameterType();
200

201
		Constructor<?> ctor = BeanUtils.findPrimaryConstructor(clazz);
202
		if (ctor == null) {
203 204 205 206 207 208 209 210 211 212 213 214
			Constructor<?>[] ctors = clazz.getConstructors();
			if (ctors.length == 1) {
				ctor = ctors[0];
			}
			else {
				try {
					ctor = clazz.getDeclaredConstructor();
				}
				catch (NoSuchMethodException ex) {
					throw new IllegalStateException("No primary or default constructor found for " + clazz, ex);
				}
			}
215
		}
216

217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
		Object attribute = constructAttribute(ctor, attributeName, binderFactory, webRequest);
		if (parameter != nestedParameter) {
			attribute = Optional.of(attribute);
		}
		return attribute;
	}

	/**
	 * Construct a new attribute instance with the given constructor.
	 * <p>Called from
	 * {@link #createAttribute(String, MethodParameter, WebDataBinderFactory, NativeWebRequest)}
	 * after constructor resolution.
	 * @param ctor the constructor to use
	 * @param attributeName the name of the attribute (never {@code null})
	 * @param binderFactory for creating WebDataBinder instance
	 * @param webRequest the current request
	 * @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
	 */
	protected Object constructAttribute(Constructor<?> ctor, String attributeName,
			WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {

241 242 243 244
		if (ctor.getParameterCount() == 0) {
			// A single default constructor -> clearly a standard JavaBeans arrangement.
			return BeanUtils.instantiateClass(ctor);
		}
245

246 247 248 249 250 251 252
		// 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);
253

254 255
		Object[] args = new Object[paramTypes.length];
		WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName);
256 257
		String fieldDefaultPrefix = binder.getFieldDefaultPrefix();
		String fieldMarkerPrefix = binder.getFieldMarkerPrefix();
258
		boolean bindingFailure = false;
259

260
		for (int i = 0; i < paramNames.length; i++) {
261 262 263 264 265 266 267 268 269 270 271 272 273
			String paramName = paramNames[i];
			Class<?> paramType = paramTypes[i];
			Object value = webRequest.getParameterValues(paramName);
			if (value == null) {
				if (fieldDefaultPrefix != null) {
					value = webRequest.getParameter(fieldDefaultPrefix + paramName);
				}
				if (value == null && fieldMarkerPrefix != null) {
					if (webRequest.getParameter(fieldMarkerPrefix + paramName) != null) {
						value = binder.getEmptyValue(paramType);
					}
				}
			}
274
			try {
275 276 277 278 279 280 281
				MethodParameter methodParam = new MethodParameter(ctor, i);
				if (value == null && methodParam.isOptional()) {
					args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
				}
				else {
					args[i] = binder.convertIfNecessary(value, paramType, methodParam);
				}
282 283 284 285 286 287 288 289
			}
			catch (TypeMismatchException ex) {
				bindingFailure = true;
				binder.getBindingResult().addError(new FieldError(
						binder.getObjectName(), paramNames[i], ex.getValue(), true,
						new String[] {ex.getErrorCode()}, null, ex.getLocalizedMessage()));
			}
		}
290

291 292
		if (bindingFailure) {
			throw new BindException(binder.getBindingResult());
293 294
		}
		return BeanUtils.instantiateClass(ctor, args);
295
	}
296

297
	/**
298 299
	 * Extension point to bind the request to the target object.
	 * @param binder the data binder instance to use for the binding
300 301
	 * @param request the current request
	 */
302
	protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
303 304 305 306
		((WebRequestDataBinder) binder).bind(request);
	}

	/**
307
	 * Validate the model attribute if applicable.
308 309 310
	 * <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".
311
	 * @param binder the DataBinder to be used
312
	 * @param parameter the method parameter declaration
313
	 */
314 315
	protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
		Annotation[] annotations = parameter.getParameterAnnotations();
316
		for (Annotation ann : annotations) {
317 318 319 320 321
			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);
322
				break;
323 324 325 326 327
			}
		}
	}

	/**
328
	 * Whether to raise a fatal bind exception on validation errors.
329
	 * <p>The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}.
330
	 * @param binder the data binder used to perform data binding
331
	 * @param parameter the method parameter declaration
332 333
	 * @return {@code true} if the next method parameter is not of type {@link Errors}
	 * @see #isBindExceptionRequired(MethodParameter)
334
	 */
335
	protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
336 337 338 339 340 341 342 343 344 345
		return isBindExceptionRequired(parameter);
	}

	/**
	 * Whether to raise a fatal bind exception on validation errors.
	 * @param parameter the method parameter declaration
	 * @return {@code true} if the next method parameter is not of type {@link Errors}
	 * @since 5.0
	 */
	protected boolean isBindExceptionRequired(MethodParameter parameter) {
346
		int i = parameter.getParameterIndex();
347
		Class<?>[] paramTypes = parameter.getExecutable().getParameterTypes();
348 349 350 351
		boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
		return !hasBindingResult;
	}

352
	/**
353
	 * Return {@code true} if there is a method-level {@code @ModelAttribute}
R
Polish  
Rossen Stoyanchev 已提交
354 355
	 * or, in default resolution mode, for any return value type that is not
	 * a simple type.
356
	 */
357
	@Override
358
	public boolean supportsReturnType(MethodParameter returnType) {
359 360
		return (returnType.hasMethodAnnotation(ModelAttribute.class) ||
				(this.annotationNotRequired && !BeanUtils.isSimpleProperty(returnType.getParameterType())));
361 362
	}

363 364 365
	/**
	 * Add non-null return values to the {@link ModelAndViewContainer}.
	 */
366
	@Override
367
	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
368
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
369

370 371
		if (returnValue != null) {
			String name = ModelFactory.getNameForReturnValue(returnValue, returnType);
372
			mavContainer.addAttribute(name, returnValue);
373
		}
374
	}
375

376
}