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 {

    @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> registry = new HashMap<>();
        //key:Key:RequestMappingInfo(映射信息)。value:HandlerMethod
        private final Map mappingLookup = new LinkedHashMap<>();
        //Key:请求路径。value:映射信息
        private final MultiValueMap urlLookup = new LinkedMultiValueMap<>();
       //Key:mappingName(默认策略:类名#方法名)。value:一组HandlerMethod
        private final Map> nameLookup = new ConcurrentHashMap<>();
       //key:HandlerMethod。value:cors(跨域资源共享策略)
        private final Map 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 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注册

你可能感兴趣的:(Spring源码篇(1)RequestMappingHandlerMapping(Handler的注册))