zoukankan      html  css  js  c++  java
  • Tomcat 路由请求的实现 Mapper

    在分析 Tomcat 实现之前,首先看一下 Servlet 规范是如何规定容器怎么把请求映射到一个 servlet。本文首发于(微信公众号:顿悟源码

    1. 使用 URL 路径

    收到客户端请求后,容器根据请求 URL 的上下文名称匹配 Web 应用程序,然后根据去除上下文路径和路径参数的路径,按以下规则顺序匹配,并且只使用第一个匹配的 Servlet,后续不再尝试匹配:

    1. 精确匹配,查找一个与请求路径完全匹配的 Servlet
    2. 前缀路径匹配,递归的尝试匹配最长路径前缀的 Servlet,通过使用 "/" 作为路径分隔符,在路径树上一次一个目录的匹配,选择路径最长的
    3. 扩展名匹配,如果 URL 最后一部分包含扩展名,如 .jsp,则尝试匹配处理此扩展名请求的 Servlet
    4. 如果前三个规则没有匹配成功,那么容器要为请求提供一个默认 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 路由表

    上图包含了各类的核心成员变量和方法,也直观的体现了类之间的关系。

    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 的一些比较方法。

  • 相关阅读:
    Div在BOdy中居中
    c_lc_填充每个节点的下一个右侧节点指针 I~II(递归)
    c_pat_哈密顿回路 & 最大点集 & 是否是旅行商路径 & 欧拉路径 & 最深的根(邻接矩阵存图)
    c_lc_二叉搜索树的最近公共祖先 & 二叉树的最近公共祖先(利用性质 | 从p,q开始存储每个结点的父亲)
    c_pat_树题大杂烩(利用性质)
    现在的我,理解了这种「激情」
    b_pat_排成最小的数字 & 月饼(字符串拼接比较a+b<b+a)
    c_lc_二叉搜索树中的众数(中序遍历+延迟更新前驱结点)
    b_pat_分享 & 链表排序 & 链表去重(链表模拟)
    b_pat_弹出序列(栈模拟)
  • 原文地址:https://www.cnblogs.com/chuonye/p/10808765.html
Copyright © 2011-2022 走看看