Spring MVC RequestMappingInfo路由条件匹配

前言

我们已经知道,被@RequestMapping标注的方法会被解析为 HandlerMethod,它也是 Spring MVC 中最常用的 Handler 类型。现在的问题是,HTTP 请求是如何路由到对应的 HandlerMethod?你可能脱口而出:根据请求的 Url 匹配啊!的确,Url 匹配是最简单一种规则,但事实上 Spring MVC 的功能之丰富超乎你想象。

比如,你可以在两个方法上定义相同的请求路径,根据请求参数的不同路由到不同的方法上处理:

@GetMapping(path = "api", params = "id")
public Object apiV1(@RequestParam("id") String id) {
    return "apiV1:" + id;
}

@GetMapping(path = "api", params = "name")
public Object apiV2(@RequestParam("name") String name) {
    return "apiV2:" + name;
}

除此之外,你还可以根据请求方法、请求头、Content-Type、Accept、甚至自定义更复杂的路由规则。Spring MVC 处理这套路由机制是很复杂的,来看下是怎么实现的吧。

RequestCondition

首先我们重新认识一下@RequestMapping注解,先了解它支持配置哪些路由条件。

public @interface RequestMapping {
  String[] path() default {};
  RequestMethod[] method() default {};
  String[] params() default {};
  String[] headers() default {};
  String[] consumes() default {};
  String[] produces() default {};
}
  • path:请求的路径数组,可以使用 Ant 模式匹配
  • method:请求的方法数组
  • params:请求的参数数组,可以指定键或键值
  • headers:请求头数组,可以指定键或键值
  • consumes:指定只接收哪些媒体类型参数的请求,匹配 Content-Type 请求头,例如:application/json
  • produces:指定返回的媒体类型,匹配 Accept 请求头,例如:text/html

这里举一个典型示例:

@RequestMapping(
        path = "get/{userId}",
        method = {RequestMethod.GET},
        headers = {"token"},
        params = {"name"},
        consumes = {"application/json"},
        produces = {"text/html"}
)
public Object getUser(@PathVariable("userId") String userId,
                      @RequestParam("name") String name) {

    return null;
}

在这个示例中,请求的 Url 必须匹配"get/{userId}“、请求方法必须是 GET、请求头必须携带 token、必须有一个 name 参数、Content-Type 必须是"application/json”、Accept 必须包含"text/html",满足以上所有条件请求才会路由到该方法。

因为要匹配的条件特别多,Spring MVC 专门为此抽象了一个接口:RequestCondition,它代表 HTTP 请求要匹配的条件。

public interface RequestCondition<T> {
  T combine(T other);
  
  T getMatchingCondition(HttpServletRequest request);
   
  int compareTo(T other, HttpServletRequest request);
}
  • combine:条件合并,例如合并方法和类上的@RequestMapping注解
  • getMatchingCondition:获得匹配的条件,例如有多个 path,返回匹配上的 path,返回 null 表示不匹配
  • compareTo:条件比较,匹配到多个候选项时选出一个最佳项

RequestCondition 有很多实现类,@RequestMapping里的每一项规则都对应一个具体的实现类:

如图所示,请求路径匹配对应 PathPatternsRequestCondition、请求方法条件匹配对应 RequestMethodsRequestCondition、请求头条件匹配对应 HeadersRequestCondition 等等。CompositeRequestCondition 组合了多个 RequestCondition,本身不具备条件匹配的能力,委托给内部的 requestConditions。

所有的实现类就不一一分析了,我们看几个常用的。

RequestMethodsRequestCondition

RequestMethodsRequestCondition 是请求方法条件类,内部用一个 Set 记录支持的请求方法,通过构造函数传入:

private RequestMethodsRequestCondition(Set<RequestMethod> methods) {
	this.methods = methods;
}

匹配规则也很简单,就是看 methods 是否包含了:

private RequestMethodsRequestCondition matchRequestMethod(String httpMethodValue) {
	RequestMethod requestMethod;
	try {
		requestMethod = RequestMethod.valueOf(httpMethodValue);
		if (getMethods().contains(requestMethod)) {
			return requestMethodConditionCache.get(httpMethodValue);
		}
		if (requestMethod.equals(RequestMethod.HEAD) && getMethods().contains(RequestMethod.GET)) {
			return requestMethodConditionCache.get(HttpMethod.GET.name());
		}
	}
	catch (IllegalArgumentException ex) {
		// Custom request method
	}
	return null;
}

HeadersRequestCondition

HeadersRequestCondition 是请求头条件类,支持多种匹配方式:

  • key:请求头必须有 key
  • !key:请求头必须没有 key
  • key=value:请求头必须有 key、且值必须是 value

配置通过构造函数传入,因为匹配的复杂性,Spring MVC 会把配置的字符串解析成表达式 HeaderExpression。

public HeadersRequestCondition(String... headers) {
	// 解析表达式
	this.expressions = parseExpressions(headers);
}

解析过程并不复杂,内部用三个变量记录键值、以及是否取反:

AbstractNameValueExpression(String expression) {
	int separator = expression.indexOf('=');
	if (separator == -1) {
		this.isNegated = expression.startsWith("!");
		this.name = (this.isNegated ? expression.substring(1) : expression);
		this.value = null;
	}
	else {
		this.isNegated = (separator > 0) && (expression.charAt(separator - 1) == '!');
		this.name = (this.isNegated ? expression.substring(0, separator - 1) : expression.substring(0, separator));
		this.value = parseValue(expression.substring(separator + 1));
	}
}

匹配的过程也不复杂,挨个和所有 HeaderExpression 匹配,必须所有都匹配通过才行:

public HeadersRequestCondition getMatchingCondition(HttpServletRequest request) {
	if (CorsUtils.isPreFlightRequest(request)) {
		return PRE_FLIGHT_MATCH;
	}
	for (HeaderExpression expression : this.expressions) {
		if (!expression.match(request)) {
			return null;
		}
	}
	return this;
}

RequestMappingInfo

在 RequestCondition 众多实现类中,有一个最为关键,也是我们要重点分析的——RequestMappingInfo,它可以看作是@RequestMapping的封装类。
它聚合了@RequestMapping注解的所有条件对象,内部用七个变量来表示,匹配时必须所有的条件都匹配通过才算,任一条件不匹配都会导致失败。

public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
  // 请求路径条件 path = {"/api"}
  private final PathPatternsRequestCondition pathPatternsCondition;
  // 方法条件 method = {RequestMethod.GET, RequestMethod.POST}
  private final RequestMethodsRequestCondition methodsCondition;
  // 参数条件 params = {"name"}
  private final ParamsRequestCondition paramsCondition;
  // 请求头条件 headers = {"token"}
  private final HeadersRequestCondition headersCondition;
  // 请求头Content-Type条件 consumes = {"application/json"}
  private final ConsumesRequestCondition consumesCondition;
  // 请求头Accept条件 produces = {"text/html"}
  private final ProducesRequestCondition producesCondition;
  // 自定义条件
  private final RequestConditionHolder customConditionHolder;
}

它是如何被构建的呢???
AbstractHandlerMethodMapping 属性填充完毕后,会自动检测容器内所有 ControllerBean,然后解析 Bean 上所有被 @RequestMapping 标注的方法,源码在 RequestMappingHandlerMapping#getMappingForMethod 。

  • 先解析方法上的注解,构建 RequestMappingInfo 实例
  • 再解析类上的注解,构建 RequestMappingInfo 实例
  • 最后将二者合并
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
	// 基于方法构建RequestMappingInfo
	RequestMappingInfo info = createRequestMappingInfo(method);
	if (info != null) {
		// 基于类构建RequestMappingInfo
		RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
		if (typeInfo != null) {
			// 二者合并 比如类上path=/user 方法上path=/get/{id} 匹配的完整路径:/user/get/{id}
			info = typeInfo.combine(info);
		}
		String prefix = getPathPrefix(handlerType);
		if (prefix != null) {
			info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
		}
	}
	return info;
}

RequestMappingInfo 实例化采用建造者模式,通过内部类 DefaultBuilder 完成实例化,没什么复杂的,就是读取注解属性完成实例化:

protected RequestMappingInfo createRequestMappingInfo(
		RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {

	RequestMappingInfo.Builder builder = RequestMappingInfo
			.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
			.methods(requestMapping.method())
			.params(requestMapping.params())
			.headers(requestMapping.headers())
			.consumes(requestMapping.consumes())
			.produces(requestMapping.produces())
			.mappingName(requestMapping.name());
	if (customCondition != null) {
		builder.customCondition(customCondition);
	}
	return builder.options(this.config).build();
}

@RequestMapping注解上的配置,就对应了 RequestMappingInfo 内部的七个变量。
最后,我们看一下 RequestMappingInfo 的匹配过程吧。其实非常简单,就是按顺序依次匹配内部的七个条件对象,全部匹配通过才算通过。

public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
	RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
	if (methods == null) {
		return null;
	}
	ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
	if (params == null) {
		return null;
	}
	HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
	if (headers == null) {
		return null;
	}
	ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
	if (consumes == null) {
		return null;
	}
	ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
	if (produces == null) {
		return null;
	}
	PathPatternsRequestCondition pathPatterns = null;
	if (this.pathPatternsCondition != null) {
		pathPatterns = this.pathPatternsCondition.getMatchingCondition(request);
		if (pathPatterns == null) {
			return null;
		}
	}
	PatternsRequestCondition patterns = null;
	if (this.patternsCondition != null) {
		patterns = this.patternsCondition.getMatchingCondition(request);
		if (patterns == null) {
			return null;
		}
	}
	RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
	if (custom == null) {
		return null;
	}
	return new RequestMappingInfo(this.name, pathPatterns, patterns,
			methods, params, headers, consumes, produces, custom, this.options);
}

尾巴

Spring MVC 把 HTTP 请求路由到 HandlerMethod 是一个极其复杂的过程,要匹配的条件项之多,为此专门抽象出了 RequestCondition 接口,代表请求要匹配的条件。@RequestMapping注解的每一项配置都对应一个 RequestCondition 实现类,每个实现类只负责自己的条件匹配逻辑。
最后,为了聚合这些条件,Spring MVC 还提供了 RequestMappingInfo 子类,内部用七个变量记录注解配置的条件对象,方便一次性完成匹配。有了 RequestMappingInfo 对象,Spring MVC 就能轻松把请求路由到 HandlerMethod。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

知识图谱 vs GPT

简介&#xff1a; 当我们谈论知识图谱时&#xff0c;我们指的是一种结构化的知识表示形式&#xff0c;是一种描述真实世界中事物及其关系的语义模型&#xff0c;用于描述实体之间的关系。它通过将知识组织成图形结构&#xff0c;提供了一种更全面、准确和智能的信息处理方式。知…

【论文阅读笔记】Mip-NeRF 360: Unbounded Anti-Aliased Neural Radiance Fields

目录 概述摘要引言参数化效率歧义性 mip-NeRF场景和光线参数化从粗到细的在线蒸馏基于区间的模型的正则化实现细节实验限制总结&#xff1a;附录退火膨胀采样背景颜色 paper&#xff1a;https://arxiv.org/abs/2111.12077 code&#xff1a;https://github.com/google-research/…

分布式系统架构设计之分布式事务的概述和面临的挑战

在当今大规模应用和服务的背景下&#xff0c;分布式系统的广泛应用已经成为了一种必然的主流趋势。然后&#xff0c;伴随着分布式系统的应用范围的增长&#xff0c;分布式事务处理成为了一个至关重要的关键话题。在传统的单体系统中&#xff0c;事务处理通常相对简单&#xff0…

opencv006 绘制直线、矩形、⚪、椭圆

绘制图形是opencv经常使用的操作之一&#xff0c;库中提供了很多有用的接口&#xff0c;今天来学习一下吧&#xff01; &#xff08;里面的函数和参数还是有点繁琐的&#xff09; 最终结果显示 函数介绍 直线 line(img, pt1, pt2, color, thickness, lineType, shift) img: 在…

django websocket

目录 核心代码 consumers.py from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer import datetime import time from asgiref.sync import async_to_sync class ChatConsumer(WebsocketConsumer):def websocket_conne…

【STM32】STM32学习笔记-编码器接口测速(20)

00. 目录 文章目录 00. 目录01. 预留02. 编码器测速接线图03. 编码器测速程序示例04. 程序下载05. 附录 01. 预留 02. 编码器测速接线图 03. 编码器测速程序示例 Encoder.h #ifndef __ENCODER_H #define __ENCODER_Hvoid Encoder_Init(void); int16_t Encoder_Get(void);#en…

someip中通过event方式通信,为什么实际使用时使用的是eventGroup?

someip是一种面向服务的可伸缩的协议,用于控制消息的汽车中间件的解决方案。someip提供了三种接口类型:Method,Event和Field,分别对应不同的通信机制和场景。 Event是一种主动发送的接口,用于通知客户端服务端的状态变化或者事件发生。Event可以按照一定的规则或者周期发…

IDEA中自动导包及快捷键

导包设置及快捷键 设置&#xff1a;Setting->Editor->General->Auto import快捷键 设置&#xff1a;Setting->Editor->General->Auto import java区域有两个关键选项 Add unambiguous imports on the fly 快速添加明确的导包 IDEA将在我们书写代码的时候…

JS中模块的导入导出

背景 学习js过程中&#xff0c;发现导入导出有的是使用的export 导出&#xff0c;import导入&#xff0c;有的是使用exports或module.exports导出&#xff0c;使用require导入&#xff0c;不清楚使用场景和规则&#xff0c;比较混乱。 经过了解发现&#xff0c;NodeJS 中&…

莫比乌斯函数

积性函数定义 若gcd(p,q)1&#xff0c;有f(p*q)f(p)*f(q)&#xff0c;则f(x)是积性函数 其中规定f(1)1&#xff0c;对于积性函数有&#xff1a;所有的积性函数都可以用筛法求出 常见的积性函数有欧拉函数和莫比乌斯函数 筛法求莫比乌斯函数 const int N 1e9 5; const int …

QT_01 安装、创建项目

QT - 安装、创建项目 1. 概述 1.1 什么是QT Qt 是一个跨平台的 C图形用户界面应用程序框架。 它为应用程序开发者提供建立艺术级图形界面所需的所有功能。 它是完全面向对象的&#xff0c;很容易扩展&#xff0c;并且允许真正的组件编程。 1.2 发展史 1991 年 Qt 最早由奇…

C# A* 算法 和 Dijkstra 算法 结合使用

前一篇&#xff1a;路径搜索算法 A* 算法 和 Dijkstra 算法-CSDN博客文章浏览阅读330次&#xff0c;点赞9次&#xff0c;收藏5次。Dijkstra算法使用优先队列来管理待处理的节点&#xff0c;通过不断选择最短距离的节点进行扩展&#xff0c;更新相邻节点的距离值。Dijkstra算法使…

Hadoop入门学习笔记——八、数据分析综合案例

视频课程地址&#xff1a;https://www.bilibili.com/video/BV1WY4y197g7 课程资料链接&#xff1a;https://pan.baidu.com/s/15KpnWeKpvExpKmOC8xjmtQ?pwd5ay8 Hadoop入门学习笔记&#xff08;汇总&#xff09; 目录 八、数据分析综合案例8.1. 需求分析8.1.1. 背景介绍8.1.2…

Java业务功能并发问题处理

业务场景&#xff1a; 笔者负责的功能需要调用其他系统的进行审批&#xff0c;而接口的调用过程耗时有点长&#xff08;可能长达10秒&#xff09;&#xff0c;一个订单能被多个人提交审批&#xff0c;当订单已提交后会更改为审批中&#xff0c;不能再次审批&#xff08;下游系…

js逆向第11例:猿人学第4题雪碧图、样式干扰

任务4:采集这5页的全部数字,计算加和并提交结果 打开控制台查看请求地址https://match.yuanrenxue.cn/api/match/4,返回的是一段html网页代码 复制出来格式化后,查看具体内容如下: <td><img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAA…

mysql与其他数据库有何区别?

随着信息技术的不断发展&#xff0c;数据库系统在各行各业中得到了广泛的应用。其中&#xff0c;MySQL作为一种流行的关系型数据库管理系统&#xff0c;与其他数据库系统存在一些明显的区别。本文将就MySQL与其他数据库的区别进行深入探讨。 1、更低的成本 MySQL是一个开源的关…

小兔鲜儿 uniapp - 项目打包

目录 微信小程序端​ 核心步骤​ 步骤图示​ 条件编译​ 条件编译语法​ 打包为 H5 端​ 核心步骤​ 路由基础路径​ 打包为 APP 端​ 微信小程序端​ 把当前 uni-app 项目打包成微信小程序端&#xff0c;并发布上线。 核心步骤​ 运行打包命令 pnpm build:mp-weix…

Mybatis系列课程-一对一

目标 学会使用 assocation的select 与column 属性 select:设置分步查询的唯一标识 column:将查询出的某个字段作为分步查询的下一个查询的sql条件 步骤 第一步:修改Student.java 增加 private Grade grade; // 如果之前已经增加过, 跳过这步 第二步:修改StudentMapper.java…

YOLOv8改进 | 2023Neck篇 | 利用Gold-YOLO改进YOLOv8对小目标检测

一、本文介绍 本文给大家带来的改进机制是Gold-YOLO利用其Neck改进v8的Neck,GoLd-YOLO引入了一种新的机制——信息聚集-分发(Gather-and-Distribute, GD)。这个机制通过全局融合不同层次的特征并将融合后的全局信息注入到各个层级中,从而实现更高效的信息交互和融合。这种…

【PX4-AutoPilot教程-TIPS】Ubuntu中安装指定版本的gcc-arm-none-eabi

Ubuntu中安装指定版本的gcc-arm-none-eabi 在 Ubuntu 中开发基于 ARM 架构的 STM32 芯片&#xff0c;需要安装交叉编译器 gcc-arm-none-eabi编译代码&#xff0c;那么什么是交叉编译器呢&#xff1f; Ubuntu 自带的 gcc 编译器是针对 X86 架构的&#xff01;而我们现在要编译…