源码剖析Spring MVC如何将请求映射到Controller? 您所在的位置:网站首页 spring如何 源码剖析Spring MVC如何将请求映射到Controller?

源码剖析Spring MVC如何将请求映射到Controller?

2023-04-03 00:50| 来源: 网络整理| 查看: 265

文章目录 一、前言二、核心链路分析1、确定请求映射的入口1)HandlerMapping注入Spring容器2)HandlerMethod注册到MappingRegistry1> 判断Class是否为一个Handler2> 解析Class中的所有HandlerMethod 并注册到MappingRegistry中 2、请求路径匹配1)解析请求路径1> MVC层面请求路径解析2> Servlet层面请求路径解析 2)根据请求路径找到相应的HandlerMethod1> 查找RequestMappingInfo动态地址做正则匹配2> 查找HandlerMethod 三、总结

一、前言

最近有朋友问我:Spring MVC 中如何将请求映射到指定的Controller中的;

结合博主之前的 Spring MVC的请求执行流程 一文,这里做一个更细粒度的分析。

在这里插入图片描述

从Spring MVC的请求执行流程来看,DispatcherServlet#doDispatch()方法中会做请求的映射;具体体现在获取请求对应的HandlerExecutionChain逻辑中。

在这里插入图片描述

二、核心链路分析 1、确定请求映射的入口

DispatcherServlet#getHandler():

@Nullable protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { // 在Spring初始化的时候会加载所有的handlerMappings if (this.handlerMappings != null) { for (HandlerMapping mapping : this.handlerMappings) { // 获取请求对应的HandlerExecutionChain HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; } } } return null; } 1)HandlerMapping注入Spring容器

Spring启动时会加载所有HandlerMapping类型的Bean到IOC容器中,默认有5个,分别为:

RequestMappingHandlerMapping,请求处理器BeanNameHandlerMappingRouterFunctionMappingResourceHandlerMappingWelcomePageHandlerMapping

针对HTTP GET、POST、PUT等普通请求,RequestMappingHandlerMapping负责处理,并且其中包含了 所有可以处理的请求路径、以及 请求和相应Controller(具体的类、方法)的映射Mapping。

2)HandlerMethod注册到MappingRegistry

Spring启动时会加载RequestMappingHandlerMapping到IOC容器中,RequestMappingHandlerMapping中使用MappingRegistry 保存了所有的请求映射关系,使用HandlerMethod保存了一个请求映射。

在这里插入图片描述

RequestMappingInfoHandlerMapping 继承了 抽象类 AbstractHandlerMethodMapping, AbstractHandlerMethodMapping又实现了InitializingBean接口、重写了InitializingBean#afterPropertiesSet()方法;

因此实例化RequestMappingInfoHandlerMapping时会进入到AbstractHandlerMethodMapping#afterPropertiesSet()方法;

在这里插入图片描述

进入到initHandlerMethods()方法之后,会遍历IOC容器中所有的Bean,如果Bean被@Controller 或 @RequestMapping 注解标注,则将Class中被@RequesMapping 注解标注的方法解析为HandlerMapping、并注册到MappingRegistry中。

1> 判断Class是否为一个Handler

所谓的判断Class是否为一个Handler,即判断Class是否被@Controller 或 @RequestMapping 注解标注; 在这里插入图片描述

AbstractHandlerMethodMapping#isHandler()方法用于判断某个类是否为一个拥有 MethodHandler的处理器。

在这里插入图片描述

具体的实现在其子类RequestMappingHandlerMapping中:

在这里插入图片描述

仅仅判断类有没有被@Controller 或 @RequestMapping 注解标注。

2> 解析Class中的所有HandlerMethod 并注册到MappingRegistry中

确定一个类被@Controller 或 @RequestMapping 注解标注 后,需要进一步确定Class类中的哪几个方法是HandlerMethod(被@RequestMapping注解标注)、可以处理什么URL路径;

在这里插入图片描述

遍历类的方法,Spring封装了几层,具体的执行链路如下:

在这里插入图片描述

解析方法的HandlerMethod信息 的逻辑 被封装到函数式接口(@FunctionalInterface)一路往下传递(从放在MetadataLookup中 到 MethodCallback 中)。

(1) 解析方法的HandlerMethod信息:

在这里插入图片描述

在这里插入图片描述

如果方法没有被@RequestMapping注解标注,则返回null,否则返回具体的HandlerMapping信息,比如:

在这里插入图片描述

(2) 将HandlerMethod注册到MappingRegistry中:

在这里插入图片描述

注册完一个HandlerMethod之后,MappingRegistry的内容如下:

在这里插入图片描述

2、请求路径匹配

请求进入到DispatcherServlet之后的时序图:

在这里插入图片描述

对应的代码执行链路如下:

在这里插入图片描述

1)解析请求路径

UrlPathHelper#getLookupPathForRequest()方法中会对请求进行路径解析;其中会从两个维度进行路径解析:

mvc层面,处理请求本身的路径;SERVLET层面,处理请求 和 Servlet配置的Mapping 的关系;比如: servlet mapping = “/test/*”; request URI = “/test/a” -> “/a”.

在这里插入图片描述

1> MVC层面请求路径解析

在这里插入图片描述 这里主要做三件事:

(1)获取请求的ContextPath:

就一般请求而言,请求的contextPath都为“”;如果在RequestDispatcher include中调用,则检测include请求URL; /** * Return the context path for the given request, detecting an include request * URL if called within a RequestDispatcher include. *

As the value returned by {@code request.getContextPath()} is not * decoded by the servlet container, this method will decode it. * @param request current HTTP request * @return the context path */ public String getContextPath(HttpServletRequest request) { String contextPath = (String) request.getAttribute(WebUtils.INCLUDE_CONTEXT_PATH_ATTRIBUTE); if (contextPath == null) { contextPath = request.getContextPath(); } if (StringUtils.matchesCharacter(contextPath, '/')) { // Invalid case, but happens for includes on Jetty: silently adapt it. contextPath = ""; } return decodeRequestString(request, contextPath); }

(2)获取请求HttpServletRequest的URI:

/** * Return the request URI for the given request, detecting an include request * URL if called within a RequestDispatcher include. *

As the value returned by {@code request.getRequestURI()} is not * decoded by the servlet container, this method will decode it. *

The URI that the web container resolves should be correct, but some * containers like JBoss/Jetty incorrectly include ";" strings like ";jsessionid" * in the URI. This method cuts off such incorrect appendices. * @param request current HTTP request * @return the request URI */ public String getRequestUri(HttpServletRequest request) { String uri = (String) request.getAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE); if (uri == null) { uri = request.getRequestURI(); } return decodeAndCleanUriString(request, uri); }

(3)获取requestUri和contextPath的差值:

将给定的“mapping”(即:contextPath)与“requestUri”的开头匹配,如果匹配,则返回额外的部分;该方法用于解决 HttpServletRequest返回的上下文路径和servlet路径中没有分号内容的问题。 /** * Match the given "mapping" to the start of the "requestUri" and if there * is a match return the extra part. This method is needed because the * context path and the servlet path returned by the HttpServletRequest are * stripped of semicolon content unlike the requestUri. */ @Nullable private String getRemainingPath(String requestUri, String mapping, boolean ignoreCase) { int index1 = 0; int index2 = 0; for (; (index1 index1 = requestUri.indexOf('/', index1); if (index1 == -1) { return null; } c1 = requestUri.charAt(index1); } if (c1 == c2 || (ignoreCase && (Character.toLowerCase(c1) == Character.toLowerCase(c2)))) { continue; } return null; } if (index2 != mapping.length()) { return null; } else if (index1 == requestUri.length()) { return ""; } else if (requestUri.charAt(index1) == ';') { index1 = requestUri.indexOf('/', index1); } return (index1 != -1 ? requestUri.substring(index1) : ""); } 2> Servlet层面请求路径解析

如果配置了alwaysUseFullPath,则不会做Servlet层面的请求路径解析;

在这里插入图片描述

此处不对Servlet层面请求路径的解析进行过多讲解,一般不会走进去。

2)根据请求路径找到相应的HandlerMethod

在解析完请求的路径之后,对MappingRegistry加一个读锁,然后再做路径匹配;

在这里插入图片描述

真正的请求路径匹配逻辑在AbstractHandlerMethodMapping#lookupHandlerMethod()方法中;

先根据请求路径lookupPath从MappingRegistry的urlLookup缓存中找到路径对应的请求映射信息RequestMappingInfo。由于REST ful风格接口的缘故,可能根据请求路径会找到多个RequestMappingInfo,因此需要再对RequestMappingInfo做条件匹配,找到相应RequestMethod的RequestMappingInfo;然后再根据找到的唯一的RequestMappingInfo从MappingRegistry的mappingLookup缓存中找到请求映射信息对应的HandlerMethod,也就找到了具体的某个Controller中的某个方法。 1> 查找RequestMappingInfo

在这里插入图片描述

因为Rest ful风格的缘故,可能会找到多个RequestMappingInfo。

MappingRegistry的urlLookup缓存是在SpringBoot启动时初始化的,见文章上半部分。

动态地址做正则匹配

如果请求是动态地址,例如:@GetMapping("/get/{orderId}"),则无法从MappingRegistry的urlLookup缓存中获取到请求路径对应的RequestMappingInfo,此时需要遍历所有的RequestMappingInfo,做正则匹配,进而找到具体的HandlerMethod。

在这里插入图片描述

2> 查找HandlerMethod

在这里插入图片描述

因为请求的RequestMethod为GET,所以GET类型的RequestMappingInfo符合条件;

在这里插入图片描述 在这里插入图片描述

MappingRegistry的mappingLookup缓存也是在SpringBoot启动时初始化的,见文章上半部分。

最后将获取到的HandlerMethod一路向上返回;

在这里插入图片描述

三、总结

Spring启动时会加载HandlerMapping的所有实现类;包括:负责处理HTTP请求的RequestMappingHandlerMapping;

RequestMappingHandlerMapping中使用MappingRegistry 保存了所有的请求映射关系,使用HandlerMethod保存了一个请求映射;

RequestMappingInfoHandlerMapping间接实现了InitializingBean接口,因此RequestMappingInfoHandlerMapping实例化时会进去到重写后的InitializingBean#afterPropertiesSet()逻辑;

遍历IOC容器中所有的Bean,如果Bean被@Controller 或 @RequestMapping 注解标注,则将Class中被@RequesMapping 注解标注的方法解析为HandlerMapping、并注册到MappingRegistry中。

将请求路径 和 RequestMappingInfo 作为KV保存在urlLookup缓存中;

private final MultiValueMap urlLookup = new LinkedMultiValueMap();

将RequestMappingInfo 和 HandlerMethod 作为kv保存在mappingLookup缓存中。

private final Map mappingLookup = new LinkedHashMap();

请求打过来之后,首先会对请求路径从两个维度进行处理;

APP层面,处理请求本身的路径; 将requestURI路径和请求头的contextPath做差值。比如:requestURI是“/test/a”,contextPath是“/test”,则返回的请求路径为“/a”。 SERVLET层面,处理请求 和 Servlet配置的Mapping 的关系; 比如:servlet mapping = “/test/*”,假如requestURI是“/test/a”,则会去找“/a”路径。

然后根据解析后的请求路径去MappingRegistry中的urlLookup缓存找RequestMappingInfo;

由于Rest ful风格的存在,可能根据一个请求路径找到多个RequestMappingInfo;

所以需要进一步通过RequestMethod找到执行类型(GET/POST/PUT/DELTE)的RequestMappingInfo

动态地址无法根据请求路径找到具体的RequestMappingInfo,需要遍历所有的RequestMappingInfo做正则匹配,找到具体的RequestMappingInfo。

比如:@GetMapping("/get/{orderId}")

然后再根据RequestMappingInfo去MappingRegistry中的mappingLookup缓存找HandlerMethod。

HandlerMethod中保存了请求需要执行的Class(Controller)、Method;


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有