Recently I changed my view technology from JSF to Spring MVC. I used the latest release (3.1.RELEASE) of the framework and there are some significant changes compared to the older 2.5.x versions
The most important changes I believe were the introduction of the <mvc> namespace (supposedly to simplify developers’ life) and the intensive use of annotations for request mappings.
I Configuration
Below is a simple XML configuration file my-spring-mvc.xml for the Spring MVC servlet:
<tx:annotation-driven /> <context:component-scan base-package="com.doan.onlinelibrary" /> <mvc:resources location="/resources/img/" mapping="/resources/img/**" /> <mvc:resources location="/resources/css/" mapping="/resources/css/**" /> <mvc:resources location="/resources/js/" mapping="/resources/js/**" /> <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/> <bean id="customJacksonViewAwareMessageConverter" class="com.doan.onlinelibrary.json.converter.JacksonViewAwareHttpMessageConverter"/> <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" > <property name="messageConverters"> <list> <ref bean="customJacksonViewAwareMessageConverter"/> <bean class = "org.springframework.http.converter.StringHttpMessageConverter"> <property name="supportedMediaTypes" value = "text/plain;charset=UTF-8" /> </bean> </list> </property> </bean> ... ... <!-- Config for messages, templates & views -->
I skipped on purpose the configuration for message bundles, template & view resolvers because they are out of the scope of this post.
Please notice at lines 9 & 13 the declaration of the DefaultAnnotationHandlerMapping and AnnotationMethodHandlerAdapter beans. Both of them are playing key roles for request mapping in Spring MVC.
This configuration file for Spring MVC should be declared in the web.xml as a servlet:
<servlet> <servlet-name>my-spring-mvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/classes/my-spring-mvc.xml </param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>my-spring-mvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
II Request handlers listing
The DefaultAnnotationHandlerMapping extends the AbstractDetectingUrlHandlerMapping class which has a method of interest: detectHandlers()
protected void detectHandlers() throws BeansException { ... String[] beanNames = (this.detectHandlersInAncestorContexts ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) : getApplicationContext().getBeanNamesForType(Object.class)); // Take any bean name that we can determine URLs for. for (String beanName : beanNames) { String[] urls = determineUrlsForHandler(beanName); if (!ObjectUtils.isEmpty(urls)) { // URL paths found: Let's consider it a handler. registerHandler(urls, beanName); } ... ... }
At lines 3 & 4, it simply list all beans within the current application context (the one for the Spring MVC servlet). Optionally it will also detect all beans in parent application context if the flag detectHandlersInAncestorContexts was set to true.
At line 8, the determineUrlsForHandler() method is called. All the job is done there.
protected String[] determineUrlsForHandler(String beanName) { ApplicationContext context = getApplicationContext(); Class<?> handlerType = context.getType(beanName); RequestMapping mapping = context.findAnnotationOnBean(beanName, RequestMapping.class); if (mapping != null) { // @RequestMapping found at type level this.cachedMappings.put(handlerType, mapping); Set<String> urls = new LinkedHashSet<String>(); String[] typeLevelPatterns = mapping.value(); if (typeLevelPatterns.length > 0) { // @RequestMapping specifies paths at type level String[] methodLevelPatterns = determineUrlsForHandlerMethods(handlerType, true); for (String typeLevelPattern : typeLevelPatterns) { if (!typeLevelPattern.startsWith("/")) { typeLevelPattern = "/" + typeLevelPattern; } boolean hasEmptyMethodLevelMappings = false; for (String methodLevelPattern : methodLevelPatterns) { if (methodLevelPattern == null) { hasEmptyMethodLevelMappings = true; } else { String combinedPattern = getPathMatcher().combine(typeLevelPattern, methodLevelPattern); addUrlsForPath(urls, combinedPattern); } } if (hasEmptyMethodLevelMappings || org.springframework.web.servlet.mvc.Controller.class.isAssignableFrom(handlerType)) { addUrlsForPath(urls, typeLevelPattern); } } return StringUtils.toStringArray(urls); } else { // actual paths specified by @RequestMapping at method level return determineUrlsForHandlerMethods(handlerType, false); } } else if (AnnotationUtils.findAnnotation(handlerType, Controller.class) != null) { // @RequestMapping to be introspected at method level return determineUrlsForHandlerMethods(handlerType, false); } else { return null; } }
Line 3, we check whether the current class has the @RequestMapping annotation at class level
If so, the class is put into a mapping cache at line 7
Line 9, we extract all the URL paths defined in the @RequestMapping annotation (for example /pages/Admin)
At line 12, we list all URL mappings at method level. If we had a mapping like:
@Controller @RequestMapping("/pages/Books") public class BooksController { @RequestMapping(value = "search", method = RequestMethod.GET) public String setupForm(Model model) {...} @RequestMapping(value = "search", method = RequestMethod.POST) public String doSearch(Model model) {...} @RequestMapping(value = "clearSearch", method = RequestMethod.GET) public String clear(Model model) {...} }
it will result in methodLevelPatterns = {“search”,”clearSearch”}
At line 23, all method level URLs are combined with class level URL pattern so we get combinedPattern = {“/pages/Books/search”,”/pages/Books/clearSearch”}
Then, at line 24, these URLs are added in an URL set urls
Lines 36 & 41 correspond to the case where URLs are defined directly at method level and not class level.
III Requests handling
Now that the URL mapping is established, let’s focus on the AnnotationMethodHandlerAdapter.
The main entry for request handling starts with the method handle(HttpServletRequest request, HttpServletResponse response, Object handler)
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Class<?> clazz = ClassUtils.getUserClass(handler); Boolean annotatedWithSessionAttributes = this.sessionAnnotatedClassesCache.get(clazz); if (annotatedWithSessionAttributes == null) { annotatedWithSessionAttributes = (AnnotationUtils.findAnnotation(clazz, SessionAttributes.class) != null); this.sessionAnnotatedClassesCache.put(clazz, annotatedWithSessionAttributes); } if (annotatedWithSessionAttributes) { // Always prevent caching in case of session attribute management. checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true); // Prepare cached set of session attributes names. } else { // Uses configured default cacheSeconds setting. checkAndPrepare(request, response, true); } // Execute invokeHandlerMethod in synchronized block if required. if (this.synchronizeOnSession) { HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { return invokeHandlerMethod(request, response, handler); } } } return invokeHandlerMethod(request, response, handler); }
At line 8, the handler class is put in a cache if it is annotated with @SessionAttribute.
At lines 12 & 17, the request is checked against supported HTTP methods (GET, POST, PUT, …). If the request HTTP method is not supported, a HttpRequestMethodNotSupportedException will be raised.
At line 21, if the “synchronizeOnSession” property was set to true for the handler, the processing will be synchonized against a session mutex
In all cases, the real processing is delegated to the method invokeHandlerMethod()
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; }
At line 7, a BindingAwareModelMap is created. This is simply a plain HashMap object, acting as an implicit Model object that will be injected to the request handler.
At this point, it is usefull to remind our readers that Spring MVC offers a quite large list of method argument types for all request handler methods annotated with @RequestMapping: supported argument types. The argument matching and mapping is performed by the method invokeHandlerMethod() at line 9
At line 10, the Model and View lookup is done by getModelAndView() and finally Spring binds the model values to the view in the updateModelAttributes() method (line 11).
To be continued …
Pingback: Spring MVC part II : @RequestMapping internals « Yet Another Java Blog