Spring源码篇(1)RequestMappingHandlerMapping(Handler的注册)

JAVA && Spring && SpringBoot2.x — 学习目录

Spring源码篇(1)—RequestMappingHandlerMapping(Handler的注册)
Spring源码篇(2)—RequestMappingInfo与RequestCondition(Handler的映射)
SpringBoot2.x—定制HandlerMapping映射规则

1. SpringMVC中的HandlerMapping

图1-SpringMVC的执行流程.png

图1来源——SpringMVC执行流程和原理

组件 名称 作用
DispatcherServlet 前端控制器 接受请求,响应结果
HandlerMapping 处理器映射器 根据请求URL查找handler,获取Handler链
HandlerAdapter 处理器适配器 按照特定规则,去执行handler
Handler 处理器(Controller) 接受用户请求,调用业务处理方法
ViewResolver 视图解析器 进行视图解析,将逻辑视图解析为物理视图
View 视图 将数据展示给用户的页面,例如JSP、freemarker

根据上述描述,HandlerMapping实际上完成了两件工作:

  1. 根据请求获取到HandlerMethod,即定位到Controller层的那个方法进行处理;
  2. 将HandlerMethod与Interceptor封装为一个HandlerExecutionChain。

1.1 HandlerMapping接口

HandlerMapping是一个接口类,作用便是根据request获取HandlerExecutionChain,其接口方法如下所示:

public interface HandlerMapping {
    @Nullable
    HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

1.2 本篇讨论的重点

该接口只有一个方法,便是返回HandlerExecutionChain,那么HandlerMapping中的对象是Handler是怎么被缓存,以便后续可以快速进行映射?


本篇讨论的重点.png

2. Handler注册原理

1. @RequestMapping标签的属性

请求与容器中的@RequestMapping注解参数进行匹配,获取到最符合条件的HandlerMethod。那么@RequestMapping的参数是什么?

属性 作用 示例值
name 名字 默认策略:类名#方法名
path/value 映射路径 value = "test"
method 方法类型 method = RequestMethod.GET
params 请求参数 params = "myParam=myValue"
headers 请求头 headers = "Referer=http://www.baidu.com"
consumes 请求的content-type字段 consumes = "text/plain"
produces 请求的accept字段 produces = {"text/plain", "application/*"}

在项目启动时,会将方法上的@RequestMapping会被转换为RequestMappingInfo对象,保存起来,以便后续request请求与给定的RequestMappingInfo进行匹配。

并且RequestMappingInfo对象预留了一个属性,用于用户统一的为所有@RequestMapping注解方法配置。

2. Handler注册的流程

项目启动之后,Spring可以获取到容器中的所有Bean对象。而RequestMappingHandlerMapping@RequestMapping标签的处理器映射器。

  1. 根据@Controller和@RequestMapping标签筛选Bean;
  2. 根据@RequestMapping标签来筛选Bean中所有方法,将@RequestMapping标签解析为RequestMappingInfo对象。
  3. RequestMappingInfomethod对象进行注册
HandlerMapping注册handler流程.png

3. RequestMappingInfo对象

在映射HandlerMethod时,通过url获取到一组RequestMappingInfo对象,而后每个mapping对象都要和request的属性进行匹配。再次获取到一组RequestMappingInfo对象,使用比较器获取到一组最优的RequestMappingInfo对象。

上文所述,@RequestMappingInfo注解最终解析为RequestMappingInfo对象:

public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {

    @Nullable
    private final String name;
    //请求路径条件(该对象完成了url合并,筛选url,择优url)
    private final PatternsRequestCondition patternsCondition;
    //请求方法条件
    private final RequestMethodsRequestCondition methodsCondition;
    //请求参数条件
    private final ParamsRequestCondition paramsCondition;
    //请求头条件
    private final HeadersRequestCondition headersCondition;
    //请求content-type条件
    private final ConsumesRequestCondition consumesCondition;
    //请求accept条件
    private final ProducesRequestCondition producesCondition;
    //自定义请求条件
    private final RequestConditionHolder customConditionHolder;
}

处理@RequestMapping含有的属性,SpringMVC为我们提供了一个自定义条件,用户可以进行统一的配置,灵活的筛选Controller中方法执行业务。

4. 项目启动时注册了什么信息

源码:org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry

class MappingRegistry {
        //Key:RequestMappingInfo(映射信息)。value:mapping、handlerMethod、directUrls、mappingName
        private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
        //key:Key:RequestMappingInfo(映射信息)。value:HandlerMethod
        private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
        //Key:请求路径。value:映射信息
        private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
       //Key:mappingName(默认策略:类名#方法名)。value:一组HandlerMethod
        private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
       //key:HandlerMethod。value:cors(跨域资源共享策略)
        private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
}
请求映射url流程.png

映射关系:url->mapping->handlerMethod->corsConfiguration。故这些信息都要被注册。

2. Handler的注册源码

源码:org.springframework.web.servlet.handler.AbstractHandlerMethodMapping

2.1 根据子类筛选Bean对象

image.png
image.png

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#isHandler中,若是该类上存在@Controller注解或者@RequestMapping注解的类均可以通过。

image.png

2.2 根据子类获取MappingInfo对象

Spring拦截请求是方法级别的。processCandidateBean方法中筛选出容器所有的@Controller@RequestMapping类。对方法的筛选(甚至可以理解为将Handler转化为Mapping)还是需要在子类中实现的。

image.png
  1. AbstractHandlerMethodMapping的(抽象)钩子方法。RequestMappingHandlerMapping便是根据方法或类上是否存在@RequestMapping注解来决定是否解析该方法。
  2. 注解解析完毕后,将类注解和方法注解进行合并。
    @Override
    @Nullable
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        //判断方法上是否存在@RequestMapping注解,存在便解析为RequestMappingInfo对象
        RequestMappingInfo info = createRequestMappingInfo(method);
        if (info != null) {
        //判断类上是否存在@RequestMapping注解,存在便解析为RequestMappingInfo对象
            RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
            if (typeInfo != null) {
                //两个对象进行合并
                info = typeInfo.combine(info);
            }
            String prefix = getPathPrefix(handlerType);
            if (prefix != null) {
                info = RequestMappingInfo.paths(prefix).build().combine(info);
            }
        }
        return info;
    }
    @Nullable
    private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
        //判断类/方法上是否存在钩子方法
        RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
        //(具体钩子方法)获取自定义的请求条件
        RequestCondition<?> condition = (element instanceof Class ?
                getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
        return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
    }

用户继承RequestMappingHandlerMapping类重写getCustomMethodConditiongetCustomTypeCondition方法,自定义RequestCondition对象。

优点:request不仅可以@RequestMapping属性进行匹配(当然匹配的规则是固定的),还可以使用自定义规则进行匹配。一般在@RequestMapping类/方法上使用自定义注解。这样在解析含有@RequestMapping注解的类/方法时,用户便可以解析自定义注解的值,从而创建自定义的RequestCondition对象。
本质上,由@RequestMapping注解的属性来决定mapping的属性条件,转换为由自定义注解来决定mapping的属性条件。

例如:API版本控制,前提是不同的版本需要相同的url,但根据请求需要定位到不同的业务方法上。可以在@RequestMapping中定义headers属性。但是源码中根据headers属性进行精确匹配(即完全相等)。
若是只有0.1和0.2个版本,用户上送0.3也要匹配到0.2版本,那么应该如何处理?
便要使用getCustomMethodCondition创建自定义RequestCondition对象,以便完成自定义业务的筛选。

2.3 注册mapping和handler

那么mapping对象的结构是怎样的呢,其实便是将@RequestMapping注解中的属性封装为mapping对象。而在注册的时候,mapping可以作为key。根据用户request的url、header、param等属性来匹配mapping对象。

mapping对象结构.png
        public void register(T mapping, Object handler, Method method) {
            this.readWriteLock.writeLock().lock();
            try {
                HandlerMethod handlerMethod = createHandlerMethod(handler, method);
               //若是解析@RequestMapping方法得到的mapping对象是完全相同的,则会在启动的时候出现异常。
                assertUniqueMethodMapping(handlerMethod, mapping);
                this.mappingLookup.put(mapping, handlerMethod);

                List<String> directUrls = getDirectUrls(mapping);
                for (String url : directUrls) {
                    //将url和mapping进行绑定
                    this.urlLookup.add(url, mapping);
                }

                String name = null;
                if (getNamingStrategy() != null) {
                //根据命名策略,获取名字:类名#方法名
                    name = getNamingStrategy().getName(handlerMethod, mapping);
                    addMappingName(name, handlerMethod);
                }
                //判断方法/类上是否存在@CrossOrigin,若存在,将跨域配置与mapping绑定
                CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
                if (corsConfig != null) {
                    this.corsLookup.put(handlerMethod, corsConfig);
                }

                this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
            }
            finally {
                this.readWriteLock.writeLock().unlock();
            }
        }

在项目中不应该存在完全相同的mapping对象,否则的话,会在启动的时候出现异常。

比如在项目中存在多个版本的接口。这些版本使用的url路径必须相同,我们可以在@RequestMapping中定义header属性来区分版本。这样即使存在相同的url参数,但由于每个@RequestMapping注解的方法header参数不同。那么也属于不同的RequestMappingInfo对象。即DispatcherServlet可以通过request确定唯一的handler对象。

官网文档

RequestMappingHandlerMapping API文档

推荐阅读

Spring MVC之RequestMappingHandlerMapping匹配

Spring Mvc之定制RequestMappingHandlerMapping

springboot 自定义 RequestMappingHandlerMapping / HandlerInterceptor 报错

RequestMappingHandlerMapping的用法

自定义RequestMappingHandlerMapping实现API版本控制

Spring源码从入门到放弃-Controller注册

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