Tomcat 路由请求的实现 Mapper
在分析 Tomcat 实现之前,首先看一下 Servlet 规范是如何规定容器怎么把请求映射到一个 servlet。本文首发于(微信公众号:顿悟源码)
1. 使用 URL 路径
收到客户端请求后,容器根据请求 URL 的上下文名称匹配 Web 应用程序,然后根据去除上下文路径和路径参数的路径,按以下规则顺序匹配,并且只使用第一个匹配的 Servlet,后续不再尝试匹配:
- 精确匹配,查找一个与请求路径完全匹配的 Servlet
- 前缀路径匹配,递归的尝试匹配最长路径前缀的 Servlet,通过使用 "/" 作为路径分隔符,在路径树上一次一个目录的匹配,选择路径最长的
- 扩展名匹配,如果 URL 最后一部分包含扩展名,如 .jsp,则尝试匹配处理此扩展名请求的 Servlet
- 如果前三个规则没有匹配成功,那么容器要为请求提供一个默认 Servlet
容器在匹配时区分大小写。
2. 映射规范
在 web.xml 部署描述符中,使用以下语法定义映射:
- 以 '/' 字符开始、以 '/*' 后缀结尾的字符串使用路径匹配
- 以 '*.' 为前缀的字符串使用扩展名匹配
- 空字符串("")是一个特殊的 URL 模式,其精确映射到应用的上下文根,即,http://host:port// 请求形式。在这种情况下,路径信息是 '/' 且 servlet 路径和上下文路径是空字符串("")
- 只包含 '/' 的字符串表示应用的默认 Servlet,在这种情况下,servlet 路径是请求 URL 减去上下文路径且路径信息是 null
- 所以其他字符串仅使用精确,完全匹配
3. 映射示例
假设有以下映射配置:
/foo/bar/* servlet1
/baz/* servlet2
/catalog servlet3
*.bop servlet4
那么以下请求路径的匹配情况是:
/foo/bar/index.html servlet1
/foo/bar/index.bop servlet1
/baz servlet2
/baz/index.html servlet2
/catalog servlet3
/catalog/index.html default servlet
/catalog/racecar.bop servlet4
/index.bop servlet4
注意,在 /catalog/index.html 和 /catalog/racecar.bop 的情况下,不使用映射到 /catalog 的 servlet,是因为不是完全匹配。
4. 实现
实现请求映射的一般方法是,首先构建一个路由表,然后按照规范进行匹配,最后返回匹配结果。Tomcat 就是如此,与请求映射相关的类有三个,分别是:
- Mapper: 存储请求路由表并执行匹配
- MapperListener: 查询所有的 Host、Context、Wrapper 构建路由表
- MappingData: 请求映射结果
4.1 构建路由表
这里使用的源码版本是 6.0.53,此版本 MapperListener 是通过 JMX 查询 Host、Context、Wrapper,然后加入到 Mapper 的路由表中。而在高版本,如7和8中,则使用的是 containerEvent 和 lifecycleEvent 容器和生命周期事件进行构建。
Mapper 内部设计了路由表的组成结构,相关的类图如下:
上图包含了各类的核心成员变量和方法,也直观的体现了类之间的关系。
Mapper 在构建路由时,addHost 和 addContext 比较简单,都是对数组的操作,这里着重对 addWrapper 的源码进行分析。
从类图中可看出 Context 内部有四种 Wrapper,对应着处理不同映射规则的 Servlet,分别是:
- exactWrappers: 处理精确,完全匹配的 Wrapper 数组
- wildcardWrappers: 处理模糊匹配的 Wrapper 数组,即以 '/*' 结尾的路径匹配
- extensionWrappers: 处理扩展名匹配的 Wrapper 数组,即以 '*.' 为前缀的路径
- defaultWrapper: 默认 Servlet,即只包含 '/' 的路径
addWrapper 就是以这种规则,根据请求 path 按条件将 Wrapper 插入对应的数组中,核心源码如下:
protected void addWrapper(Context context, String path, Object wrapper,
boolean jspWildCard) {
synchronized (context) {
Wrapper newWrapper = new Wrapper();
newWrapper.object = wrapper; // StandardWrapper 对象
newWrapper.jspWildCard = jspWildCard; // 是否是 JspServlet
if (path.endsWith("/*")) {
// Wildcard wrapper 模糊匹配,最长前缀路径匹配
// 存储名称时去除 /* 字符
newWrapper.name = path.substring(0, path.length() - 2);
... // 插入到 context 处理模糊匹配的 Wrapper 数组中
context.wildcardWrappers = newWrappers;
} else if (path.startsWith("*.")) {
// Extension wrapper 扩展名匹配
newWrapper.name = path.substring(2); // 存储名称时去除 *. 字符
... // 插入到 context 处理扩展名匹配的 Wrapper 数组中
context.extensionWrappers = newWrappers;
} else if (path.equals("/")) {
// Default wrapper 默认 Servlet
newWrapper.name = ""; // 名称为空字符串
context.defaultWrapper = newWrapper;
} else {
// Exact wrapper 完全匹配
newWrapper.name = path;
... // 插入到 context 处理完全匹配的 Wrapper 数组中
context.exactWrappers = newWrappers;
}
}
}
上文的 Servlet 映射实例的配置,在内存中,存储情况如下:
- exactWrappers[]: servlet3(/catalog)
- wildcardWrappers[]: servlet1(/foo/bar); servlet2(/baz)
- extensionWrappers[]: servlet4(bop)
4.2 执行映射
触发映射请求的动作是 CoyoteAdapter 的 postParseRequest() 方法,最终由 Mapper 内部的 internalMap 和 internalMapWrapper 两个方法完成。
internalMap 根据 name 字符串匹配 Host 和 Context,其中 Host 不区分大小写,Context 区分。internalMapWrapper 实现的就是 Servlet 规范描述的 URL 匹配规则。
有一点需要注意,在遍历数组查找 Host、Context、Wrapper 时,使用的是二分查找,比较的是字符串,在返回结果时,返回的是与参数尽可能接近或相等的元素下标,其中的一个 find 源码如下:
private static final int find(MapElement[] map, String name) {
int a = 0;
int b = map.length - 1;
// 如果数组为空
if (b == -1) {
return -1;
} // 或者小于数组的第一个元素,那么返回 -1 表示没找到
if (name.compareTo(map[0].name) < 0) {
return -1;
} // 或者大于数组的第一个元素,且数组长度为 1,返回下标 0
if (b == 0) {
return 0;
}
// 二分查找等于或长度最接近 name 的数组元素下标
int i = 0;
while (true) {
i = (b + a) / 2; // 中间元素下标
int result = name.compareTo(map[i].name);
if (result > 0) { // 大于 map[i]
a = i; // 从中间往后开始查找
} else if (result == 0) {
return i; // 等于,直接返回 i
} else { // 小于,从中间往前开始查找
b = i;
}
if ((b - a) == 1) {// 如果下次比较的元素就剩两个
int result2 = name.compareTo(map[b].name);
if (result2 < 0) {
return a; // 小于返回下标 a
} else {
return b; // 大于等于返回下标 b
}
}
}
}
以上文映射实例的配置为例,分析 /foo/bar/index.html 映射 Servlet 的源码实现,注意这里使用的路径,要去除上下文路径和路径参数。
首先尝试完全匹配:
// Rule 1 -- Exact Match
Wrapper[] exactWrappers = context.exactWrappers;
// 获取处理完全匹配的 Wrapper 数组,这里是 [servlet3(/catalog)]
internalMapExactWrapper(exactWrappers, path, mappingData);
private final void internalMapExactWrapper(...) {
int pos = find(wrappers, path); // 查找 path 长度最相近或相等的 wrapper
if ((pos != -1) && (path.equals(wrappers[pos].name))) {
// 如果匹配成功,设置匹配数据,直接返回,后续不再匹配
mappingData.requestPath.setString(wrappers[pos].name);
mappingData.wrapperPath.setString(wrappers[pos].name);
mappingData.wrapper = wrappers[pos].object;
}
}
如果完全匹配失败,然后尝试最长路径的模糊匹配,核心代码如下:
// Rule 2 -- Prefix Match
boolean checkJspWelcomeFiles = false;
// 获取处理路径匹配的 Wrapper 数组,这里是 [servlet1(/foo/bar),servlet2(/baz)]
Wrapper[] wildcardWrappers = context.wildcardWrappers;
// 确保完全匹配失败
if (mappingData.wrapper == null) {
internalMapWildcardWrapper(wildcardWrappers, path,...);
}
private final void internalMapWildcardWrapper(...) {
...
int pos = find(wrappers, path);
boolean found = false;
while (pos >= 0) {
// 如果以 path 以 /foo/bar 开头
if (path.startsWith(wrappers[pos].name)) {
length = wrappers[pos].name.length();
if (path.getLength() == length) {
// 长度正好相等,则匹配成功
found = true;
break;
} else if (path.startsWithIgnoreCase("/", length)) {
// 或者跳过这个开头并且以 "/" 开始,也匹配成功
found = true;
break;
}
}
}
// 这里的 path 是 /foo/bar/index.html,符合第二个 if
if (found) {
mappingData.wrapperPath.setString
mappingData.pathInfo.setChars
...
}
}
此时已经成功匹配到 Servlet,后续的匹配将不会不执行。简单对后面的匹配进行分析,扩展名匹配比较简单,首先会从 path 中找到扩展名的值,然后在 extensionWrappers 数组中查找即可;如果前面都没匹配成功,那么就返回默认的 Wrapper
5. 小结
在返回的 MappingData 结果中,有几个 path 需要注意一下,它们分别在以下位置:
|-- Context Path --|-- Servlet Path -|--Path Info--|
http://localhost:8080 /webapp /helloServlet /hello
|-------- Request URI ----------------------------|
看源码时,发现 Tomcat 写了大量的代码,那是因为,它为了减少内存拷贝,设计了一个 CharChunk,在一个 char[] 数组视图上,实现了类似 String 的一些比较方法。
Tomcat 路由请求的实现 Mapper的更多相关文章
- 如何安装部署和优化Tomcat?(Tomcat部署和优化与压测,虚拟主机配置,Tomcat处理请求的过程)
文章目录 前言 一:Tomcat安装部署 1.1:Tomcat简介 1.2:Tomcat核心组件 1.3:Tomcat处理请求的过程 1.3.1:请求过程基本解释 1.3.2:请求过程详细解释 1.4 ...
- tomcat发请求,查看各个环节的耗时时间
从一台机器给另一台机器tomcat发请求,查看各个环节的耗时时间 - 业精于勤,荒于嬉:行成于思,毁于随. - CSDN博客https://blog.csdn.net/YAOQINGGG/articl ...
- vue路由请求 router
创建一个Router.js文件 // 路由请求//声明一个常量设置路菜单// import Vue from "vue/types/index";import Vue from ' ...
- JMeter tomcat测试请求
JMeter tomcat测试请求 Apache Jmeter是开源的压力测试工具,可以测试tomcat 的吞吐量等信息 下载地址: http://jmeter.apache.org/download ...
- ASP.NET WebApi 学习与实践系列(2)---WebApi 路由请求的理解
目录 写在前面 WebApi Get 请求 1.无参数的请求 2.一个参数的请求 3.多个参数的请求 4.实体参数的请求 WebApi Post 请求 1.键值对请求 2.dynamic 动态类型请求 ...
- Tomcat处理请求流程
Connector组件的Acceptor监听客户端套接字连接并接收Socket. 将连接交给线程池Executor处理,开始执行请求响应任务. Processor组件读取消息报文,解析请求行.请求体. ...
- 修改Tomcat响应请求时返回的Server内容
HTTP Server在响应请求时,会返回服务器的Server信息,比如 Tomcat 7 的Header是: 这东西其实会给一些别有用心之人带来一定的提示作用:为安全起见,我们一般会建议去掉或修改这 ...
- tomcat对请求路径的匹配过程(原创)
1.匹配服务 如果有两个应用,一个应用只能通过80端口访问,另一个应用只能通过8080端口访问,这种情况下,可以分开两个服务,然后分别创建80端口和8080端口的连接器. 2.匹配主机 一个服务下配置 ...
- How Tomcat works — 六、tomcat处理请求
tomcat已经启动完成了,那么是怎么处理请求的呢?怎么到了我们所写的servlet的呢? 目录 Http11ConnectionHandler Http11Processor CoyoteAdapt ...
随机推荐
- MySQL Other--mysql_config_editor学习使用
mysql_config_editor工具 为避免MySQL明文密码出现在脚本或命令中,从MySQL5.6开始提供了mysql_config_editor工具,可以将数据库连接信息进行加密并保存到用户 ...
- 【前端_js】javascript中数组的map()方法
数组的map()方法用于遍历数组,每遍历一个元素就调用回调方法一次,并将回调函数的返回结果作为新数组的元素,被遍历的数组不会被改变. 语法:let newAarray = arr.map(functi ...
- atlas笔记
目录 环境 Mysql+Atlas配置 atlas:mysql-proxy扩展,mysql中间件,可以实现分表.分库(sharding版本).读写分离.数据库连接池等功能! Atlas类似于Twemp ...
- 随笔记录--RegExp类型
阅读Javascript高级程序设计第五章 -- RegExp类型总结 对于基础教程部分, 有小伙伴不熟悉的,可以参考 正则表达式 - 教程 1. 基础部分回顾: ECMASript通过RegExp类 ...
- 性能测试基础---ant集成1
·Jmeter的命令行与ant等的集成.·为什么需要使用Jmeter的命令行模式(Non-GUI).·为了更好的利用负载机的资源.GUI模式会消耗更多的系统资源.·为了更好的掌握jmeter和其它工具 ...
- 谷歌学术出现We're sorry解决办法
出现这个的原因应该是同ip段的或者就是这个ip曾经是个google的黑名单ip,因为恶意爬取谷歌学术了.解决办法就是申请Hurricane Electric Free IPv6 Tunnel Brok ...
- Nginx+Tomcat实现动静分离和负载均衡
一.什么是动静分离? Nginx动静分离简单来说就是把动态和静态请求分开,不能理解成只是单纯的把动态页面和静态页面物理分离.严格意义上说应该是将动态请求和静态请求分开,可以理解成使用Nginx处理静态 ...
- centos7删除PHP怎么操作
前面我们说了centos7删除MariaDB,现在我们说说centos7删除PHP怎么操作?当然不是特殊需要,不要去删除PHP,后果很严重.操作之前请做好所有的备份!首先查看有没安装php以及版本 # ...
- Maven 中 dependencyManagement 标签使用
1.在Maven中dependencyManagement的作用其实相当于一个对所依赖jar包进行版本管理的管理器. 2.pom.xml文件中,jar的版本判断的两种途径 1:如果dependenci ...
- mysql小结(了解)
Mysql总结 1.数据库的概念 """ 数据库:库(文件夹).表(表结构文件.表数据文件(索引结构)).字段(数据的描述).记录(数据的本体) 分类:效率问题(内存大于 ...