SpringMVC 是目前主流的 Web MVC 框架之一。
我们使用浏览器通过地址 http://ip:port/contextPath/path 进行访问,SpringMVC 是如何得知用户到底是访问哪个 Controller 中的方法,这期间到底发生了什么。
本文将分析 SpringMVC 是如何处理请求与 Controller 之间的映射关系的,让读者知道这个过程中到底发生了什么事情。
本文实际上是在上文基础上,深入分析
<pre>HandlerMapping 里的 </pre>
<pre>HandlerExecutionChain getHandler(HttpServletRequest var1) throws Exception;</pre>
<pre> 该方法的具体实现,包括它如何找到对应的方法,以及如何把结果保存在 map 里,以便让请求转发到对应的 handler 上,同时也分析了 handleradaptor 具体做了什么事情。</pre>
源码分析
在分析源码之前,我们先了解一下几个东西。
1. 这个过程中重要的接口和类。
HandlerMethod 类:
Spring3.1 版本之后引入的。是一个封装了方法参数、方法注解,方法返回值等众多元素的类。
它的子类 InvocableHandlerMethod 有两个重要的属性 WebDataBinderFactory 和 HandlerMethodArgumentResolverComposite,很明显是对请求进行处理的。
InvocableHandlerMethod 的子类 ServletInvocableHandlerMethod 有个重要的属性 HandlerMethodReturnValueHandlerComposite,很明显是对响应进行处理的。
ServletInvocableHandlerMethod 这个类在 HandlerAdapter 对每个请求处理过程中,都会实例化一个出来 (上面提到的属性由 HandlerAdapter 进行设置),分别对请求和返回进行处理。(RequestMappingHandlerAdapter 源码,实例化 ServletInvocableHandlerMethod 的时候分别 set 了上面提到的重要属性)
MethodParameter 类:
HandlerMethod 类中的 parameters 属性类型,是一个 MethodParameter 数组。MethodParameter 是一个封装了方法参数具体信息的工具类,包括参数的的索引位置,类型,注解,参数名等信息。
HandlerMethod 在实例化的时候,构造函数中会初始化这个数组,这时只初始化了部分数据,在 HandlerAdapter 对请求处理过程中会完善其他属性,之后交予合适的 HandlerMethodArgumentResolver 接口处理。
以类 DeptController 为例:
@Controller
@RequestMapping(value = "/dept")
public class DeptController {
@Autowired
private IDeptService deptService;
@RequestMapping("/update")
@ResponseBody
public String update(Dept dept) {deptService.saveOrUpdate(dept);
return "success";
}
}
(刚初始化时的数据)
(HandlerAdapter 处理后的数据)
RequestCondition 接口:
Spring3.1 版本之后引入的。是 SpringMVC 的映射基础中的请求条件,可以进行 combine, compareTo,getMatchingCondition 操作。这个接口是映射匹配的关键接口,其中 getMatchingCondition 方法关乎是否能找到合适的映射。
RequestMappingInfo 类:
Spring3.1 版本之后引入的。是一个封装了各种请求映射条件并实现了 RequestCondition 接口的类。
有各种 RequestCondition 实现类属性,patternsCondition,methodsCondition,paramsCondition,headersCondition,consumesCondition 以及 producesCondition,这个请求条件看属性名也了解,分别代表 http 请求的路径模式、方法、参数、头部等信息。
RequestMappingHandlerMapping 类:
处理请求与 HandlerMethod 映射关系的一个类。
2.Web 服务器启动的时候,SpringMVC 到底做了什么。
先看 AbstractHandlerMethodMapping 的 initHandlerMethods 方法中。
我们进入 createRequestMappingInfo 方法看下是如何构造 RequestMappingInfo 对象的。
PatternsRequestCondition 构造函数:
类对应的 RequestMappingInfo 存在的话,跟方法对应的 RequestMappingInfo 进行 combine 操作。
然后使用符合条件的 method 来注册各种 HandlerMethod。
下面我们来看下各种 RequestCondition 接口的实现类的 combine 操作。
PatternsRequestCondition:
RequestMethodsRequestCondition:
方法的请求条件,用个 set 直接 add 即可。
其他相关的 RequestConditon 实现类读者可自行查看源码。
最终,RequestMappingHandlerMapping 中两个比较重要的属性
private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>();
private final MultiValueMap<String, T> urlMap = new LinkedMultiValueMap<String, T>();
T 为 RequestMappingInfo。
构造完成。
我们知道,SpringMVC 的分发器 DispatcherServlet 会根据浏览器的请求地址获得 HandlerExecutionChain。
这个过程我们看是如何实现的。
首先看 HandlerMethod 的获得 (直接看关键代码了):
这里的比较器是使用 RequestMappingInfo 的 compareTo 方法 (RequestCondition 接口定义的)。
然后构造 HandlerExecutionChain 加上拦截器
实例
写了这么多,来点例子让我们验证一下吧。
@Controller
@RequestMapping(value = "/wildcard")
public class TestWildcardController {@RequestMapping("/test/**")
@ResponseBody
public String test1(ModelAndView view) {view.setViewName("/test/test");
view.addObject("attr", "TestWildcardController -> /test/**");
return view;
}
@RequestMapping("/test/*")
@ResponseBody
public String test2(ModelAndView view) {view.setViewName("/test/test");
view.addObject("attr", "TestWildcardController -> /test*");
return view;
}
@RequestMapping("test?")
@ResponseBody
public String test3(ModelAndView view) {view.setViewName("/test/test");
view.addObject("attr", "TestWildcardController -> test?");
return view;
}
@RequestMapping("test/*")
@ResponseBody
public String test4(ModelAndView view) {view.setViewName("/test/test");
view.addObject("attr", "TestWildcardController -> test/*");
return view;
}
}
由于这里的每个 pattern 都带了 * 因此,都不会加入到 urlMap 中,但是 handlerMethods 还是有的。
当我们访问:http://localhost:8888/SpringMVCDemo/wildcard/test1 的时候。
会先根据 “/wildcard/test1” 找 urlMap 对应的 RequestMappingInfo 集合,找不到的话取 handlerMethods 集合中所有的 key 集合 (也就是 RequestMappingInfo 集合)。
然后进行匹配,匹配根据 RequestCondition 的 getMatchingCondition 方法。
最终匹配到 2 个 RequestMappingInfo:
然后会使用比较器进行排序。
之前也分析过,比较器是有优先级的。
我们看到,RequestMappingInfo 除了 pattern,其他属性都是一样的。
我们看下 PatternsRequestCondition 比较的逻辑:
因此,/test* 的通配符比 /test? 的多,因此,最终选择了 /test?
直接比较优先于通配符。
@Controller
@RequestMapping(value = "/priority")
public class TestPriorityController {@RequestMapping(method = RequestMethod.GET)
@ResponseBody
public String test1(ModelAndView view) {view.setViewName("/test/test");
view.addObject("attr", "其他 condition 相同,带有 method 属性的优先级高");
return view;
}
@RequestMapping()
@ResponseBody
public String test2(ModelAndView view) {view.setViewName("/test/test");
view.addObject("attr", "其他 condition 相同,不带 method 属性的优先级高");
return view;
}
}
这里例子,其他 requestCondition 都一样,只有 RequestMethodCondition 不一样。
看出,方法多的优先级越多。
至于其他的 RequestCondition,大家自行查看源码吧。
资源文件映射
以上分析均是基于 Controller 方法的映射 (RequestMappingHandlerMapping)。
SpringMVC 中还有静态文件的映射,SimpleUrlHandlerMapping。
DispatcherServlet 找对应的 HandlerExecutionChain 的时候会遍历属性 handlerMappings,这个一个实现了 HandlerMapping 接口的集合。
由于我们在 *-dispatcher.xml 中加入了以下配置:
<mvc:resources location="/static/" mapping="/static/**"/>
Spring 解析配置文件会使用 ResourcesBeanDefinitionParser 进行解析的时候,会实例化出 SimpleUrlHandlerMapping。
其中注册的 HandlerMethod 为 ResourceHttpRequestHandler。
访问地址:http://localhost:8888/SpringMVCDemo/static/js/jquery-1.11.0.js
地址匹配到 /static/**。
最终 SimpleUrlHandlerMapping 找到对应的 Handler -> ResourceHttpRequestHandler。
ResourceHttpRequestHandler 进行 handleRequest 的时候,直接输出资源文件的文本内容。
总结
大致上整理了一下 SpringMVC 对请求的处理,包括其中比较关键的类和接口,希望对读者有帮助。
让自己对 SpringMVC 有了更深入的认识,也为之后分析数据绑定,拦截器、HandlerAdapter 等打下基础。