Tomcat源码剖析(四)Tomcat如何找到请求Url对应的Servlet

【分析】

Servlet中,规定的Url组成为:ip:port/context/servlet-path

因此,想要找到对应的Servlet,必须首先解析context,然后再找到对应的servlet-path

同时,servlet协议规定,servlet-path有4中不同的匹配规则:

  • 精确匹配
  • 路径匹配
  • 扩展匹配
  • 默认匹配

因此,需要不同的匹配方式

【Tomcat】

Tomcat底层实现确实是上面那样,不过在实际的实现中,更加复杂。在ServletTomcat配置中,可一个配置多个Host,每个Host可以配置多个Context,每个Context可以配置多个Servlet,因此单单使用一个HashMap是无法满足需求的,因为可能存在Servlet名字相同但是Context不同的情况。

Tomcat中,最终的实现是一个树结构:

Host

|_ Context

|_Context

​ |_Servlet

​ |_Servlet

因此,在Tomcat实现中,会一边查找树节点,一边根据不同的规则匹配。

【细节】

Tomcat的树结构,是通过数组来实现的。比如MappedHost中包含MappedContext[] contexts;MappedContext包含MappedWrapper[] exactWrappers.

对于这些数组,Tomcat在每次插入的时候,都会先排序再插入,每次索引的时候,都会通过二分查找进行匹配。

扩容方式为:每次增加一个元素都会使用System.arraycopy()方法生成一个新的数组。

为什么不用HashMap?这样实现起来更加简单而且性能应该不会太差。

因为HashMap虽然好,但是HashMap只能精确匹配,而这里由于很多情况都需要模糊查找,有些时候只能返回最相近的元素,因此这里没有使用HashMap,而采用了二分查找最相邻的元素。

【Code】

明白了细节的问题后,就可以直接看Tomcat的源代码了。

Tomcat中,实现此功能的是org.apache.catalina.mapper.Mapper

Mapper会作为Service模块的组件。

Service启动的时候,会调用MapperaddHost()添加server.xml中配置的Host,默认只有一个localhost,这样就创建好了一棵树的最顶端。

当有请求到来的时候,Connector会解析Http然后在CoyoteAdapter中,调用

connector.getService().getMapper().map(serverName, decodedURI,
                    version, request.getMappingData());

来查找Servlet

Mapper#map()

    public void map(MessageBytes host, MessageBytes uri, String version,
                    MappingData mappingData) throws IOException {

        if (host.isNull()) {
            String defaultHostName = this.defaultHostName;
            if (defaultHostName == null) {
                return;
            }
            host.getCharChunk().append(defaultHostName);
        }
        host.toChars();
        uri.toChars();
        internalMap(host.getCharChunk(), uri.getCharChunk(), version, mappingData);
    }

可以看到,关键的还是internalMap()方法

Mapper#internalMap()

private final void internalMap(CharChunk host, CharChunk uri,
        String version, MappingData mappingData) throws IOException {

    if (mappingData.host != null) {
        throw new AssertionError();
    }

    //首先获取Host,也就是一棵树的头节点
    MappedHost[] hosts = this.hosts;
    //需要注意的是,URL是不区分大小写的,因此这里的比较都是通过IgnoreCase比较的
    MappedHost mappedHost = exactFindIgnoreCase(hosts, host);
    //如果没有找到匹配的host,则尝试匹配二级域名。比如*.baidu.com
    if (mappedHost == null) {

        int firstDot = host.indexOf('.');
        if (firstDot > -1) {
            int offset = host.getOffset();
            try {
                host.setOffset(firstDot + offset);
                mappedHost = exactFindIgnoreCase(hosts, host);

            } finally {
                // Make absolutely sure this gets reset
                host.setOffset(offset);
            }
        }
        //如果依然没有找到,则使用默认的host
        if (mappedHost == null) {
            mappedHost = defaultHost;
            if (mappedHost == null) {
                return;
            }
        }
    }

    mappingData.host = mappedHost.object;

    if (uri.isNull()) {
        // Can't map context or wrapper without a uri
        return;
    }

    uri.setLimit(-1);

    //匹配Context,Context是在webapp目录下的war或文件夹解压出来对应生成的
    ContextList contextList = mappedHost.contextList;
    MappedContext[] contexts = contextList.contexts;
    //找到最后一个uri大于等于contextName的pos 
    //注意这里不是等于,属于模糊匹配
    int pos = find(contexts, uri);
    if (pos == -1) {
        return;
    }

    int lastSlash = -1;
    int uriEnd = uri.getEnd();
    int length = -1;
    boolean found = false;
    MappedContext context = null;
    while (pos >= 0) {
        context = contexts[pos];
        //首先直接检查url是不是以contextName开始的
        if (uri.startsWith(context.name)) {
            length = context.name.length();
            if (uri.getLength() == length) {
                found = true;
                break;
            } else if (uri.startsWithIgnoreCase("/", length)) {
                found = true;
                break;
            }
        }
        //如果不是,则截取第一个/开始的字符进行匹配
        //比如:ip:port/test/test2
        //则会截取/test去和缓存contextName匹配
        if (lastSlash == -1) {
            lastSlash = nthSlash(uri, contextList.nesting + 1);
        } else {
            lastSlash = lastSlash(uri);
        }
        uri.setEnd(lastSlash);
        pos = find(contexts, uri);
    }
    uri.setEnd(uriEnd);
    //如果依然没有查找成功,则看下是否有允许contextName为空的情况
    if (!found) {
        if (contexts[0].name.equals("")) {
            context = contexts[0];
        } else {
            context = null;
        }
    }
    if (context == null) {
        return;
    }
    //查找成功
    mappingData.contextPath.setString(context.name);

    ContextVersion contextVersion = null;
    ContextVersion[] contextVersions = context.versions;
    final int versionCount = contextVersions.length;
    if (versionCount > 1) {
        Context[] contextObjects = new Context[contextVersions.length];
        for (int i = 0; i < contextObjects.length; i++) {
            contextObjects[i] = contextVersions[i].object;
        }
        mappingData.contexts = contextObjects;
        if (version != null) {
            contextVersion = exactFind(contextVersions, version);
        }
    }
    if (contextVersion == null) {
        // Return the latest version
        // The versions array is known to contain at least one element
        contextVersion = contextVersions[versionCount - 1];
    }
    mappingData.context = contextVersion.object;
    mappingData.contextSlashCount = contextVersion.slashCount;

    //匹配Servlet
    if (!contextVersion.isPaused()) {
        internalMapWrapper(contextVersion, uri, mappingData);
    }

}

可以看到,在Mapper中,会根据启动时server.xml中配置的Host,以及webapps目录下的文件装载Context,然后根据Requesturi一一匹配HostContext,如果匹配成功,则进入internalMapWrapper

这里可以看到,很多时候ContextName是允许为空的,那应该怎么配置才能让ContextName为空呢?

答案是在server.xml中添加Context配置

<Context path="" docBase="F:\tomcat\webapps\study" debug="0">
  </Context>
  

最重要的便是将path设置为空即可,这里path还可以自定义其他名字。

为什么可以这样配置呢?

代码比较多,这里简单说下:Tomcat中,负责扫描Context的模块为HostConfigHostConfig所执行的对象为ContextName

 public ContextName(String name, boolean stripFileExtension) {

        String tmp1 = name;

        // Convert Context names and display names to base names

        // Strip off any leading "/"
        if (tmp1.startsWith("/")) {
            tmp1 = tmp1.substring(1);
        }

        // Replace any remaining /
        tmp1 = tmp1.replaceAll("/", FWD_SLASH_REPLACEMENT);

        // Insert the ROOT name if required
        if (tmp1.startsWith(VERSION_MARKER) || "".equals(tmp1)) {
            tmp1 = ROOT_NAME + tmp1;
        }

        // Remove any file extensions
        if (stripFileExtension &&
                (tmp1.toLowerCase(Locale.ENGLISH).endsWith(".war") ||
                        tmp1.toLowerCase(Locale.ENGLISH).endsWith(".xml"))) {
            tmp1 = tmp1.substring(0, tmp1.length() -4);
        }

        baseName = tmp1;

        String tmp2;
        // Extract version number
        int versionIndex = baseName.indexOf(VERSION_MARKER);
        if (versionIndex > -1) {
            version = baseName.substring(versionIndex + 2);
            tmp2 = baseName.substring(0, versionIndex);
        } else {
            version = "";
            tmp2 = baseName;
        }

        if (ROOT_NAME.equals(tmp2)) {
            path = "";
        } else {
            path = "/" + tmp2.replaceAll(FWD_SLASH_REPLACEMENT, "/");
        }

        if (versionIndex > -1) {
            this.name = path + VERSION_MARKER + version;
        } else {
            this.name = path;
        }
    }

ContextName的构造方法中,path便是通过获取war包名或者文件夹名赋值的,当初始化完成便会加入Hostchild,而如果直接在server.xml中指定了Context元素,则Context会直接读取配置的path作为类似War包的Name初始化,因此想要自己配置ContextName,可以在server.xml中进行配置

Mapper#internalMap()

    /**
     * Wrapper mapping.
     * @throws IOException if the buffers are too small to hold the results of
     *                     the mapping.
     */
    private final void internalMapWrapper(ContextVersion contextVersion,
                                          CharChunk path,
                                          MappingData mappingData) throws IOException {

        int pathOffset = path.getOffset();
        int pathEnd = path.getEnd();
        boolean noServletPath = false;

        int length = contextVersion.path.length();
        if (length == (pathEnd - pathOffset)) {
            noServletPath = true;
        }
        int servletPath = pathOffset + length;
        path.setOffset(servletPath);

        // Rule 1 -- Exact Match
        MappedWrapper[] exactWrappers = contextVersion.exactWrappers;

        internalMapExactWrapper(exactWrappers, path, mappingData);

        // Rule 2 -- Prefix Match
        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);
    }

这里可以看到,代码比较长。但是可以直接从注释和方法名就能看出,这是在解析Servlet模糊匹配规则:

  • 精确匹配:完整的匹配到URL 比如 /myapp/test.jsp

  • 路径匹配:匹配前面大部分URL,后面任意:比如:/myapp/*

  • 扩展名匹配:以扩展名的形式匹配URL,比如:*.jsp

  • 资源文件处理:也就是看看是否有匹配的默认首页文件

    <welcome-file-list>
          <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    
  • 默认匹配:当所有都不满足的时候指定的URL:比如:/

这里可以看出来/是比/*要弱一级的。/的时候,如果存在welcome-file-list,则回去找welcome-file-list,找不到才会匹配/,而/*会比welcome-file-list优先查找