SpringMVC工作原理之处理映射[HandlerMapping]

请求过来是怎么映射到对应的方法上,这里离不开映射处理器 HandlerMapping,今天这篇笔记就来探究 HandlerMapping 实现逻辑。
本篇笔记主要分析SpringMVC 5.1.1 这个版本。

SpringMVC运行流程

SpringMVC 内部是根据 HandlerMappingRequestController 里面的方法对应起来的,为了方便理解,我这里把实现它的子类统称为映射处理器[ps: 自己一时兴起瞎起的,不准确还请见谅]。
HandlerMapping 功能就是根据请求匹配到对应的 Handler,然后将找到的 Handler 和所有匹配的 HandlerInterceptor(拦截器)绑定到创建的 HandlerExecutionChain 对象上并返回。
HandlerMapping 只是一个接口类,不同的实现类有不同的匹对方式,根据功能的不同我们需要在 SpringMVC 容器中注入不同的映射处理器 HandlerMapping

简单工作图如下

HandlerMapping.jpg

1 HandlerMapping 接口

1.1 HandlerMapping 注入

DispatcherServlet 类中有下面这个方法

public class DispatcherServlet extends FrameworkServlet {
  private void initHandlerMappings(ApplicationContext context) {...}
}

容器被初始化的时候会被调用,加载容器中注入的 HandlerMapping。其实常用到的 HandlerMapping 都是由 <mvc:annotation-driven /> 标签帮我们注册的(包括 RequestMappingHandlerMappingBeanNameUrlHandlerMapping),如果没有写该标签系统也会帮我们注入默认的映射器,当然也有些需要我们自己手动注入。

1.2 HandlerExecutionChain 初始化

DispatcherServlet 类中,doDispatch(..) 方法总通过调用本类的 getHandler(..) 方法得到 HandlerExecutionChain 对象。

看到这里肯定很模糊,具体 HandlerMapping 内部通过调用 getHandler(..) 得到 HandlerExecutionChain 对象细节请往下看。

1.3 HandlerMapping 接口

HandlerMapping 接口中只有一个方法

首先我们来看下实现类结构

展开细看

大致上分为两大类 AbstractUrlHandlerMappingAbstractHandlerMethodMapping。都继承自 AbstractHandlerMapping 抽象类,实现 HandlerMapping 接口。

2 HandlerMapping 接口实现抽象类 AbstractHandlerMapping

AbstractHandlerMapping 类中实现 getHandler(..) 接口方法得到 HandlerExecutionChain 对象

同时 AbstractHandlerMapping 继承 WebApplicationObjectSupport,初始化时会自动调用模板方法 initApplicationContext

2.1 AbstractHandlerMapping 实现类分支之一 AbstractUrlHandlerMapping

AbstractUrlHandlerMapping:URL 映射的抽象基类,提供将处理程序映射到 Controller,所以该类最终直接返回的 handler 就是 Controller 对象。

实现父抽象类的抽象方法 getHandlerInternal(..) 匹配并返回对应的 Handler 对象。

接下来咱们看看根据路径匹对 handler 的方法 lookupHandler(..)

上面代码可以看出从 this.handlerMap 中通过 urlPath 匹对找到对应的 handler 对象。那接下来就看下开始是怎么将 handler 对象加入到 this.handlerMap 集合中是关键。

那接下来调研这个 protected void registerHandler(String urlPath, Object handler) {} 这个方法什么时候调用,怎么调用就是接下来的重点了。
从源码来看是在 AbstractUrlHandlerMapping 子类里面调用。

AbstractUrlHandlerMapping 的子类从上面截图的类结构可以看出来,大致分为两类:

  • 间接继承 AbstractUrlHandlerMappingBeanNameUrlHandlerMapping
  • 直接继承 AbstractUrlHandlerMappingSimpleUrlHandlerMapping
2.1.1 BeanNameUrlHandlerMapping

首先来看其父类 AbstractDetectingUrlHandlerMapping 怎么调用 registerHandler(String urlPath, Object handler) 又怎么匹配到配置在容器中的 handler 并将其注入到 AbstractUrlHandlerMappingthis.handlerMap 中。

接下来看下 BeanNameUrlHandlerMapping 里面的 determineUrlsForHandler(..) 方法是怎么实现匹对 beanName 是否跟该映射器相关并返回 URLs 的逻辑吧。

从上面的源码分析我们可以得知,在 SpringMVC 容器中,且在注入了 BeanNameUrlHandlerMapping 映射器的时候,只要是以 "/" 开头的 bean 的 name,都会作为该映射器匹配的 Handler 对象。

接下来咱们就自定义一个经过该映射器匹对的视图,但是在自定义之前我们需要先了解下 Controller 这个接口。因为使用 AbstractUrlHandlerMapping 的实现类时,需要让控制层的类实现 Controller 接口(一般继承 AbstractController 即可),另外还有一些已经实现了的 Controller 类,如下图所示。但是不论是自己实现 Controller 接口还是继承系统已经实现的类,都只能处理一个请求。

首先编写控制层代码

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HelloController extends AbstractController {

    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("访问方法进来了");
        ModelAndView mv = new ModelAndView();
        mv.setViewName("success");
        return mv;
    }
}

接下面在 SpringMVC 容器中注入 BeanNameUrlHandlerMapping 映射器和自定义的 Controller

<!-- 注册 HandlerMapping -->
<!--<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"></bean>-->

<!-- <mvc:annotation-driven /> 自动帮我们注入 BeanNameUrlHandlerMapping 映射器, 所以与上面手动注入该映射器选其一就行  -->
<mvc:annotation-driven />

<!-- 注册 Handler -->
<bean id="/hello" class="com.ogemray.urlHandlerMapping.HelloController"></bean>

注意手动注入 BeanNameUrlHandlerMapping 映射器记得不要跟 <mvc:annotation-driven /> 标签自动帮我们注入重复(如自己手动注入要么放在 <mvc:annotation-driven /> 标签之前,要么直接不写),不然重复注册两次该映射器虽说没有大的影响,但是也有点浪费内存没必要。
注意自定义 Controller 实现类注入 beanidname 必须以 "/" 开头,因为上面分析源码说过,BeanNameUrlHandlerMapping 映射器主要映射以 "/" 开头的 beanName。

2.1.2 SimpleUrlHandlerMapping

在接下来咱们看看该映射器是怎么调用父类的 registerHandler(String urlPath, Object handler) 方法将 handler 加进 AbstractUrlHandlerMappingthis.handlerMap 中。

从上面源码可以看出 SimpleUrlHandlerMapping 映射器跟前面 BeanNameUrlHandlerMapping 映射器有点不一样。后者是有点类似遍历容器里面有所的 beannameid 找到匹配的,并且 beannameid 有特殊要求,匹配的则加入。而前者则是先将加入该映射器的 handler 先加进该映射器的一个集合属性里面,容器初始化的时候免去了遍历麻烦的步骤。

接下来咱们就自定义一个经过该映射器匹对的视图。

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HelloController extends AbstractController {

    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("访问方法进来了");
        ModelAndView mv = new ModelAndView();
        mv.setViewName("success");
        return mv;
    }
}

接下面在 Spring MVC 容器中注入 SimpleUrlHandlerMapping 映射器和自定义的 Controller

<!-- 注册 HandlerMapping -->
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
        <props>
            <prop key="/hello">helloController</prop>
        </props>
    </property>
</bean>
<!-- 注册 Handler -->
<bean id="helloController" class="com.ogemray.urlHandlerMapping.HelloController"></bean>

2.2 AbstractHandlerMapping 实现类分支之二 AbstractHandlerMethodMapping

AbstractHandlerMethodMapping 最终获取的 handlerHandlerMethod 类型对象。实现该抽象类的子类映射器具体映射什么样的方法,怎么实现请求和方法的映射,该抽象类都给出了抽象接口,可高度自定义[ps: 这里给我印象很深,学到了]。

接下来看下该类的实现链:

具体实现类只有一个 RequestMappingHandlerMapping,在开始下面正式逻辑分析之前,我们需要了解几个类。

2.2.1 简单了解 HandlerMethod

HandlerMethod 其实可以简单理解为保持方法信息的pojo类

2.2.2 RequestMappingInfo

主要用来记录方法上 @RequestMapping() 注解里面的参数,针对 RequestMappingHandlerMapping 映射器来使用。

在容器初始化过程中创建映射器(RequestMappingHandlerMapping)对象时,会寻找所有被@Controller 注解类中被 @RequestMapping 注解的方法,然后解析方法上的 @RequestMapping 注解,把解析结果封装成 RequestMappingInfo 对象,也就是说RequestMappingInfo 对象是用来装载方法的匹配相关信息,每个匹配的方法都会对应一个 RequestMappingInfo 对象。现在大家应该能明白 RequestMappingInfo 的作用了吧。

  • PatternsRequestCondition:模式请求路径过滤器,对应记录和判断 @RequestMapping 注解上的 value 属性。
  • RequestMethodsRequestCondition:请求方法过滤器,对应记录和判断 @RequestMapping 注解上的 method 属性。
  • ParamsRequestCondition:请求参数过滤器,对应记录和判断 @RequestMapping 注解上的 params 属性。
  • HeadersRequestCondition:头字段过滤器,对应记录和判断 @RequestMapping 注解上的 headers 属性。
  • ConsumesRequestCondition:请求媒体类型过滤器,对应记录和判断 @RequestMapping 注解上的 consumes 属性。
  • ProducesRequestCondition:应答媒体类型过滤器,对应记录和判断 @RequestMapping 注解上的 produces 属性。
  • RequestConditionHolder:预留自定义扩展过滤器。
2.2.3 进入 AbstractHandlerMethodMapping 映射器内部

① 首先来看下该类实现父抽象类(AbstractHandlerMapping) 的抽象方法 getHandlerInternal(..) 匹配并返回对应的 handler 对象。

跟前面的另一个实现分支 AbstractUrlHandlerMapping 实现看起来差不多,都是根据请求路径来匹对,但是内部配对方式有什么不同还需要我们接着往下看。

注意:

  • Match 就是该抽象类里面自定义的一个内部类,用来记录方法标记信息对象 mapping 和方法源信息对象 HandlerMethod
  • 当请求为 restful 风格时,将会遍历所有的 mapping,然后一个个匹对,非常耗时和费资源。优化请参考 springMVC在restful风格的性能优化
  • 上面的两个抽象方法(getMatchingMapping(..)getMappingComparator(..))
    前者要实现检查提供的请求映射信息中的条件是否与请求匹配。
    后者要实现当一个 Request 对应多个 mapping 时的择优方案。

② 看下存储映射关系对象(MappingRegistry)内部结构
说到这里,可能大家对于这个 this.mappingRegistery 对象十分好奇,里面到底是怎么存储数据的,先是可以根据 lookupPath 找到 List<mapping>,接着后来又根据 mapping 找到 HandlerMethod 对象。

该实体类里面最重要的两个记录集合分别是 mappingLookupurlLookup

  • urlLookup:主要用来记录 lookupPath 请求路径对应的 mapping 集合。
    这里 Spring 留了一个很活的机制,拿 @RequestMapping 注解来说,他的 value 属性本身就是一个字符数组,在多重设置中难免有路径重复的,所以最终有可能会出现一个 lookupPath 对应多个 RequestMappingInfo,最终在请求过来的时候给了自定义抽象方法让实现类自己实现择优的方式。
    MutivalueMap 是 SpringMVC 自定义的一个 Map 类,key 对应的 value 是一个集合,这从名字上也能看出来。

  • mappingLookup:key 是 mapping 对象,value 是 HandlerMethod 对象,最终是通过 lookupPathurlLookup 集合中找到对应的 mapping 对象,通过 mappingmappingLookup 集合中找到 HandlerMethod 对象。

③ 看下是怎么将映射关系装进缓存(MappingRegistry) 对象中的

容器初始化的时候都干了些什么

isHandler(..) 是该抽象类定义的抽象方法,由实现类自己去实现匹对哪些类。看下 RequestMappingHandlerMapping 映射器是怎么实现的吧

看来 RequestMappingHandlerMapping 映射器,只要类上有 ControllerRequestMapping 注解,就符合该映射器管辖范围。

接着解析往下看

来个分支看下 RequestMappingHandlerMapping 是怎么实现抽象方法 getMappingForMethod(..) 方法的,该映射器都匹配什么样的方法呢?

猜也能猜到,RequestMappingHandlerMapping 映射器肯定匹配有 @RequestMapping 注解的方法,并返回该方法的映射信息对象 RequestMappingInfo 对象。

下面就到了最后一步,具体这个映射关系是怎么装入映射器的 MappingRegistry 对象属性的缓存的呢?

3 总结

到这里,关于 SpringMVC 内部是怎么通过 HandlerMapping 映射器将各自对应映射的资源在容器初始的时候装到自身的缓存,在请求过来时又是怎么找到对应的资源返回最终对应的 handler 对象已经描述完了。

现在开发我们基本都不用 AbstractUrlHandlerMapping 这种类型的映射器了,但是 SpringMVC 内部还有用到的地方,例如直接 <mvc:view-controller path="" view-name=""/> 标签配置资源不经过视图控制器直接跳转就用到了 SimpleUrlHandlerMapping 这种映射器。AbstractUrlHandlerMapping 匹对解析对应请求最终返回的 handlerController 对象。
现在我们习惯直接用 @Controller@RequestMapping 这样注解来描述视图控制器的逻辑,这种资源映射用的是 AbstractHandlerMethodMapping 抽象类的子类 RequestMappingHandlerMapping 映射器,匹对解析对应的请求返回HandlerMethod 对象。

通过研究这种映射,对于我个人来说学到了很多,优秀的设计模式遵循开闭原则,扩展放开修改关闭,高度模块化同时也支持高度自定义话,优秀!!!

其他相关文章

SpringMVC入门笔记
SpringMVC工作原理之处理映射[HandlerMapping]
SpringMVC工作原理之适配器[HandlerAdapter]
SpringMVC工作原理之参数解析
SpringMVC之自定义参数解析
SpringMVC工作原理之视图解析及自定义
SpingMVC之<mvc:annotation-driven/>标签

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,265评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,078评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,852评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,408评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,445评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,772评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,921评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,688评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,130评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,467评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,617评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,276评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,882评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,740评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,967评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,315评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,486评论 2 348

推荐阅读更多精彩内容

  • 1. 配置 springMVC的核心是DispatcherServlet,它实现了HttpServlet类,在we...
    aaron1993阅读 1,280评论 0 0
  • 对于java中的思考的方向,1必须要看前端的页面,对于前端的页面基本的逻辑,如果能理解最好,不理解也要知道几点。 ...
    神尤鲁道夫阅读 804评论 0 0
  • HandlerMapping的类继承图如下: 其中有一个DefaultAnnotationHanlerMappin...
    宙斯是只猫阅读 3,355评论 0 7
  • 引言 一直以来都在使用Spring mvc,能够熟练使用它的各种组件。但是,它一直像个黑盒一样,我并不知道它内部是...
    yoqu阅读 905评论 0 24
  • 一开始画很害怕,怕自己画不好于是坚持每日一画,渐渐的找到了感觉愿前辈们多指点
    顾李李阅读 272评论 4 4