• servlet映射路径匹配解析


    开头

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

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

    
        servlet01
        com.monian.study.servlet.Servlet01
      
      
        servlet01
        /test/servlet/get
      
    
      
        servlet02
        com.monian.study.servlet.Servlet02
      
      
        servlet02
        /test/servlet/*
      
    
      
        servlet03
        com.monian.study.servlet.Servlet03
      
      
        servlet03
        /test/*
      
    
      
        servlet04
        com.monian.study.servlet.Servlet04
      
      
        servlet04
        /
      
      
      
        
        servlet05
        com.monian.study.servlet.Servlet05
      
      
        servlet05
        *.do
      

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

    servlet代码
    public class Servlet01 extends HttpServlet {
    
      @Override
      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("Servlet01");
      }
    }
    
    
    public class Servlet02 extends HttpServlet {
    
      @Override
      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("Servlet02");
      }
    }
    
    public class Servlet03 extends HttpServlet {
    
      @Override
      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("Servlet03");
      }
    }
    
    public class Servlet04 extends HttpServlet {
    
      @Override
      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("Servlet04");
      }
    }
    
    public class Servlet05 extends HttpServlet {
    
      @Override
      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("Servlet05");
      }
    
    }

     

    源码

    org.apache.catalina.mapper.Mapper#internalMapWrapper
    // 在本例子中 path = '/zxq/test/servlet/get',用offset和end来控制路径部分长度
    // contextPath = '/zxq'
    private final void internalMapWrapper(ContextVersion contextVersion,
        CharChunk path,
        MappingData mappingData) throws IOException {
    
      int pathOffset = path.getOffset();
      int pathEnd = path.getEnd();
      boolean noServletPath = false;
      
      // contextVersion.path = '/zxq'
      int length = contextVersion.path.length();
      if (length == (pathEnd - pathOffset)) {
        noServletPath = true;
      }
      int servletPath = pathOffset + length;
      // path = '/text/servlet/get'
      path.setOffset(servletPath);
    
      // 规则1:先开始精确匹配
      MappedWrapper[] exactWrappers = contextVersion.exactWrappers;
      internalMapExactWrapper(exactWrappers, path, mappingData);
    
      // 规则2:前缀匹配,也就是路径匹配
      boolean checkJspWelcomeFiles = false;
      MappedWrapper[] wildcardWrappers = contextVersion.wildcardWrappers;
      if (mappingData.wrapper == null) {
        internalMapWildcardWrapper(wildcardWrappers, contextVersion.nesting,
            path, mappingData);
        if (mappingData.wrapper != null && mappingData.jspWildCard) {
          char[] buf = path.getBuffer();
          if (buf[pathEnd - 1] == '/') {
            /*
             * Path ending in '/' was mapped to JSP servlet based on
             * wildcard match (e.g., as specified in url-pattern of a
             * jsp-property-group.
             * Force the context's welcome files, which are interpreted
             * as JSP files (since they match the url-pattern), to be
             * considered. See Bugzilla 27664.
             */
            mappingData.wrapper = null;
            checkJspWelcomeFiles = true;
          } else {
            // See Bugzilla 27704
            mappingData.wrapperPath.setChars(buf, path.getStart(),
                path.getLength());
            mappingData.pathInfo.recycle();
          }
        }
      }
    
      if(mappingData.wrapper == null && noServletPath &&
          contextVersion.object.getMapperContextRootRedirectEnabled()) {
        // The path is empty, redirect to "/"
        path.append('/');
        pathEnd = path.getEnd();
        mappingData.redirectPath.setChars
            (path.getBuffer(), pathOffset, pathEnd - pathOffset);
        path.setEnd(pathEnd - 1);
        return;
      }
    
      // Rule 3 -- Extension Match
      MappedWrapper[] extensionWrappers = contextVersion.extensionWrappers;
      if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
        internalMapExtensionWrapper(extensionWrappers, path, mappingData,
            true);
      }
    
      // Rule 4 -- Welcome resources processing for servlets
      if (mappingData.wrapper == null) {
        boolean checkWelcomeFiles = checkJspWelcomeFiles;
        if (!checkWelcomeFiles) {
          char[] buf = path.getBuffer();
          checkWelcomeFiles = (buf[pathEnd - 1] == '/');
        }
        if (checkWelcomeFiles) {
          for (int i = 0; (i < contextVersion.welcomeResources.length)
              && (mappingData.wrapper == null); i++) {
            path.setOffset(pathOffset);
            path.setEnd(pathEnd);
            path.append(contextVersion.welcomeResources[i], 0,
                contextVersion.welcomeResources[i].length());
            path.setOffset(servletPath);
    
            // Rule 4a -- Welcome resources processing for exact macth
            internalMapExactWrapper(exactWrappers, path, mappingData);
    
            // Rule 4b -- Welcome resources processing for prefix match
            if (mappingData.wrapper == null) {
              internalMapWildcardWrapper
                  (wildcardWrappers, contextVersion.nesting,
                      path, mappingData);
            }
    
            // Rule 4c -- Welcome resources processing
            //            for physical folder
            if (mappingData.wrapper == null
                && contextVersion.resources != null) {
              String pathStr = path.toString();
              WebResource file =
                  contextVersion.resources.getResource(pathStr);
              if (file != null && file.isFile()) {
                internalMapExtensionWrapper(extensionWrappers, path,
                    mappingData, true);
                if (mappingData.wrapper == null
                    && contextVersion.defaultWrapper != null) {
                  mappingData.wrapper =
                      contextVersion.defaultWrapper.object;
                  mappingData.requestPath.setChars
                      (path.getBuffer(), path.getStart(),
                          path.getLength());
                  mappingData.wrapperPath.setChars
                      (path.getBuffer(), path.getStart(),
                          path.getLength());
                  mappingData.requestPath.setString(pathStr);
                  mappingData.wrapperPath.setString(pathStr);
                }
              }
            }
          }
    
          path.setOffset(servletPath);
          path.setEnd(pathEnd);
        }
    
      }
    
      /* welcome file processing - take 2
       * Now that we have looked for welcome files with a physical
       * backing, now look for an extension mapping listed
       * but may not have a physical backing to it. This is for
       * the case of index.jsf, index.do, etc.
       * A watered down version of rule 4
       */
      if (mappingData.wrapper == null) {
        boolean checkWelcomeFiles = checkJspWelcomeFiles;
        if (!checkWelcomeFiles) {
          char[] buf = path.getBuffer();
          checkWelcomeFiles = (buf[pathEnd - 1] == '/');
        }
        if (checkWelcomeFiles) {
          for (int i = 0; (i < contextVersion.welcomeResources.length)
              && (mappingData.wrapper == null); i++) {
            path.setOffset(pathOffset);
            path.setEnd(pathEnd);
            path.append(contextVersion.welcomeResources[i], 0,
                contextVersion.welcomeResources[i].length());
            path.setOffset(servletPath);
            internalMapExtensionWrapper(extensionWrappers, path,
                mappingData, false);
          }
    
          path.setOffset(servletPath);
          path.setEnd(pathEnd);
        }
      }
    
    
      // Rule 7 -- Default servlet
      if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
        if (contextVersion.defaultWrapper != null) {
          mappingData.wrapper = contextVersion.defaultWrapper.object;
          mappingData.requestPath.setChars
              (path.getBuffer(), path.getStart(), path.getLength());
          mappingData.wrapperPath.setChars
              (path.getBuffer(), path.getStart(), path.getLength());
          mappingData.matchType = MappingMatch.DEFAULT;
        }
        // Redirection to a folder
        char[] buf = path.getBuffer();
        if (contextVersion.resources != null && buf[pathEnd -1 ] != '/') {
          String pathStr = path.toString();
          // Note: Check redirect first to save unnecessary getResource()
          //       call. See BZ 62968.
          if (contextVersion.object.getMapperDirectoryRedirectEnabled()) {
            WebResource file;
            // Handle context root
            if (pathStr.length() == 0) {
              file = contextVersion.resources.getResource("/");
            } else {
              file = contextVersion.resources.getResource(pathStr);
            }
            if (file != null && file.isDirectory()) {
              // Note: this mutates the path: do not do any processing
              // after this (since we set the redirectPath, there
              // shouldn't be any)
              path.setOffset(pathOffset);
              path.append('/');
              mappingData.redirectPath.setChars
                  (path.getBuffer(), path.getStart(), path.getLength());
            } else {
              mappingData.requestPath.setString(pathStr);
              mappingData.wrapperPath.setString(pathStr);
            }
          } else {
            mappingData.requestPath.setString(pathStr);
            mappingData.wrapperPath.setString(pathStr);
          }
        }
      }
    
      path.setOffset(pathOffset);
      path.setEnd(pathEnd);
    }

     

    匹配路径代码

    org.apache.catalina.mapper.Mapper#find(org.apache.catalina.mapper.Mapper.MapElement[], org.apache.tomcat.util.buf.CharChunk, int, int)
    // 从map找到一个最与路径匹配的
    private static final  int find(MapElement[] map, CharChunk name,
        int start, int end) {
    
      int a = 0;
      int b = map.length - 1;
    
      // Special cases: -1 and 0
      if (b == -1) {
        return -1;
      }
    
      
      // -1表示完全不匹配,直接返回 
      if (compare(name, start, end, map[0].name) < 0 ) {
        return -1;
      }
      // 完全匹配或部分匹配,且只有一个待匹配的servlet直接返回
      if (b == 0) {
        return 0;
      }
    
      // 类似于二分查找,找到一个最长路径匹配 
      int i = 0;
      while (true) {
        i = (b + a) >>> 1;
        int result = compare(name, start, end, map[i].name);
        if (result == 1) {
          a = i;
        } else if (result == 0) {
          return i;
        } else {
          b = i;
        }
        if ((b - a) == 1) {
          int result2 = compare(name, start, end, map[b].name);
          if (result2 < 0) {
            return a;
          } else {
            return b;
          }
        }
      }
    
    }
    
    
    
    private static final int compare(CharChunk name, int start, int end,
        String compareTo) {
      int result = 0;
      char[] c = name.getBuffer();
      int len = compareTo.length();
      if ((end - start) < len) {
        len = end - start;
      }
      // 比较url-pattern与 请求路径path,若有一个字符不相等退出循环 
      for (int i = 0; (i < len) && (result == 0); i++) {
        if (c[i + start] > compareTo.charAt(i)) {
          result = 1;
        } else if (c[i + start] < compareTo.charAt(i)) {
          result = -1;
        }
      }
        
      // 都相等的话再比较长度,请求路径长度比待匹配部分长
      if (result == 0) {
        if (compareTo.length() > (end - start)) {
          result = -1;
        } else if (compareTo.length() < (end - start)) {
          result = 1;
        }
      }
      // result=0代表完全匹配, result=-1代表不匹配,result=1代表开头部分匹配 
      return result;
    }

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

     

    精确匹配

    可以看到符合精确匹配的只有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 extends MapElement> 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是精确匹配的

     

    通配符匹配 (路径匹配)

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

    /**
     * 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是匹配的,输出

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

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

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

     

    扩展名匹配(后缀匹配)

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

     

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

     

    /**
     * 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);
        }
      }
    }

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

     

    首页welcome资源匹配  

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

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

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

     

    默认匹配

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

     

  • 相关阅读:
    14:00面试,14:06就出来了,问的问题有点变态。。。
    iView复合型输入框动态生成表达式
    笔试强训第21天--(洗牌+MP3的光标)
    Vue获取methods中方法的return返回值
    Vue开发中常用的 js处理数组的方法
    项目延期常见的三个原因及对应的解决方法
    如何在 Linux 中检查我的网卡速度?
    CAS:474922-26-4,DSPE-PEG-NH2,DSPE-PEG-amine,磷脂-聚乙二醇-氨基供应
    通过海康私有协议Ehome/ISUP协议将海康摄像头、录像机等设备统一接入到LiveNVR Web流媒体平台实现统一汇聚及Web播放等的配置说明,
    CSS 盒子模型
  • 原文地址:https://www.cnblogs.com/monianxd/p/16573389.html