mvc:annotation-driven会将每一个Controllor内的被@RequestMapping注解标注的方法解析为HandlerMethod对象,并存储在RequestMappingHandlerMapping的MappingRegistry属性中,寻址就是根据request请求信息,找到正确的HandlerMethod对象的过程。
HandlerMethod和RequestMappingInfo的关系
HandlerMethod意为方法相关的信息封装,RequestMappingInfo则是方法所要求的条件信息,HandlerMethod和RequestMappingInfo的映射关系,通过MappingRegistry的Map<T, HandlerMethod>已经做好了映射,T即为RequestMappingInfo。
HandlerMethod
public class HandlerMethod {
// method所在的Controllor实例
private final Object bean;
private final BeanFactory beanFactory;
// Controllor的class类型
private final Class<?> beanType;
// method本身
private final Method method;
private final Method bridgedMethod;
// method的参数信息
private final MethodParameter[] parameters;
private HttpStatus responseStatus;
private String responseStatusReason;
private HandlerMethod resolvedFromHandlerMethod;
//...
}
RequestCondition
public interface RequestCondition<T> {
// 类上的条件和方法上的条件进行合并(并集)
T combine(T other);
// 返回该请求所匹配的条件
T getMatchingCondition(HttpServletRequest request);
// 如果条件要求是一个人,但来了一个胖子和瘦子,该方法决定选胖子,还是瘦子
int compareTo(T other, HttpServletRequest request);
}
combine()举例:Class条件是GET,Method条件是POST,combine()的结果是GET或者POST。
getMatchingCondition()举例:条件是GET或者POST,当前request是GET请求,则返回GET条件。
compareTo()举例:条件有/emp*和/emp?,现在的request请求是/emp1,/emp1同时符合/emp*和/emp?,选谁呢?最终选择/emp?,因为/emp?粒度更小,范围更小,compareTo()具有决定权。
RequestCondition的各种实现类类图:
(Made In IntelliJ IDEA)
(Made In Edraw Max)
再对照这幅图,读者大概就知道每个条件对应哪一个配置了。
RequestMappingInfo
RequestMappingInfo较为特殊,它是一个典型的装饰器设计模式(Decorator),它装饰了具体的条件。
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
private final PatternsRequestCondition patternsCondition;
private final RequestMethodsRequestCondition methodsCondition;
private final ParamsRequestCondition paramsCondition;
private final HeadersRequestCondition headersCondition;
private final ConsumesRequestCondition consumesCondition;
private final ProducesRequestCondition producesCondition;
private final RequestConditionHolder customConditionHolder;
//...
}
我们看看它的combine()方法,都是调用了具体的条件的combine()方法,将结果返回。
public RequestMappingInfo combine(RequestMappingInfo other) {
String name = combineNames(other);
PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition);
RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition);
ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition);
HeadersRequestCondition headers = this.headersCondition.combine(other.headersCondition);
ConsumesRequestCondition consumes = this.consumesCondition.combine(other.consumesCondition);
ProducesRequestCondition produces = this.producesCondition.combine(other.producesCondition);
RequestConditionHolder custom = this.customConditionHolder.combine(other.customConditionHolder);
return new RequestMappingInfo(name, patterns,
methods, params, headers, consumes, produces, custom.getCondition());
}
PatternsRequestCondition.combine()
我们以PatternsRequestCondition.combine()为例,它调用了AntPathMatcher.combine()方法:
public String combine(String pattern1, String pattern2) {
if (!StringUtils.hasText(pattern1) && !StringUtils.hasText(pattern2)) {
return "";
}
if (!StringUtils.hasText(pattern1)) {
return pattern2;
}
if (!StringUtils.hasText(pattern2)) {
return pattern1;
}
boolean pattern1ContainsUriVar = (pattern1.indexOf('{') != -1);
if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) {
// /* + /hotel -> /hotel ; "/*.*" + "/*.html" -> /*.html
// However /user + /user -> /usr/user ; /{foo} + /bar -> /{foo}/bar
return pattern2;
}
// /hotels/* + /booking -> /hotels/booking
// /hotels/* + booking -> /hotels/booking
if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) {
return concat(pattern1.substring(0, pattern1.length() - 2), pattern2);
}
// /hotels/** + /booking -> /hotels/**/booking
// /hotels/** + booking -> /hotels/**/booking
if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) {
return concat(pattern1, pattern2);
}
int starDotPos1 = pattern1.indexOf("*.");
if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) {
// simply concatenate the two patterns
return concat(pattern1, pattern2);
}
String ext1 = pattern1.substring(starDotPos1 + 1);
int dotPos2 = pattern2.indexOf('.');
String file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2));
String ext2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2));
boolean ext1All = (ext1.equals(".*") || ext1.equals(""));
boolean ext2All = (ext2.equals(".*") || ext2.equals(""));
if (!ext1All && !ext2All) {
throw new IllegalArgumentException("Cannot combine patterns: " + pattern1 + " vs " + pattern2);
}
String ext = (ext1All ? ext2 : ext1);
return file2 + ext;
}
作者的注释,已经明确了路径映射合并的规则。
不过,在路径合并之前,它做了一项操作,将不以/开头的路径,加上/,譬如“index”,会被处理为"/index"。
PatternsRequestCondition.prependLeadingSlash():
private static Set<String> prependLeadingSlash(Collection<String> patterns) {
if (patterns == null) {
return Collections.emptySet();
}
Set<String> result = new LinkedHashSet<String>(patterns.size());
for (String pattern : patterns) {
if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) {
pattern = "/" + pattern;
}
result.add(pattern);
}
return result;
}
RequestMethodsRequestCondition.combine()
RequestMethodsRequestCondition.combine()直接采用了取并集的操作,这也是类条件为GET,method条件为POST,最终是GET或者POST关系的原因。
public RequestMethodsRequestCondition combine(RequestMethodsRequestCondition other) {
Set<RequestMethod> set = new LinkedHashSet<RequestMethod>(this.methods);
set.addAll(other.methods);
return new RequestMethodsRequestCondition(set);
}
其余的条件合并,大都于此类似,读者自行查看。
RequestCondition条件选择优先级
我们还是得从装饰器角色RequestMappingInfo入手,其条件优先级基本是:pattern > param > header > consume > produce > method > custom。
public int compareTo(RequestMappingInfo other, HttpServletRequest request) {
int result;
// HEAD请求时,method条件优先
if (HttpMethod.HEAD.matches(request.getMethod())) {
result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
if (result != 0) {
return result;
}
}
result = this.patternsCondition.compareTo(other.getPatternsCondition(), request);
if (result != 0) {
return result;
}
result = this.paramsCondition.compareTo(other.getParamsCondition(), request);
if (result != 0) {
return result;
}
result = this.headersCondition.compareTo(other.getHeadersCondition(), request);
if (result != 0) {
return result;
}
result = this.consumesCondition.compareTo(other.getConsumesCondition(), request);
if (result != 0) {
return result;
}
result = this.producesCondition.compareTo(other.getProducesCondition(), request);
if (result != 0) {
return result;
}
result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
if (result != 0) {
return result;
}
result = this.customConditionHolder.compareTo(other.customConditionHolder, request);
if (result != 0) {
return result;
}
return 0;
}
如何理解pattern > param > header > consume > produce > method > custom的优先级顺序呢?
原理是:
1、当多个HandlerMethod均满足条件,此时使用pattern排序规则,取排序后的第一个HandlerMethod。如果pattern排序规则返回0(未获得实质排序),则再选择param排序规则,以此类推。最后,选择第一个HandlerMethod作为目标方法。
举例:请求URL为/emp1,发现HandlerMethod(/emp*)和HandlerMethod(/emp?)均满足要求,此时使用pattern条件的排序规则,排序后变成[/emp?, /emp*],然后取第一个HandlerMethod,那么HandlerMethod(/emp?)就被选中了。
再举例:当URL相同时,URL参数?name=张三&pwd=123,此时HandlerMethod({name=张三})和HandlerMethod({name=张三, pwd=123})均满足条件,根据param排序规则,参数多的优先级高,于是HandlerMethod({name=张三, pwd=123})会被最终选中。
@RequestMapping(path = {"/index"}, params = {"pwd=123"})
public String index(Model model, HttpServletRequest request) {
model.addAttribute("msg", "Hello controllor.");
return "hello";
}
@RequestMapping(path = {"/index"}, params = {"name=张三", "pwd=123"})
public String index2(Model model, HttpServletRequest request) {
model.addAttribute("msg", "Hello controllor.");
return "hello";
}
AntPatternComparator.compare()
PatternsRequestCondition通过AntPatternComparator.compare()方法,来指定排序规则:
public int compare(String pattern1, String pattern2) {
PatternInfo info1 = new PatternInfo(pattern1);
PatternInfo info2 = new PatternInfo(pattern2);
// path为空和path=/**等价
if (info1.isLeastSpecific() && info2.isLeastSpecific()) {
return 0;
}
else if (info1.isLeastSpecific()) {
return 1;
}
else if (info2.isLeastSpecific()) {
return -1;
}
// path直接匹配pattern,譬如path=/emp/emp*, pattern=/emp/emp*
boolean pattern1EqualsPath = pattern1.equals(path);
boolean pattern2EqualsPath = pattern2.equals(path);
if (pattern1EqualsPath && pattern2EqualsPath) {
return 0;
}
else if (pattern1EqualsPath) {
return -1;
}
else if (pattern2EqualsPath) {
return 1;
}
// path=/emp/emp/**和path=/emp/emp/abc
if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) {
return 1;
}
else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) {
return -1;
}
// 比较Uri变量数量+*数量+**的数量
if (info1.getTotalCount() != info2.getTotalCount()) {
return info1.getTotalCount() - info2.getTotalCount();
}
if (info1.getLength() != info2.getLength()) {
return info2.getLength() - info1.getLength();
}
// 比较*的数量
if (info1.getSingleWildcards() < info2.getSingleWildcards()) {
return -1;
}
else if (info2.getSingleWildcards() < info1.getSingleWildcards()) {
return 1;
}
if (info1.getUriVars() < info2.getUriVars()) {
return -1;
}
else if (info2.getUriVars() < info1.getUriVars()) {
return 1;
}
return 0;
}
查找HandlerMethod
AbstractHandlerMethodMapping#lookupHandlerMethod()
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<Match>();
// 解析时不带通配符(*|?)条件直接放入urlLookup中
// 此处直接使用urlLookup查找
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// 遍历系统中所有的RequestMappingInfo,找到满足条件的HandlerMethod集合
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
// 使用RequestMappingInfo的compareTo方法进行排序
Collections.sort(matches, comparator);
//...
// 取第一个HandlerMethod
Match bestMatch = matches.get(0);
//...
// 返回HandlerMethod
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
request请求寻址HandlerMethod实战
@Controller
@RequestMapping("/emp")
public class EmpControllor {
@RequestMapping(value = "/emp1")
public ModelAndView index1(ModelAndView mav) {
mav.setViewName("employee");
mav.addObject("path", "/emp/emp1");
return mav;
}
@RequestMapping(value = "/emp?")
public ModelAndView index2(ModelAndView mav) {
mav.setViewName("employee");
mav.addObject("path", "/emp/emp?");
return mav;
}
@RequestMapping("/emp*")
public ModelAndView list(ModelAndView mav) {
mav.setViewName("employee");
mav.addObject("path", "/emp/emp*");
return mav;
}
@RequestMapping("/emp/*")
public ModelAndView add(ModelAndView mav) {
mav.setViewName("employee");
mav.addObject("path", "/emp/emp/*");
return mav;
}
@RequestMapping("/emp/**")
public ModelAndView update(ModelAndView mav) {
mav.setViewName("employee");
mav.addObject("path", "/emp/emp/**");
return mav;
}
}
创建一个WEB-INF/jsp/employee.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page isELIgnored="false"%>
<html>
<head>
<title>Title</title>
</head>
<body>
path = <b>${path}</b>
</body>
</html>
1、http://localhost:8080/emp/emp1
输出:path = /emp/emp1
原理:由于不带*|?通配符,urlLookup直接寻址/emp1
(Made In Postman)
2、http://localhost:8080/emp/emp2
输出:path = /emp/emp?
原理:/emp2同时匹配/emp*和/emp?,但是/emp*的*号数量大于/emp?,最终选择了/emp?
(Made In Postman)
3、http://localhost:8080/emp/emp*
输出:path = /emp/emp*
原理:pattern1.equals(path)直接比较匹配的结果
(Made In Postman)
4、http://localhost:8080/emp/emp/abc
输出:path = /emp/emp/*
原理:/emp/**的通配符多于/emp*,最终选择了/emp/*
(Made In Postman)
5、方法多的会优先么?
@RequestMapping(value = "/emp/**", method = {RequestMethod.GET})
public ModelAndView update1(ModelAndView mav) {
mav.setViewName("employee");
mav.addObject("path", "/emp/emp/**[GET]");
return mav;
}
@RequestMapping(value = "/emp/**", method = {RequestMethod.GET, RequestMethod.POST})
public ModelAndView update2(ModelAndView mav) {
mav.setViewName("employee");
mav.addObject("path", "/emp/emp/**[GET, POST]");
return mav;
}
http://localhost:8080/emp/emp/abc/123
请求:GET
输出:java.lang.IllegalStateException: Ambiguous handler methods mapped for HTTP path 'xxx'
原理:当条件是GET或者POST,当前request是GET请求,getMatchingCondition()方法将返回GET条件,即当前reqeust请求的GET条件,于是系统中找到了2个完全一样的GET方法,报错退出。
我们将GET修改为POST请求后,输出:path = /emp/emp/**[GET, POST]
原理:条件是GET或者POST,当前request是POST请求,getMatchingCondition()方法将返回POST条件,唯一找到上面的update2()方法,执行成功。
(Made In Postman)
所以,一定要注意,匹配时使用getMatchingCondition(request)返回的当前reqeust的条件进行匹配,而不是我们配置的所有条件。
原文出处:http://my.oschina.net/zudajun