开头

servlet是javaweb用来处理请求和响应的重要对象,本文将从源码的角度分析tomcat内部是如何根据请求路径匹配得到处理请求的servlet的

假设有一个request请求路径为/text/servlet/get,并且在web.xml中配置了4个servlet,代码如下,那么该请求调用的是哪一个servlet呢?

<servlet>
    <servlet-name>servlet01</servlet-name>
    <servlet-class>com.monian.study.servlet.Servlet01</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>servlet01</servlet-name>
    <url-pattern>/test/servlet/get</url-pattern>
  </servlet-mapping>

  <servlet>
    <servlet-name>servlet02</servlet-name>
    <servlet-class>com.monian.study.servlet.Servlet02</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>servlet02</servlet-name>
    <url-pattern>/test/servlet/*</url-pattern>
  </servlet-mapping>

  <servlet>
    <servlet-name>servlet03</servlet-name>
    <servlet-class>com.monian.study.servlet.Servlet03</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>servlet03</servlet-name>
    <url-pattern>/test/*</url-pattern>
  </servlet-mapping>

  <servlet>
    <servlet-name>servlet04</servlet-name>
    <servlet-class>com.monian.study.servlet.Servlet04</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>servlet04</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>


    <servlet>
    <servlet-name>servlet05</servlet-name>
    <servlet-class>com.monian.study.servlet.Servlet05</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>servlet05</servlet-name>
    <url-pattern>*.do</url-pattern>
  </servlet-mapping>

相应各个servlet的代码,代码很简单,调用哪一个servlet就输出哪个servlet的名称:

源码

匹配路径代码

针对上述的匹配举个例子,假设有两个servlet都是通配符匹配的,url-pattern为 /test/one/* 和/test/* ,tomcat解析的时候会去掉通配符再排序['/test', 'test/one'],之后再去匹配数据中的元素也就是map[i].name,匹配路径 '/test/one/two'会返回url-parttern=/test/one/* 的这个servlet,这就是最长路径匹配

精确匹配

servlet映射路径匹配解析-LMLPHP

可以看到符合精确匹配的只有servlet01,且name就是它配置的url-pattern值,然后与requestPath进行匹配 

private final void internalMapExactWrapper
    (MappedWrapper[] wrappers, CharChunk path, MappingData mappingData) {
  // 找到一个与path精确匹配的wrapper
  MappedWrapper wrapper = exactFind(wrappers, path);
  if (wrapper != null) {
    mappingData.requestPath.setString(wrapper.name);
    mappingData.wrapper = wrapper.object;
    if (path.equals("/")) {
      // Special handling for Context Root mapped servlet
      mappingData.pathInfo.setString("/");
      mappingData.wrapperPath.setString("");
      // This seems wrong but it is what the spec says...
      mappingData.contextPath.setString("");
      mappingData.matchType = MappingMatch.CONTEXT_ROOT;
    } else {
      mappingData.wrapperPath.setString(wrapper.name);
      mappingData.matchType = MappingMatch.EXACT;
    }
  }
}


private static final <T, E extends MapElement<T>> E exactFind(E[] map,
    CharChunk name) {
  // find方法会返回部分匹配或完全匹配的map
  int pos = find(map, name);
  if (pos >= 0) {
    E result = map[pos];
    // 完全匹配
    if (name.equals(result.name)) {
      return result;
    }
  }
  return null;
}

显而易见的开头那个request与servlet01的url-pattern是精确匹配的

servlet映射路径匹配解析-LMLPHP

通配符匹配 (路径匹配)

接下来web.xml去掉servlet01的配置,只剩下4个servlet,从前面来看,精确匹配肯定是失败的因为现在去掉servlet01已经没有符合要求的servlet去精确匹配了,只能进行路径匹配了,而路径匹配符合要求的有两个servlet

servlet映射路径匹配解析-LMLPHP

/**
 * Wildcard mapping.
 */
private final void internalMapWildcardWrapper
(MappedWrapper[] wrappers, int nesting, CharChunk path,
    MappingData mappingData) {

  int pathEnd = path.getEnd();

  int lastSlash = -1;
  int length = -1;
  // 找一个最匹配path路径的,根据上面的匹配代码可以得到servlet02
  int pos = find(wrappers, path);
  if (pos != -1) {
    boolean found = false;
    while (pos >= 0) {
      if (path.startsWith(wrappers[pos].name)) {
        length = wrappers[pos].name.length();
        if (path.getLength() == length) {
          found = true;
          break;
        // path不以/开头,则重新找
        } else if (path.startsWithIgnoreCase("/", length)) {
          found = true;
          break;
        }
      }
      // 获取path最后一个/ 所在的位置
      if (lastSlash == -1) {
        lastSlash = nthSlash(path, nesting + 1);
      } else {
        lastSlash = lastSlash(path);
      }
      path.setEnd(lastSlash);
      pos = find(wrappers, path);
    }
    path.setEnd(pathEnd);
    if (found) {
      mappingData.wrapperPath.setString(wrappers[pos].name);
      if (path.getLength() > length) {
        mappingData.pathInfo.setChars
            (path.getBuffer(),
                path.getOffset() + length,
                path.getLength() - length);
      }
      mappingData.requestPath.setChars
          (path.getBuffer(), path.getOffset(), path.getLength());
      mappingData.wrapper = wrappers[pos].object;
      mappingData.jspWildCard = wrappers[pos].jspWildCard;
      mappingData.matchType = MappingMatch.PATH;
    }
  }
}

因此servlet02是匹配的,输出

servlet映射路径匹配解析-LMLPHP

若再web.xml去掉servlet02,那么匹配的就是servlet03了

另外我们可以从上面的代码得到若请求路径path = '/test/servlet/get', 则 '/*' 、 '/test/*' 、 '/test/servlet/*' 、 '/test/servlet/get/*' 与之匹配,'/test/serv/*' 这种不匹配 

路径匹配是能匹配请求路径以 .jsp 、.html结尾的request的

servlet映射路径匹配解析-LMLPHP

扩展名匹配(后缀匹配)

web.xml中注释servlet02和servlet03后,再次访问.jsp后缀结尾的请求就会直接报404了,可以看后续的匹配逻辑虽然能匹配到处理.jsp的servlet但我们并没有在相应路径下配置jsp文件,那么自然报404错误了 

servlet映射路径匹配解析-LMLPHP

下图可以看到后缀匹配的servlet有三个,一个我们自定义的后缀为do,另外两个jsp和jspx是tomcat内置的默认处理jsp的servlet

servlet映射路径匹配解析-LMLPHP

/**
 * Extension mappings.
 *
 * @param wrappers          Set of wrappers to check for matches
 * @param path              Path to map
 * @param mappingData       Mapping data for result
 * @param resourceExpected  Is this mapping expecting to find a resource
 */
private final void internalMapExtensionWrapper(MappedWrapper[] wrappers,
    CharChunk path, MappingData mappingData, boolean resourceExpected) {
  char[] buf = path.getBuffer();
  int pathEnd = path.getEnd();
  int servletPath = path.getOffset();
  int slash = -1;
  for (int i = pathEnd - 1; i >= servletPath; i--) {
    if (buf[i] == '/') {
      slash = i;
      break;
    }
  }
  if (slash >= 0) {
    int period = -1;
    for (int i = pathEnd - 1; i > slash; i--) {
      if (buf[i] == '.') {
        period = i;
        break;
      }
    }
    if (period >= 0) {
      // 截取到后缀的字符位置 匹配
      path.setOffset(period + 1);
      path.setEnd(pathEnd);
      MappedWrapper wrapper = exactFind(wrappers, path);
      if (wrapper != null
          && (resourceExpected || !wrapper.resourceOnly)) {
        mappingData.wrapperPath.setChars(buf, servletPath, pathEnd
            - servletPath);
        mappingData.requestPath.setChars(buf, servletPath, pathEnd
            - servletPath);
        mappingData.wrapper = wrapper.object;
        mappingData.matchType = MappingMatch.EXTENSION;
      }
      path.setOffset(servletPath);
      path.setEnd(pathEnd);
    }
  }
}

servlet映射路径匹配解析-LMLPHP

根据find的匹配逻辑可以匹配到我们自定义的servlet05,输出 

servlet映射路径匹配解析-LMLPHP

首页welcome资源匹配  

若上述匹配都失败了则尝试寻找默认的资源文件,默认有三个,也可以自定义配置 

servlet映射路径匹配解析-LMLPHP

假设请求路径为http://localhost:8082/zxq/ 以'/'结尾,那么会尝试将文件名加到path后面,以index.jsp为例,加完后路径为'/zxq/index.jsp',之后会以此新路径再去尝试精确匹配、路径匹配、物理文件查找再进行扩展名匹配顺序查找,直到找到能处理此path的servlet 

在webapp目录下加一个index.jsp文件之后能成功访问到,执行此请求的就是tomcat默认的jsp servlet

servlet映射路径匹配解析-LMLPHP

servlet映射路径匹配解析-LMLPHP

默认匹配

<url-pattern>/</url-pattern> '/'就是默认匹配,当上述匹配都失败的时候,则启用这个servlet,也就是本文中的servlet04

servlet映射路径匹配解析-LMLPHP

08-10 18:42