Spring MVC part II : @RequestMapping internals

This post follows upon the Spring MVC part I : Request Handling topic.

In this post we discuss in details how Spring handles the @RequestMapping annotation set on handler methods.

I @RequestMapping arguments resolver

The argument resolver is done in the invokeHandlerMethod() method of the class org.springframework.web.bind.annotation.support.HandlerMethodInvoker.

A @SessionAttributes & @ModelAttribute handling

public final Object invokeHandlerMethod(Method handlerMethod, Object handler,
NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception {

	Method handlerMethodToInvoke = BridgeMethodResolver.findBridgedMethod(handlerMethod);
	try {
		boolean debug = logger.isDebugEnabled();
		for (String attrName : this.methodResolver.getActualSessionAttributeNames()) {
			Object attrValue = this.sessionAttributeStore.retrieveAttribute(webRequest, attrName);
			if (attrValue != null) {
				implicitModel.addAttribute(attrName, attrValue);
			}
		}
		for (Method attributeMethod : this.methodResolver.getModelAttributeMethods()) {
			Method attributeMethodToInvoke = BridgeMethodResolver.findBridgedMethod(attributeMethod);
			Object[] args = resolveHandlerArguments(attributeMethodToInvoke, handler, webRequest, implicitModel);
			...	
			String attrName = AnnotationUtils.findAnnotation(attributeMethod, ModelAttribute.class).value();
			if (!"".equals(attrName) && implicitModel.containsAttribute(attrName)) {
				continue;
			}
			ReflectionUtils.makeAccessible(attributeMethodToInvoke);
			Object attrValue = attributeMethodToInvoke.invoke(handler, args);
			if ("".equals(attrName)) {
				Class resolvedType = GenericTypeResolver.resolveReturnType(attributeMethodToInvoke, handler.getClass());
				attrName = Conventions.getVariableNameForReturnType(attributeMethodToInvoke, resolvedType, attrValue);
			}
			if (!implicitModel.containsAttribute(attrName)) {
				implicitModel.addAttribute(attrName, attrValue);
			}
		}
		Object[] args = resolveHandlerArguments(handlerMethodToInvoke, handler, webRequest, implicitModel);
		...	
		ReflectionUtils.makeAccessible(handlerMethodToInvoke);
		return handlerMethodToInvoke.invoke(handler, args);
	}
	...
}

The first for loop at line 7 simply injects into the implicit model HashMap all attributes declared in the @SessionAttributes annotation. These attributes are retrieved from the HTTP session.

The second for loop at line 11 executes all methods in the current request handler that are annotated with @ModelAttribute

...
@ModelAttribute("page")
public String getPage()
{
	return "adminPage";
}
...
@ModelAttribute("user")
public String getUser()
{
	return this.userService.getCurrentLoggedUser();
}
...

These methods are supposed to be called for each request and the returned object is added to the implicit model HashMap (line 28).

Finally, the method arguments are resolved by calling resolveHandlerArguments() at line 31
 

B Argument types resolution

private Object[] resolveHandlerArguments(Method handlerMethod, Object handler,
NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception {

	Class[] paramTypes = handlerMethod.getParameterTypes();
	Object[] args = new Object[paramTypes.length];

	for (int i = 0; i < args.length; i++) {
		MethodParameter methodParam = new MethodParameter(handlerMethod, i);
		methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer);
		GenericTypeResolver.resolveParameterType(methodParam, handler.getClass());
		String paramName = null;
		String headerName = null;
		boolean requestBodyFound = false;
		String cookieName = null;
		String pathVarName = null;
		String attrName = null;
		boolean required = false;
		String defaultValue = null;
		boolean validate = false;
		Object[] validationHints = null;
		int annotationsFound = 0;
		Annotation[] paramAnns = methodParam.getParameterAnnotations();
		for (Annotation paramAnn : paramAnns) {
			if (RequestParam.class.isInstance(paramAnn)) {
				RequestParam requestParam = (RequestParam) paramAnn;
				paramName = requestParam.value();
				required = requestParam.required();
				defaultValue = parseDefaultValueAttribute(requestParam.defaultValue());
				annotationsFound++;
			}
			else if (RequestHeader.class.isInstance(paramAnn)) {
				RequestHeader requestHeader = (RequestHeader) paramAnn;
				headerName = requestHeader.value();
				required = requestHeader.required();
				defaultValue = parseDefaultValueAttribute(requestHeader.defaultValue());
				annotationsFound++;
			}
			else if (RequestBody.class.isInstance(paramAnn)) {
				requestBodyFound = true;
				annotationsFound++;
			}
			else if (CookieValue.class.isInstance(paramAnn)) {
				CookieValue cookieValue = (CookieValue) paramAnn;
				cookieName = cookieValue.value();
				required = cookieValue.required();
				defaultValue = parseDefaultValueAttribute(cookieValue.defaultValue());
				annotationsFound++;
			}
			else if (PathVariable.class.isInstance(paramAnn)) {
				PathVariable pathVar = (PathVariable) paramAnn;
				pathVarName = pathVar.value();
				annotationsFound++;
			}
			else if (ModelAttribute.class.isInstance(paramAnn)) {
				ModelAttribute attr = (ModelAttribute) paramAnn;
				attrName = attr.value();
				annotationsFound++;
			}
			else if (Value.class.isInstance(paramAnn)) {
				defaultValue = ((Value) paramAnn).value();
			}
			else if (paramAnn.annotationType().getSimpleName().startsWith("Valid")) {
				validate = true;
				Object value = AnnotationUtils.getValue(paramAnn);
				validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value});
			}
		}

		if (annotationsFound > 1) {
			throw new IllegalStateException("Handler parameter annotations are exclusive choices - " +"do not specify more than one such annotation on the same parameter: " + handlerMethod);
		}
		if (annotationsFound == 0) {
			Object argValue = resolveCommonArgument(methodParam, webRequest);
			if (argValue != WebArgumentResolver.UNRESOLVED) {
				args[i] = argValue;
			}
			else if (defaultValue != null) {
				args[i] = resolveDefaultValue(defaultValue);
			}
			else {
				Class<?> paramType = methodParam.getParameterType();
				if (Model.class.isAssignableFrom(paramType) || Map.class.isAssignableFrom(paramType)) {
					if (!paramType.isAssignableFrom(implicitModel.getClass())) {
						throw new IllegalStateException("Argument [" + paramType.getSimpleName() + "] is of type " +								"Model or Map but is not assignable from the actual model. You may need to switch " +			"newer MVC infrastructure classes to use this argument.");
					}
					args[i] = implicitModel;
				}
				else if (SessionStatus.class.isAssignableFrom(paramType)) {
					args[i] = this.sessionStatus;
				}
				else if (HttpEntity.class.isAssignableFrom(paramType)) {
					args[i] = resolveHttpEntityRequest(methodParam, webRequest);
				}
				else if (Errors.class.isAssignableFrom(paramType)) {
					throw new IllegalStateException("Errors/BindingResult argument declared " +"without preceding model attribute. Check your handler method signature!");
				}
				else if (BeanUtils.isSimpleProperty(paramType)) {
					paramName = "";
				}
				else {
					attrName = "";
				}
			}
		}
		if (paramName != null) {
			args[i] = resolveRequestParam(paramName, required, defaultValue, methodParam, webRequest, handler);
		}
		else if (headerName != null) {
			args[i] = resolveRequestHeader(headerName, required, defaultValue, methodParam, webRequest, handler);
		}
		else if (requestBodyFound) {
			args[i] = resolveRequestBody(methodParam, webRequest, handler);
		}
		else if (cookieName != null) {
			args[i] = resolveCookieValue(cookieName, required, defaultValue, methodParam, webRequest, handler);
		}
		else if (pathVarName != null) {
			args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler);
		}
		else if (attrName != null) {
			WebDataBinder binder =
					resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler);
			boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1]));
			if (binder.getTarget() != null) {
				doBind(binder, webRequest, validate, validationHints, !assignBindingResult);
			}
			args[i] = binder.getTarget();
			if (assignBindingResult) {
				args[i + 1] = binder.getBindingResult();
				i++;
			}
			implicitModel.putAll(binder.getBindingResult().getModel());
		}
	}
		return args;
}

The for loop at line 23 lists all argument types having supported annotations:

  • line 24: @RequestParam annotated argument
  • line 31: @RequestHeader annotated argument
  • line 38: @RequestBody annotated argument
  • line 42: @CookieValue annotated argument
  • line 49: @PathVariable annotated argument
  • line 54: @ModelAttribute annotated argument
  • line 62: @Valid annotated argument

If an argument is annotated more than once, Spring will raise an exception as per line 70

Next, the method resolveCommonArgument() is called if the argument has no annotation (line 73).

If the argument is of type

  • Model or Map (java.util.Map), the implicit model HashMap built earlier will be returned (line 86)
  • SessionStatus, the current session status object is returned (line 89)
  • HttpEntity, the method resolveHttpEntityRequest() is called for resolution (line 92)

At lines 106, 109, 112, 115, 118 & 122, Spring resolves the arguments based on their annotation. Custom data binders, if any, are invoked at line 125.

Let’s focus on the resolveModelAttribute() method at line 122. This method resolves all argument annotated by @ModelAttribute

private WebDataBinder resolveModelAttribute(String attrName, MethodParameter methodParam,
ExtendedModelMap implicitModel, NativeWebRequest webRequest, Object handler) throws Exception {

	// Bind request parameter onto object...
	String name = attrName;
	if ("".equals(name)) {
		name = Conventions.getVariableNameForParameter(methodParam);
	}
	Class<?> paramType = methodParam.getParameterType();
	Object bindObject;
	if (implicitModel.containsKey(name)) {
		bindObject = implicitModel.get(name);
	}
	else if (this.methodResolver.isSessionAttribute(name, paramType)) {
		bindObject = this.sessionAttributeStore.retrieveAttribute(webRequest, name);
		if (bindObject == null) {
			raiseSessionRequiredException("Session attribute '" + name + "' required - not bound in session");
		}
	}
	else {
		bindObject = BeanUtils.instantiateClass(paramType);
	}
	WebDataBinder binder = createBinder(webRequest, bindObject, name);
	initBinder(handler, name, binder, webRequest);
	return binder;
}

As expected, if the arugment name is found in the implicit model HashMap, the value is returned (line 12) otherwise the value is search in the HTTP session itself (line 15).

Please notice at lines 23 & 24 the initialization of a WebDataBinder to bind request parameter to a particular type.

Now let’s see how Spring resolves common arguments:

protected Object resolveCommonArgument(MethodParameter methodParameter, NativeWebRequest webRequest)
throws Exception {

	// Invoke custom argument resolvers if present...
	if (this.customArgumentResolvers != null) {
		for (WebArgumentResolver argumentResolver : this.customArgumentResolvers) {
			Object value = argumentResolver.resolveArgument(methodParameter, webRequest);
			if (value != WebArgumentResolver.UNRESOLVED) {
				return value;
			}
		}
	}
	// Resolution of standard parameter types...
	Class paramType = methodParameter.getParameterType();
	Object value = resolveStandardArgument(paramType, webRequest);
	if (value != WebArgumentResolver.UNRESOLVED && !ClassUtils.isAssignableValue(paramType, value)) {
		throw new IllegalStateException("Standard argument type [" + paramType.getName() +
				"] resolved to incompatible value of type [" + (value != null ? value.getClass() : null) +
				"]. Consider declaring the argument type in a less specific fashion.");
	}
	return value;
}

If you have declared custom argument resolvers, they are invoked here (line 5) otherwise Spring tries to resolve the argument by calling resolveStandardArgument() (line 15) which matches only all argument of type WebRequest or NativeWebRequest.

 

II Model and View resolution

Remember the invokedHandlerMethod() discussed in the previous topic ?

protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, Object handler)	throws Exception {

	ServletHandlerMethodResolver methodResolver = getMethodResolver(handler);
	Method handlerMethod = methodResolver.resolveHandlerMethod(request);
	ServletHandlerMethodInvoker methodInvoker = new ServletHandlerMethodInvoker(methodResolver);
	ServletWebRequest webRequest = new ServletWebRequest(request, response);
	ExtendedModelMap implicitModel = new BindingAwareModelMap();

	Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel);
	ModelAndView mav =
methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest);
	methodInvoker.updateModelAttributes(handler, (mav != null ? mav.getModel() : null), implicitModel, webRequest);
	return mav;
}
 

In this chapter we zoom in the getModelAndView() internals

public ModelAndView getModelAndView(Method handlerMethod, Class handlerType, Object returnValue,
ExtendedModelMap implicitModel, ServletWebRequest webRequest) throws Exception {

	ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class);
	if (responseStatusAnn != null) {
			HttpStatus responseStatus = responseStatusAnn.value();
			String reason = responseStatusAnn.reason();
			if (!StringUtils.hasText(reason)) {
				webRequest.getResponse().setStatus(responseStatus.value());
			}
			else {
				webRequest.getResponse().sendError(responseStatus.value(), reason);
			}
			// to be picked up by the RedirectView
			webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, responseStatus);

			responseArgumentUsed = true;
		}

		// Invoke custom resolvers if present...
		if (customModelAndViewResolvers != null) {
			for (ModelAndViewResolver mavResolver : customModelAndViewResolvers) {
				ModelAndView mav = mavResolver.resolveModelAndView(
						handlerMethod, handlerType, returnValue, implicitModel, webRequest);
				if (mav != ModelAndViewResolver.UNRESOLVED) {
					return mav;
				}
			}
		}

		if (returnValue instanceof HttpEntity) {
			handleHttpEntityResponse((HttpEntity<?>) returnValue, webRequest);
			return null;
		}
		else if (AnnotationUtils.findAnnotation(handlerMethod, ResponseBody.class) != null) {
			handleResponseBody(returnValue, webRequest);
			return null;
		}
		else if (returnValue instanceof ModelAndView) {
			ModelAndView mav = (ModelAndView) returnValue;
			mav.getModelMap().mergeAttributes(implicitModel);
			return mav;
		}
		else if (returnValue instanceof Model) {
			return new ModelAndView().addAllObjects(implicitModel).addAllObjects(((Model) returnValue).asMap());
		}
		else if (returnValue instanceof View) {
			return new ModelAndView((View) returnValue).addAllObjects(implicitModel);
		}
		else if (AnnotationUtils.findAnnotation(handlerMethod, ModelAttribute.class) != null) {
			addReturnValueAsModelAttribute(handlerMethod, handlerType, returnValue, implicitModel);
			return new ModelAndView().addAllObjects(implicitModel);
		}
		else if (returnValue instanceof Map) {
			return new ModelAndView().addAllObjects(implicitModel).addAllObjects((Map) returnValue);
		}
		else if (returnValue instanceof String) {
			return new ModelAndView((String) returnValue).addAllObjects(implicitModel);
		}
		else if (returnValue == null) {
			// Either returned null or was 'void' return.
			if (this.responseArgumentUsed || webRequest.isNotModified()) {
				return null;
			}
			else {
				// Assuming view name translation...
				return new ModelAndView().addAllObjects(implicitModel);
			}
		}
		else if (!BeanUtils.isSimpleProperty(returnValue.getClass())) {
			// Assume a single model attribute...
			addReturnValueAsModelAttribute(handlerMethod, handlerType, returnValue, implicitModel);
			return new ModelAndView().addAllObjects(implicitModel);
		}
		else {
			throw new IllegalArgumentException("Invalid handler method return value: " + returnValue);
		}
	}

The if block at line 5 injects a ResponseStatus object into the request response.
Then, if custom view and model resolvers have been declared, they are invoked (line 21)
The case of HttpEntity and @ResponseBody returned value type are processed at lines 32 & 36

From line 39 to line 69, all returned types from Model, View, ModelAndView, String, Map or @ModelAttribute are handled. In most cases a ModelAndView object is created and the implicit model HashMap is added to this object as well as the view name, if present.

If the returned value is of primitive or any type not listed above, a simple ModelAndView object is created, fileld with the implicit model and returned (line 72).
 

III Model attributes update

Last but not least, let’s see how Spring updates the model attributes before calling view renderers

public final void updateModelAttributes(Object handler, Map<String, Object> mavModel,
ExtendedModelMap implicitModel, NativeWebRequest webRequest) throws Exception {

	if (this.methodResolver.hasSessionAttributes() && this.sessionStatus.isComplete()) {
		for (String attrName : this.methodResolver.getActualSessionAttributeNames()) {
			this.sessionAttributeStore.cleanupAttribute(webRequest, attrName);
		}
	}
	// Expose model attributes as session attributes, if required.
	// Expose BindingResults for all attributes, making custom editors available.
	Map<String, Object> model = (mavModel != null ? mavModel : implicitModel);
	if (model != null) {
		try {
			String[] originalAttrNames = model.keySet().toArray(new String[model.size()]);
			for (String attrName : originalAttrNames) {
				Object attrValue = model.get(attrName);
				boolean isSessionAttr = this.methodResolver.isSessionAttribute(
						attrName, (attrValue != null ? attrValue.getClass() : null));
				if (isSessionAttr) {
					if (this.sessionStatus.isComplete()) {
						implicitModel.put(MODEL_KEY_PREFIX_STALE + attrName, Boolean.TRUE);
					}
					else if (!implicitModel.containsKey(MODEL_KEY_PREFIX_STALE + attrName)) {
						this.sessionAttributeStore.storeAttribute(webRequest, attrName, attrValue);
					}
				}
				if (!attrName.startsWith(BindingResult.MODEL_KEY_PREFIX) &&
						(isSessionAttr || isBindingCandidate(attrValue))) {
					String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + attrName;
					if (mavModel != null && !model.containsKey(bindingResultKey)) {
						WebDataBinder binder = createBinder(webRequest, attrValue, attrName);
						initBinder(handler, attrName, binder, webRequest);
						mavModel.put(bindingResultKey, binder.getBindingResult());
					}
				}
			}
		}
		catch (InvocationTargetException ex) {
			// User-defined @InitBinder method threw an exception...
			ReflectionUtils.rethrowException(ex.getTargetException());
		}
	}
}

At line 6, we can see that if the session status is set to complete, Spring will remove all attributes declared in @SessionAttributes from the HTTP session.

Next, the normal processing flow is invoked. Please notice the line 24 where all session attributes are registered into the HTTP session.

For each model attribute candidate to a custom data binder, Spring will create one and call the binding method before putting the result into the ModelAndView map (lines 31 to 33).

Advertisements

About DuyHai DOAN
Cassandra Technical Evangelist. LinkedIn profile : http://fr.linkedin.com/pub/duyhai-doan/2/224/848. Follow me on Twitter: @doanduyhai for latest updates on Cassandra

9 Responses to Spring MVC part II : @RequestMapping internals

  1. wood says:

    great article!

    Can’t wait for your next post.

    • DuyHai DOAN says:

      Thak you wood.

      At the moment I’m quite late in my post frequency (one post a week) cause I’m involved in a HTML 5 website competition. After April 10th I’ll be back for new posts.

      As a teaser:

      1) ThymeLeaf HTML5 view templating solution for Spring MVC
      2) New Spring MVC 3 full Java config
      3) Spring MVC 3 Exception interceptors & Flash scoped for attributes

  2. Pingback: Spring MVC part III: ThymeLeaf integration « Yet Another Java Blog

  3. gihrig says:

    For me this stuff (Spring MVC part I: Request Handling and Spring MVC part II : @RequestMapping internals) was pretty abstract. I don’t see what it relates to. I guess you are walking the reader through Spring MVC configuration and it’s underlying source?

    It would have been helpful if you had a simple demo app to work with, as you introduced in part III. In these first two parts, the configuration you describe does not match the example introduced in Part III.

    For the new-commer, one difficult hurdle to overcome is that Spring has so many ways to accomplish the same thing, that it’s really hard to know what to do.

    I have successfully created Spring MVC applications with the help Spring Roo, so I have some idea how this stuff works, but I’m a long way from being able to write it myself from scratch.

    While I actually agree with your position in “magics-is-evil” https://doanduyhai.wordpress.com/2012/04/21/magics-is-evil/, spring make this very difficult because one can not tell where to begin or what goes with what.

    Anyway, thanks for trying.

    • DuyHai DOAN says:

      Hello gihrig

      You’re right about the abstract nature of this article. Indeed it was not meant to be a tutorial, it is more a technical insight into the source code to understand how the “magic” is done. I wrote this article more for myself to keep the knowledge somewhere. It’s similar to my other articles on @Transactional and @PersistenceContext annotations, more technical than practical. That explains why there is no sample application.

      For the configuration mismatch between part I, II & III you’re right. Indeed I started the code analysis with Spring 3.0 and then comes Spring 3.1. They refactored the controller class hierarchy, below is the bijective mapping between old and new classes:

      DefaultAnnotationHandlerMapping -> RequestMappingHandlerMapping
      AnnotationMethodHandlerAdapter -> RequestMappingHandlerAdapter
      AnnotationMethodHandlerExceptionResolver -> ExceptionHandlerExceptionResolver

      Though the internal logic for dynamic method arguments matching is quite the same so I did not re-write the whole articles.

      About Spring Roo and all extensions of its kind (Spring Integration not to name), the problem is the abuse of convention-over-configuration. It meets the demand for 80% of the time. For the remaining 20% you need to dig into the source code to have total control.

      • gihrig says:

        The comparison between the 2.5/3.0 and 3.1 class names helped to clarify some of my confusion. But that brings up a new problem. With Spring framework having so many ways to accomplish the same thing, how do you keep up with the changes? How do you decide on the best strategy?

        Current documentation http://static.springsource.org/spring/docs/current/javadoc-api/allclasses-noframe.html documents all the classes you listed above. The older ones are marked as ‘since 2.5’ or ‘since 3.0’ whereas the newer ones are marked as ‘since 3.1’, but I did not see any way to filter the list to show only the latest classes, many of which are, no doubt, not updated in recent versions, but still the latest version in existence.

        So with over 800 pages of ‘official’ documentation and over 2500 classes – how do you keep up? It’s not like my whole life revolves around Spring framework.

        Thanks again for your insight!

  4. DuyHai DOAN says:

    @gihrig

    You can find the documentation for each Spring version here: http://static.springsource.org/spring/docs/

    Indeed, the “old” controller classes are still there in version 3.1 for backward compatibility but they are no longer maintained (no evolution).

    And you pointed out the real issue too, with Spring there are many ways to achieve the same result. A vivid example is configuration (by XML, by MVC namespace or by full Java). But it’s not surprising because the framework evolves to remain up-to-date with latest technology.

  5. Pingback: An Autowired handler for Spring MVC | Assert.This

  6. Pingback: Confluence: 陈玮暐

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: