在分析 Tomcat 实现以前,首先看一下 Servlet 规范是如何规定容器怎么把请求映射到一个 servlet。本文首发于(微信公众号:顿悟源码)html
收到客户端请求后,容器根据请求 URL 的上下文名称匹配 Web 应用程序,而后根据去除上下文路径和路径参数的路径,按如下规则顺序匹配,而且只使用第一个匹配的 Servlet,后续再也不尝试匹配:java
容器在匹配时区分大小写。web
在 web.xml 部署描述符中,使用如下语法定义映射:数组
假设有如下映射配置:微信
/foo/bar/* servlet1 /baz/* servlet2 /catalog servlet3 *.bop servlet4
那么如下请求路径的匹配状况是:app
/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,是由于不是彻底匹配。webapp
实现请求映射的通常方法是,首先构建一个路由表,而后按照规范进行匹配,最后返回匹配结果。Tomcat 就是如此,与请求映射相关的类有三个,分别是:jsp
这里使用的源码版本是 6.0.53,此版本 MapperListener 是经过 JMX 查询 Host、Context、Wrapper,而后加入到 Mapper 的路由表中。而在高版本,如7和8中,则使用的是 containerEvent 和 lifecycleEvent 容器和生命周期事件进行构建。post
Mapper 内部设计了路由表的组成结构,相关的类图以下:url
上图包含了各种的核心成员变量和方法,也直观的体现了类之间的关系。
Mapper 在构建路由时,addHost 和 addContext 比较简单,都是对数组的操做,这里着重对 addWrapper 的源码进行分析。
从类图中可看出 Context 内部有四种 Wrapper,对应着处理不一样映射规则的 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 映射实例的配置,在内存中,存储状况以下:
触发映射请求的动做是 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
在返回的 MappingData 结果中,有几个 path 须要注意一下,它们分别在如下位置:
|-- Context Path --|-- Servlet Path -|--Path Info--| http://localhost:8080 /webapp /helloServlet /hello |-------- Request URI ----------------------------|
看源码时,发现 Tomcat 写了大量的代码,那是由于,它为了减小内存拷贝,设计了一个 CharChunk,在一个 char[] 数组视图上,实现了相似 String 的一些比较方法。