Spring MVC学习笔记之Controller查找(基于Spring4.0.3)

时间:2021-12-25 16:26:41

0 摘要

本文从源码层面简单讲解springmvc的处理器映射环节,也就是查找controller详细过程

1 springmvc请求流程

Spring MVC学习笔记之Controller查找(基于Spring4.0.3)

controller查找在上图中对应的步骤1至2的过程

Spring MVC学习笔记之Controller查找(基于Spring4.0.3)
springmvc详细运行流程图

2 springmvc初始化过程

2.1 先认识两个类

1.requestmappinginfo

封装requestmapping注解

包含http请求头的相关信息

一个实例对应一个requestmapping注解

2.handlermethod

封装controller的处理请求方法

包含该方法所属的bean对象、该方法对应的method对象、该方法的参数等

Spring MVC学习笔记之Controller查找(基于Spring4.0.3)

requestmappinghandlermapping的继承关系

在springmvc初始化的时候

首先执行requestmappinghandlermapping的afterpropertiesset

然后进入abstracthandlermethodmapping的afterpropertiesset

这个方法会进入该类的inithandlermethods

负责从applicationcontext中扫描beans,然后从bean中查找并注册处理器方法

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//scan beans in the applicationcontext, detect and register handler methods.
protected void inithandlermethods() {
 ...
 //获取applicationcontext中所有的bean name
 string[] beannames = (this.detecthandlermethodsinancestorcontexts ?
 beanfactoryutils.beannamesfortypeincludingancestors(getapplicationcontext(), object.class) :
 getapplicationcontext().getbeannamesfortype(object.class));
 
 //遍历beanname数组
 for (string beanname : beannames) {
 //ishandler会根据bean来判断bean定义中是否带有controller注解或requestmapping注解
 if (ishandler(getapplicationcontext().gettype(beanname))){
 detecthandlermethods(beanname);
 }
 }
 handlermethodsinitialized(gethandlermethods());
}

Spring MVC学习笔记之Controller查找(基于Spring4.0.3)

requestmappinghandlermapping#ishandler

上图方法即判断当前bean定义是否带有controlller注解或requestmapping注解

如果只有requestmapping生效吗?不会的!

因为这种情况下spring初始化的时候不会把该类注册为spring bean,遍历beannames时不会遍历到这个类,所以这里把controller换成compoent也可以,不过一般不这么做

当确定bean为handler后,便会从该bean中查找出具体的handler方法(即controller类下的具体定义的请求处理方法),查找代码如下

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * look for handler methods in a handler
 * @param handler the bean name of a handler or a handler instance
 */
protected void detecthandlermethods(final object handler) {
 //获取当前controller bean的class对象
 class<?> handlertype = (handler instanceof string) ?
 getapplicationcontext().gettype((string) handler) : handler.getclass();
 //避免重复调用 getmappingformethod 来重建 requestmappinginfo 实例
 final map<method, t> mappings = new identityhashmap<method, t>();
 //同上,也是该controller bean的class对象
 final class<?> usertype = classutils.getuserclass(handlertype);
 //获取当前bean的所有handler method
 //根据 method 定义是否带有 requestmapping
 //若有则创建requestmappinginfo实例
 set<method> methods = handlermethodselector.selectmethods(usertype, new methodfilter() {
  @override
  public boolean matches(method method) {
  t mapping = getmappingformethod(method, usertype);
  if (mapping != null) {
   mappings.put(method, mapping);
   return true;
  }
  else {
   return false;
  }
  }
 });
 
 //遍历并注册当前bean的所有handler method
 for (method method : methods) {
  //注册handler method,进入以下方法
  registerhandlermethod(handler, method, mappings.get(method));
 }

以上代码有两个地方有调用了getmappingformethod

使用方法和类型级别requestmapping注解来创建requestmappinginfo

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@override
protected requestmappinginfo getmappingformethod(method method, class<?> handlertype) {
requestmappinginfo info = null;
//获取method的@requestmapping
requestmapping methodannotation = annotationutils.findannotation(method, requestmapping.class);
if (methodannotation != null) {
 requestcondition<?> methodcondition = getcustommethodcondition(method);
 info = createrequestmappinginfo(methodannotation, methodcondition);
 //获取method所属bean的@requtestmapping注解
 requestmapping typeannotation = annotationutils.findannotation(handlertype, requestmapping.class);
 if (typeannotation != null) {
 requestcondition<?> typecondition = getcustomtypecondition(handlertype);
 //合并两个@requestmapping注解
 info = createrequestmappinginfo(typeannotation, typecondition).combine(info);
 }
}
return info;
}

这个方法的作用就是根据handler method方法创建requestmappinginfo对象。首先判断该mehtod是否含有requestmpping注解。如果有则直接根据该注解的内容创建requestmappinginfo对象。创建以后判断当前method所属的bean是否也含有requestmapping注解。如果含有该注解则会根据该类上的注解创建一个requestmappinginfo对象。然后在合并method上的requestmappinginfo对象,最后返回合并后的对象。现在回过去看detecthandlermethods方法,有两处调用了getmappingformethod方法,个人觉得这里是可以优化的,在第一处判断method时否为handler时,创建的requestmappinginfo对象可以保存起来,直接拿来后面使用,就少了一次创建requestmappinginfo对象的过程。然后紧接着进入registerhandlermehtod方法,如下

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected void registerhandlermethod(object handler, method method, t mapping) {
 //创建handlermethod
 handlermethod newhandlermethod = createhandlermethod(handler, method);
 handlermethod oldhandlermethod = handlermethods.get(mapping);
 //检查配置是否存在歧义性
 if (oldhandlermethod != null && !oldhandlermethod.equals(newhandlermethod)) {
  throw new illegalstateexception("ambiguous mapping found. cannot map '" + newhandlermethod.getbean()
   + "' bean method \n" + newhandlermethod + "\nto " + mapping + ": there is already '"
   + oldhandlermethod.getbean() + "' bean method\n" + oldhandlermethod + " mapped.");
 }
 this.handlermethods.put(mapping, newhandlermethod);
 if (logger.isinfoenabled()) {
  logger.info("mapped \"" + mapping + "\" onto " + newhandlermethod);
 }
 //获取@requestmapping注解的value,然后添加value->requestmappinginfo映射记录至urlmap中
 set<string> patterns = getmappingpathpatterns(mapping);
 for (string pattern : patterns) {
  if (!getpathmatcher().ispattern(pattern)) {
  this.urlmap.add(pattern, mapping);
  }
 }
}

这里t的类型是requestmappinginfo。这个对象就是封装的具体controller下的方法的requestmapping注解的相关信息。一个requestmapping注解对应一个requestmappinginfo对象。handlermethod和requestmappinginfo类似,是对controlelr下具体处理方法的封装。先看方法的第一行,根据handler和mehthod创建handlermethod对象。第二行通过handlermethods map来获取当前mapping对应的handlermethod。然后判断是否存在相同的requestmapping配置。如下这种配置就会导致此处抛
invocation of init method failed; nested exception is java.lang.illegalstateexception: ambiguous mapping found. cannot map...
异常

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@controller
@requestmapping("/ambiguoustest")
public class ambiguoustestcontroller {
 @requestmapping(value = "/test1")
 @responsebody
 public string test1(){
  return "method test1";
 }
 @requestmapping(value = "/test1")
 @responsebody
 public string test2(){
  return "method test2";
 }
}

在spingmvc启动(初始化)阶段检查requestmapping配置是否有歧义,这是其中一处检查歧义的(后面还会提到一个在运行时检查歧义性的地方)。然后确认配置正常以后会把该requestmappinginfo和handlermethod对象添加至handlermethods(linkedhashmap)中,静接着把requestmapping注解的value和reuqestmappinginfo对象添加至urlmap中。

registerhandlermethod方法简单总结

该方法的主要有3个职责

1. 检查requestmapping注解配置是否有歧义。

2. 构建requestmappinginfo到handlermethod的映射map。该map便是abstracthandlermethodmapping的成员变量handlermethods。linkedhashmap。

3. 构建abstracthandlermethodmapping的成员变量urlmap,multivaluemap。这个数据结构可以把它理解成map>。其中string类型的key存放的是处理方法上requestmapping注解的value。就是具体的uri

先有如下controller

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@controller
@requestmapping("/urlmap")
public class urlmapcontroller {
 @requestmapping(value = "/test1", method = requestmethod.get)
 @responsebody
 public string test1(){
  return "method test1";
 }
 
 @requestmapping(value = "/test1")
 @responsebody
 public string test2(){
  return "method test2";
 }
 
 @requestmapping(value = "/test3")
 @responsebody
 public string test3(){
  return "method test3";
 }
}

初始化完成后,对应abstracthandlermethodmapping的urlmap的结构如下

Spring MVC学习笔记之Controller查找(基于Spring4.0.3)

以上便是springmvc初始化的主要过程

查找过程

为了理解查找流程,带着一个问题来看,现有如下controller

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@controller
@requestmapping("/lookuptest")
public class lookuptestcontroller {
 
 @requestmapping(value = "/test1", method = requestmethod.get)
 @responsebody
 public string test1(){
  return "method test1";
 }
 
 @requestmapping(value = "/test1", headers = "referer=https://www.baidu.com")
 @responsebody
 public string test2(){
  return "method test2";
 }
 
 @requestmapping(value = "/test1", params = "id=1")
 @responsebody
 public string test3(){
  return "method test3";
 }
 
 @requestmapping(value = "/*")
 @responsebody
 public string test4(){
  return "method test4";
 }
}

有如下请求

Spring MVC学习笔记之Controller查找(基于Spring4.0.3)

这个请求会进入哪一个方法?

web容器(tomcat、jetty)接收请求后,交给dispatcherservlet处理。frameworkservlet调用对应请求方法(eg:get调用doget),然后调用processrequest方法。进入processrequest方法后,一系列处理后,在line:936进入doservice方法。然后在line856进入dodispatch方法。在line:896获取当前请求的处理器handler。然后进入abstracthandlermethodmapping的lookuphandlermethod方法。代码如下

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected handlermethod lookuphandlermethod(string lookuppath, httpservletrequest request) throws exception {
 list<match> matches = new arraylist<match>();
 //根据uri获取直接匹配的requestmappinginfos
 list<t> directpathmatches = this.urlmap.get(lookuppath);
 if (directpathmatches != null) {
  addmatchingmappings(directpathmatches, matches, request);
 }
 //不存在直接匹配的requetmappinginfo,遍历所有requestmappinginfo
 if (matches.isempty()) {
  // no choice but to go through all mappings
  addmatchingmappings(this.handlermethods.keyset(), matches, request);
 }
 //获取最佳匹配的requestmappinginfo对应的handlermethod
 if (!matches.isempty()) {
  comparator<match> comparator = new matchcomparator(getmappingcomparator(request));
  collections.sort(matches, comparator);
 
  if (logger.istraceenabled()) {
  logger.trace("found " + matches.size() + " matching mapping(s) for [" + lookuppath + "] : " + matches);
  }
  //再一次检查配置的歧义性
  match bestmatch = matches.get(0);
  if (matches.size() > 1) {
  match secondbestmatch = matches.get(1);
  if (comparator.compare(bestmatch, secondbestmatch) == 0) {
   method m1 = bestmatch.handlermethod.getmethod();
   method m2 = secondbestmatch.handlermethod.getmethod();
   throw new illegalstateexception(
     "ambiguous handler methods mapped for http path '" + request.getrequesturl() + "': {" +
     m1 + ", " + m2 + "}");
  }
  }
 
  handlematch(bestmatch.mapping, lookuppath, request);
  return bestmatch.handlermethod;
 }
 else {
  return handlenomatch(handlermethods.keyset(), lookuppath, request);
 }
}

进入lookuphandlermethod方法,其中lookuppath="/lookuptest/test1",根据lookuppath,也就是请求的uri。直接查找urlmap,获取直接匹配的requestmappinginfo list。这里会匹配到3个requestmappinginfo。如下

Spring MVC学习笔记之Controller查找(基于Spring4.0.3)

然后进入addmatchingmappings方法

?
1
2
3
4
5
6
7
8
private void addmatchingmappings(collection<t> mappings, list<match> matches, httpservletrequest request) {
 for (t mapping : mappings) {
  t match = getmatchingmapping(mapping, request);
  if (match != null) {
  matches.add(new match(match, handlermethods.get(mapping)));
  }
 }
}

这个方法的职责是遍历当前请求的uri和mappings中的requestmappinginfo能否匹配上,如果能匹配上,创建一个相同的requestmappinginfo对象。再获取requestmappinginfo对应的handlermethod。然后创建一个match对象添加至matches list中。执行完addmatchingmappings方法,回到lookuphandlermethod。这时候matches还有3个能匹配上的requestmappinginfo对象。接下来的处理便是对matchers列表进行排序,然后获取列表的第一个元素作为最佳匹配。返回match的handlermethod。这里进入requestmappinginfo的compareto方法,看一下具体的排序逻辑。代码如下

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public int compareto(requestmappinginfo other, httpservletrequest request) {
 int result = patternscondition.compareto(other.getpatternscondition(), request);
 if (result != 0) {
  return result;
 }
 result = paramscondition.compareto(other.getparamscondition(), request);
 if (result != 0) {
  return result;
 }
 result = headerscondition.compareto(other.getheaderscondition(), request);
 if (result != 0) {
  return result;
 }
 result = consumescondition.compareto(other.getconsumescondition(), request);
 if (result != 0) {
  return result;
 }
 result = producescondition.compareto(other.getproducescondition(), request);
 if (result != 0) {
  return result;
 }
 result = methodscondition.compareto(other.getmethodscondition(), request);
 if (result != 0) {
  return result;
 }
 result = customconditionholder.compareto(other.customconditionholder, request);
 if (result != 0) {
  return result;
 }
 return 0;
}

代码里可以看出,匹配的先后顺序是value>params>headers>consumes>produces>methods>custom,看到这里,前面的问题就能轻易得出答案了。在value相同的情况,params更能先匹配。所以那个请求会进入test3()方法。再回到lookuphandlermethod,在找到handlermethod。springmvc还会这里再一次检查配置的歧义性,这里检查的原理是通过比较匹配度最高的两个requestmappinginfo进行比较。此处可能会有疑问在初始化springmvc有检查配置的歧义性,这里为什么还会检查一次。假如现在controller中有如下两个方法,以下配置是能通过初始化歧义性检查的。

?
1
2
3
4
5
6
7
8
9
10
@requestmapping(value = "/test5", method = {requestmethod.get, requestmethod.post})
@responsebody
public string test5(){
 return "method test5";
}
@requestmapping(value = "/test5", method = {requestmethod.get, requestmethod.delete})
@responsebody
public string test6(){
 return "method test6";
}

现在执行 http://localhost:8080/springmvc-demo/lookuptest/test5 请求,便会在lookuphandlermethod方法中抛
java.lang.illegalstateexception: ambiguous handler methods mapped for http path 'http://localhost:8080/springmvc-demo/lookuptest/test5'异常。这里抛该异常是因为requestmethodsrequestcondition的compareto方法是比较的method数。代码如下

?
1
2
3
public int compareto(requestmethodsrequestcondition other, httpservletrequest request) {
 return other.methods.size() - this.methods.size();
}

什么时候匹配通配符?当通过urlmap获取不到直接匹配value的requestmappinginfo时才会走通配符匹配进入addmatchingmappings方法。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。

原文链接:https://www.jianshu.com/p/9a363c133270