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

news2024/11/18 13:49:16

文章目录

  • 一、前言
  • 二、核心链路分析
    • 1、确定请求映射的入口
      • 1)HandlerMapping注入Spring容器
      • 2)HandlerMethod注册到MappingRegistry
        • 1> 判断Class是否为一个Handler
        • 2> 解析Class中的所有HandlerMethod 并注册到MappingRegistry中
    • 2、请求路径匹配
      • 1)解析请求路径
        • 1> MVC层面请求路径解析
        • 2> Servlet层面请求路径解析
      • 2)根据请求路径找到相应的HandlerMethod
        • 1> 查找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个,分别为:

  1. RequestMappingHandlerMapping,请求处理器
  2. BeanNameHandlerMapping
  3. RouterFunctionMapping
  4. ResourceHandlerMapping
  5. WelcomePageHandlerMapping

针对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()方法中会对请求进行路径解析;其中会从两个维度进行路径解析:

  1. mvc层面,处理请求本身的路径;
  2. 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.
	 * <p>As the value returned by {@code request.getContextPath()} is <i>not</i>
	 * 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.
	 * <p>As the value returned by {@code request.getRequestURI()} is <i>not</i>
	 * decoded by the servlet container, this method will decode it.
	 * <p>The URI that the web container resolves <i>should</i> 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 < requestUri.length()) && (index2 < mapping.length()); index1++, index2++) {
		char c1 = requestUri.charAt(index1);
		char c2 = mapping.charAt(index2);
		if (c1 == ';') {
			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()逻辑;

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

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

        private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
        
      • 将RequestMappingInfo 和 HandlerMethod 作为kv保存在mappingLookup缓存中。

        private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
        
    2. 请求打过来之后,首先会对请求路径从两个维度进行处理;

      • APP层面,处理请求本身的路径;
        • 将requestURI路径和请求头的contextPath做差值。
        • 比如:requestURI是“/test/a”,contextPath是“/test”,则返回的请求路径为“/a”。
      • SERVLET层面,处理请求 和 Servlet配置的Mapping 的关系;
        • 比如:servlet mapping = “/test/*”,假如requestURI是“/test/a”,则会去找“/a”路径。
    3. 然后根据解析后的请求路径去MappingRegistry中的urlLookup缓存找RequestMappingInfo;

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

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

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

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

    4. 然后再根据RequestMappingInfoMappingRegistry中的mappingLookup缓存找HandlerMethod。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/398335.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JavaScript速成之路】JavaScript函数

&#x1f4c3;个人主页&#xff1a;「小杨」的csdn博客 &#x1f525;系列专栏&#xff1a;【JavaScript速成之路】 &#x1f433;希望大家多多支持&#x1f970;一起进步呀&#xff01; 文章目录前言1&#xff0c;函数基础1.1&#xff0c;函数概念1.2&#xff0c;函数使用1.3&…

sHMIctrl 曲线控件使用

目录 效果 官方介绍 ​编辑 名词解释 使用方法 显示点 点在X轴位置 点在Y轴位置 量程 单位 如何设置自动标尺数据 设置量程 设置单位 效果 官方介绍 名词解释 四条曲线&#xff1a;同时最多使用4条曲线。 每条条曲线最多512点&#xff1a;X2-X1-Xn&#xff1b; 数据自定…

认识CSS值如何提高写前端代码的效率

&#x1f31f;所属专栏&#xff1a;前端只因变凤凰之路&#x1f414;作者简介&#xff1a;rchjr——五带信管菜只因一枚&#x1f62e;前言&#xff1a;该系列将持续更新前端的相关学习笔记&#xff0c;欢迎和我一样的小白订阅&#xff0c;一起学习共同进步~&#x1f449;文章简…

Spring注解开发之组件注册(一)

Spring注解开发 Spring注解开发之组件注册下半篇 IOC&#xff0c;中文名为控制反转&#xff0c;是将Java的bean对象存储在容器中&#xff0c;当需要使用时&#xff0c;通过名字获取该对象。而不是通过new关键字去创建。 1.Configuration & Bean给容器中注册组件 第一种&…

K_A16_003 基于STM32等单片机采集薄膜压力传感器参数串口与OLED0.96双显示

K_A16_003 基于STM32等单片机采集薄膜压力传感器参数串口与OLED0.96双显示一、资源说明二、基本参数参数引脚说明三、驱动说明对应程序:四、部分代码说明1、接线引脚定义STM32F103C8T6薄膜压力传感器模块五、基础知识学习与相关资料下载六、视频效果展示与程序资料获取七、注意…

Allegro如何在PCB中添加层面操作指导

Allegro如何在PCB中添加层面操作指导 在用Allegro做PCB设计的时候,根据需要,会在PCB中额外添加一些额外的层面,如下图 如何添加,具体操作如下 点击Setup点击Subclasses

实在智能RPA入选中国信通院《高质量数字化转型产品及服务全景图》

近日&#xff0c;中国信息通信研究院“高质量数字化转型创新发展大会暨中国信通院‘铸基计划’年度峰会”在北京召开&#xff0c;大会上信通院揭晓了《高质量数字化转型产品及服务全景图&#xff08;2022&#xff09;》&#xff08;以下称“全景图”&#xff09;。实在智能凭借…

自己定义typescript的类型声明文件xx.d.ts

****内容预警***菜鸟新手内容&#xff0c;大佬请绕道&#xff0c;不对的请指出我们在使用typescript的使用&#xff0c;如果安装一个包没有相应的类型声明文件&#xff0c;ts的类型检查就会报错&#xff0c;所以我们经常会安装npm包对应的types类型声明包&#xff0c;比如uuid …

手把手教你安装Linux!!!

文章目录Linux简述它们的区别安装CentOS①下载CentOS②安装Linux有两种方式③下载模拟软件④安装vmware⑤创建虚拟机⑥安装操作系统Linux简述 在国内比较流行的两款Linux发行版本CentOS和ubuntu 它们的区别 ubuntu&#xff1a;页面更加的华丽比较漂亮&#xff0c;它对计算机…

【bioinfo】融合检测软件FusionMap分析流程和报告结果

文章目录写在前面FusionMap融合检测原理FusionMap与其他软比较FusionMap分析流程FusionMap结果文件说明FusionMap mono CUP设置图片来源: https://en.wikipedia.org/wiki/Fusion_gene写在前面 下面主要内容是关于RNA-seq数据分析融合&#xff0c;用到软件是FusionMap 【Fusion…

连接微信群、Slack 和 GitHub:社区开放沟通的基础设施搭建

NebulaGraph 社区如何构建工具让 Slack、WeChat 中宝贵的群聊讨论同步到公共领域。 要开放&#xff0c;不要封闭 在开源社区中&#xff0c;开放的一个重要意义是社区内的沟通、讨论应该是透明、包容并且方便所有成员访问的。这意味着社区中的任何人都应该能够参与讨论和决策过…

编写程序:有92号和95号汽油可以选择,选择你需要的汽油,并输入需要加油的升数,点击按钮“`计算总价钱`“在div中可以得到你所需要支付的价格

需求&#xff1a; 有92号汽油和95号可以选择&#xff0c;选择你需要的汽油&#xff0c;并输入需要加油的升数&#xff0c;点击按钮"计算总价钱"在div中可以得到你所需要支付的价格。结构如下图所示&#xff1a; 详细代码如下&#xff1a; <!DOCTYPE html> &l…

图像识别技术OpenCV | C++版本

基础入门 图像与信号 图像 图像是人对视觉感知的物质再现。图像可以由光学设备获取&#xff0c;也可以人为创作。随着数字采集技术和信号处理理论的发展&#xff0c;越来越多的图像以数字形式存储。因而&#xff0c;有些情况下”图像“一词实际上是指数字图像。图像相关的话…

YOLOv8初体验:检测、跟踪、模型部署

安装 YOLOv8有两种安装方式&#xff0c;一种是直接用pip命令安装&#xff1a; pip install ultralytics另外一种是通过源码安装&#xff1a; git clone https://github.com/ultralytics/ultralytics cd ultralytics pip install -e .[dev]安装完成后就可以通过yolo命令在命令…

JavaScript的错误类型数据

在使用JavaScript开发过程中&#xff0c;当我们遇见浏览器控制台中出现的报错时&#xff0c;如何从这些错误类型快速定位到问题代码是一种必不可少的技能&#xff0c;下面我们来看看JavaScript的7种错误类型&#xff08;卷起来…&#xff09; 1、SyntaxError&#xff1a;语法错…

IP地址、网段处理模块IPy

【小白从小学Python、C、Java】【计算机等级考试500强双证书】【Python-数据分析】IP地址、网段处理模块IPy选择题以下关于python代码表述错误的一项是?from IPy import IPprint("【执行】IP(192.168.0.0/24).len()")print(IP(192.168.0.0/24).len())print("【…

realman——使用Moveit控制Gazebo中的机械臂

文章目录 概述新建工作区配置说明MoveIt端的配置机器人端的配置关节轨迹控制器关节状态控制器运行效果可能存在的问题概述 MoveIt!与 Gazebo 的联合仿真,其主要思路为搭建 ros_control 和 MoveIt!的桥梁。先在 MoveIt!端配置关节和传感器接口 yaml 文件,将其加载到 rviz 端;…

Java 某厂面试题真题合集

哈喽~大家好&#xff0c;这篇来看看Java 某厂面试题真题合集。 &#x1f947;个人主页&#xff1a;个人主页​​​​​ &#x1f948; 系列专栏&#xff1a;【日常学习上的分享】 &#x1f949;与这篇相关的文章&#xff1a; Spr…

leetcode-70 爬楼梯(java实现)

爬楼梯题目分析1 递归写法动态规划解法题目 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 分析1 递归写法 如果要爬上第n阶&#xff0c;要么是从第n-1上面再爬1阶上去的&#xff0c;要么是从…

Postman简介及接口测试流程(小菜鸟攻略)

目录 前言 一、常见接口 二、前端和后端 三、什么是接口测试 四、接口组成 1、接口说明 2、调用url 3、请求方法&#xff08;get\post&#xff09; 4、请求参数、参数类型、请求参数说明 5、返回参数说明 五、为什么要做接口测试 本章主要介绍如何使用postman做接口…