Tomcat的Catalina篇5-Web请求处理(请求映射)

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

介绍完Catalina中Web应用的加载过程,本节再来看一下它是如何处理Web应用请求的。

1. 总体过程

Tomcat通过org.apache.tomcat.util.htp.mapper.Mapper维护请求链接与Host、Context、Wrapper等Container的映射。同时,通过org.apache.catalina.connector.MapperListener监听器监听所有的Host、Context、Wrapper组件,在相关组件启动、停止时注册或者移除相关映射

此外,通过org.apache.catalina.connector.CoyoteAdapter将Connector与Mapper、Container联系 起来。当Connector接收到请求后,首先读取请求数据,然后调用CoyoteAdapter.service()方法完成请求处理。请求是如何到CoyoteAdapter.service()这里,我们在后面Coyote篇详细讲解。如下:

// CoyoteAdapte处理请求
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
        throws Exception {

    Request request = (Request) req.getNote(ADAPTER_NOTES);
    Response response = (Response) res.getNote(ADAPTER_NOTES);

    if (request == null) {
        // Create objects
        // 1. 根据Connector的请求(org,apache.coyote.Request)和响应(org.apache.coyote.Response)
        //对象创建Servlet请求(org.apache.catalina.connector.Request)和响应(org.apache,
        //catalina.connector.Response
        request = connector.createRequest();
        // HttpServletRequest里面有真正的request
        request.setCoyoteRequest(req);
        response = connector.createResponse();
        response.setCoyoteResponse(res);

        // Link objects
        request.setResponse(response);
        response.setRequest(request);

        // Set as notes
        req.setNote(ADAPTER_NOTES, request);
        res.setNote(ADAPTER_NOTES, response);

        // Set query string encoding
        req.getParameters().setQueryStringCharset(connector.getURICharset());
    }

    if (connector.getXpoweredBy()) {
        response.addHeader("X-Powered-By", POWERED_BY);
    }

    boolean async = false;
    boolean postParseSuccess = false;

    req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get());
    req.setRequestThread();

    try {
        // Parse and set Catalina and configuration specific
        // request parameters 后置解析请求
        // 2.转换请求参数并完成请求映射。
        postParseSuccess = postParseRequest(req, request, res, response);
        if (postParseSuccess) {
            //check valves if we support async
            request.setAsyncSupported(
                    connector.getService().getContainer().getPipeline().isAsyncSupported());

            // Calling the container 调用容器,拿到Engine的pipeline
            // 3.得到当前Enginef的第一个Valve并执行(invoke),以完成客户端请求处理。
            connector.getService().getContainer().getPipeline().getFirst().invoke(
                    request, response);
        }
        // 4. 如果是异步请求
        if (request.isAsync()) {
            async = true;
            // 4.1 获取请求读取事件监听器ReadListener
            ReadListener readListener = req.getReadListener();
            if (readListener != null && request.isFinished()) {
                // Possible the all data may have been read during service()
                // method so this needs to be checked here
                ClassLoader oldCL = null;
                try {
                    oldCL = request.getContext().bind(false, null);
                    if (req.sendAllDataReadEvent()) {
                        // 4.2 如果请求读取已经结束,触发ReadListener.onAllDataRead()
                        req.getReadListener().onAllDataRead();
                    }
                } finally {
                    request.getContext().unbind(false, oldCL);
                }
            }

            Throwable throwable =
                    (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);

            // If an async request was started, is not going to end once
            // this container thread finishes and an error occurred, trigger
            // the async error process
            if (!request.isAsyncCompleting() && throwable != null) {
                request.getAsyncContextInternal().setErrorState(throwable, true);
            }

        } else {
        // 5.如果为同步请求
            // Flush并关闭请求输入流
            request.finishRequest();
            // Flush 并关闭响应输出流
            response.finishResponse();
        }

    } catch (IOException e) {
        // Ignore
    } finally {
      ...
    }
}
复制代码

CoyoteAdapter..servicef的具体处理过程如下(只列出主要步骤):

  1. 根据Connector的请求(org.apache.coyote.Request)和响应(org.apache.coyote.Response)对象创建Servlet请求(org.apache.catalina.connector.Request)和响应(org.apache.catalina.connector.Response)。

  2. 转换请求参数并完成请求映射。

    • 请求UI解码,初始化请求的路径参数。
    • 检测URI是否合法,如果非法,则返回响应码400。
    • 请求映射,具体算法参见3.5,2节,映射结果保存到org.apache.catalina.connector.Request.mappingData,类型为org.apache..tomcat.util.http.mapper.MappingData,请求映射处理最终会根据URI定位到一个有效的Wrapper。
    • 如果映射结果MappingDatal的redirectPath/属性不为空(即为重定向请求),则调用org. apache.catalina.connector.Response.sendRedirect发送重定向并结束,
    • 如果当前Connector.不允许追踪(allowTrace为false)且当前请求的Method为TRACE,则返回响应码405。
    • 执行连接器的认证及授权。
  3. 得到当前Engine的第一个Valve并执行invoke,以完成客户端请求处理

注意:由于Pipeline和Valve为职责链模式,因此执行第一个Valve即保证了整个Valve链的执行。

  1. 如果为异步请求:
    • 获得请求读取事件监听器(ReadListener);
    • 如果请求读取已经结束,触发ReadListener.onAllDataRead。.
  2. 如果为同步请求:
    • Flush并关闭请求输入流;
    • Fush并关闭响应输出流。

2. 请求映射

请求映射过程具体分为两部分,一部分位于CoyoteAdapter.postParseRequest,负责根据请求路径匹配的结果,按照会话等信息获取最终的映射结果(因为只根据请求路径匹配,结果可能为多个)。第二部分位于Mapper.map,负责完成具体的请求路径的匹配

2.1 CoyoteAdapter.postRequest()

该方法中的映射处理算法如图所示: image.png

从图中可以看出,请求映射算法非常复杂(该图还不包含请求路径的匹配一加粗部分),源码为:

protected boolean postParseRequest(org.apache.coyote.Request req, Request request,
        org.apache.coyote.Response res, Response response) throws IOException, ServletException {
    ...
    
    MessageBytes decodedURI = req.decodedURI();

    if (undecodedURI.getType() == MessageBytes.T_BYTES) {
        // Copy the raw URI to the decodedURI
        decodedURI.duplicate(undecodedURI);

        // Parse (and strip out) the path parameters
        parsePathParameters(req, request);

        // URI decoding
        // %xx decoding of the URL
        try {
            // 2.1 请求URI解码,初始化请求的路径参数。
            req.getURLDecoder().convert(decodedURI.getByteChunk(), connector.getEncodedSolidusHandlingInternal());
        } catch (IOException ioe) {
            response.sendError(400, "Invalid URI: " + ioe.getMessage());
        }
        // Normalization
        if (normalize(req.decodedURI())) {
            // Character decoding
            convertURI(decodedURI, request);
            // Check that the URI is still normalized
            // Note: checkNormalize is deprecated because the test is no
            //       longer required in Tomcat 10 onwards and has been
            //       removed
            // 2.2 检测URI是否合法,如果非法,则返回响应码400。
            if (!checkNormalize(req.decodedURI())) {
                response.sendError(400, "Invalid URI");
            }
        } else {
            response.sendError(400, "Invalid URI");
        }
    } else {
        /* The URI is chars or String, and has been sent using an in-memory
         * protocol handler. The following assumptions are made:
         * - req.requestURI() has been set to the 'original' non-decoded,
         *   non-normalized URI
         * - req.decodedURI() has been set to the decoded, normalized form
         *   of req.requestURI()
         */
        decodedURI.toChars();
        // Remove all path parameters; any needed path parameter should be set
        // using the request object rather than passing it in the URL
        CharChunk uriCC = decodedURI.getCharChunk();
        int semicolon = uriCC.indexOf(';');
        if (semicolon > 0) {
            decodedURI.setChars(uriCC.getBuffer(), uriCC.getStart(), semicolon);
        }
    }

    // Request mapping.
    // 2.3 请求映射,映射结果保存到org.apache.catalina.connector.Request..
    //mappingData,类型为org.apache.tomcat.util.htp,mapper.MappingData,请求映射处理最终
    //会根据URI定位到一个有效的Wrapper。
    MessageBytes serverName;
    if (connector.getUseIPVHosts()) {
        serverName = req.localName();
        if (serverName.isNull()) {
            // well, they did ask for it
            res.action(ActionCode.REQ_LOCAL_NAME_ATTRIBUTE, null);
        }
    } else {
        serverName = req.serverName();
    }

    // Version for the second mapping loop and
    // Context that we expect to get for that version
    // (1) 定义三个局部变量
    String version = null; // 需要匹配的版本号,初始化为空,也就是匹配所有版本
    Context versionContext = null; // 用于暂存按照会话ID匹配的Context,初始化为空
    boolean mapRequired = true; // 是否需要映射,用于控制映射匹配循环,初始化为true。

    if (response.isError()) {
        // An error this early means the URI is invalid. Ensure invalid data
        // is not passed to the mapper. Note we still want the mapper to
        // find the correct host.
        decodedURI.recycle();
    }

    // (2) 通过一个循环(mapRequired==true)来处理映射匹配,因为只通过一次处理并不能确保
    //得到正确结果。
    while (mapRequired) {
        // This will map the the latest version by default
        // (3)在循环第(1)步,调用Mapper.map()方法按照请求路径进行匹配,参数为serverName、url、
        //version。因为version初始化时为空,所以第一次执行时,所有匹配该请求路径的Context均会返
        //回,此时MappingData.contexts中存放了所有结果,而MappingData.contextr中存放了最新版本
        connector.getService().getMapper().map(serverName, decodedURI,
                version, request.getMappingData());

        // If there is no context at this point, either this is a 404
        // because no ROOT context has been deployed or the URI was invalid
        // so no context could be mapped.
        // (4)如果没有任何匹配结果,那么返回404响应码,匹配结束。
        if (request.getContext() == null) {
            // Allow processing to continue.
            // If present, the rewrite Valve may rewrite this to a valid
            // request.
            // The StandardEngineValve will handle the case of a missing
            // Host and the StandardHostValve the case of a missing Context.
            // If present, the error reporting valve will provide a response
            // body.
            return true;
        }

        // Now we have the context, we can parse the session ID from the URL
        // (if any). Need to do this before we redirect in case we need to
        // include the session id in the redirect
        String sessionID;
        // (5)尝试从请求的URL、Cookie、SSL会话获取请求会话D,并将mapRequired设置为false(当
        //第(3)步执行成功后,默认不再执行循环,是否需要重新执行由后续步骤确定)。
        if (request.getServletContext().getEffectiveSessionTrackingModes()
                .contains(SessionTrackingMode.URL)) {

            // Get the session ID if there was one
            sessionID = request.getPathParameter(
                    SessionConfig.getSessionUriParamName(
                            request.getContext()));
            if (sessionID != null) {
                request.setRequestedSessionId(sessionID);
                request.setRequestedSessionURL(true);
            }
        }

        // Look for session ID in cookies and SSL session
        try {
            parseSessionCookiesId(request);
        } catch (IllegalArgumentException e) {
            // Too many cookies
            if (!response.isError()) {
                response.setError();
                response.sendError(400);
            }
            return true;
        }
        parseSessionSslId(request);

        sessionID = request.getRequestedSessionId();
        // 将mapRequired设置为false
        mapRequired = false;

        // (6)如果version不为空,且MappingData.context与versionContext相等,即表明当前匹配结果
        //是会话查询的结果,此时不再执行第(7)步。当前步骤仅用于重复匹配,第一次执行时,version
        //和versionContext均为空,所以需要继续执行第(7)步,而重复执行时,已经指定了版本,可得到
        //唯一的匹配结果。
        if (version != null && request.getContext() == versionContext) {
            // We got the version that we asked for. That is it.
        } else {
            version = null;
            versionContext = null;

            Context[] contexts = request.getMappingData().contexts;
            // Single contextVersion means no need to remap
            // No session ID means no possibility of remap
            //(7)如果不存在会话ID,那么第(3)步匹配结果即为最终结果(即使用匹配的最新版本)。否则,
            //从MappingData.contexts中查找包含请求会话ID的最新版本,查询结果分如下情况。
            if (contexts != null && sessionID != null) {
                // Find the context associated with the session
                // 没有查询结果(即表明会话D过期)或者查询结果与第(3)步匹配结果相等,这时同样使
                //用的是第(3)步的匹配结果。
                for (int i = contexts.length; i > 0; i--) {
                    Context ctxt = contexts[i - 1];
                    if (ctxt.getManager().findSession(sessionID) != null) {
                        // We found a context. Is it the one that has
                        // already been mapped?
                        // 有查询结果且与第(3)步匹配结果不相等(表明当前会话使用的不是最新版本),将version
                        //设置为查询结果的版本,versionContext设置为查询结果,将mapRequired设置为true,重
                        //置MappingData。此种情况下,需要重复执行第(3)步(之所以需要重复执行,是因为虽然
                        //通过会话ID查询到了合适的Context,但是MappingData中记录的Wrapperl以及相关的路径
                        //信息仍属于最新版本Context,是错误的),并明确指定匹配版本。指定版本后,第(3)步应
                        //只存在唯一的匹配结果。
                        if (!ctxt.equals(request.getMappingData().context)) {
                            // Set version so second time through mapping
                            // the correct context is found
                            version = ctxt.getWebappVersion();
                            versionContext = ctxt;
                            // Reset mapping
                            request.getMappingData().recycle();
                            mapRequired = true;
                            // Recycle cookies and session info in case the
                            // correct context is configured with different
                            // settings
                            request.recycleSessionInfo();
                            request.recycleCookieInfo(true);
                        }
                        break;
                    }
                }
            }
        }

        //(8) 如果mapRequired为false(即已找到唯一的匹配结果),但匹配的Context状态为暂停(如
        //正在重新加载),此时等待l秒钟,并将mapRequiredi设置为true,重置MappingData。此种情况下,
        //需要进行重新匹配,直到匹配到一个有效的Context或者无任何匹配结果为止。
        if (!mapRequired && request.getContext().getPaused()) {
            // Found a matching context but it is paused. Mapping data will
            // be wrong since some Wrappers may not be registered at this
            // point.
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // Should never happen
            }
            // Reset mapping
            request.getMappingData().recycle();
            mapRequired = true;
        }
    }

    // Possible redirect
    // 2.4 如果映射结果MappingData的redirectPath/属性不为空(即为重定向请求),则调用org,
    //apache,catalina.connector.Response,sendRedirect.发送重定向并结束。
    MessageBytes redirectPathMB = request.getMappingData().redirectPath;
    if (!redirectPathMB.isNull()) {
        String redirectPath = URLEncoder.DEFAULT.encode(
                redirectPathMB.toString(), StandardCharsets.UTF_8);
        String query = request.getQueryString();
        if (request.isRequestedSessionIdFromURL()) {
            // This is not optimal, but as this is not very common, it
            // shouldn't matter
            redirectPath = redirectPath + ";" +
                    SessionConfig.getSessionUriParamName(
                        request.getContext()) +
                "=" + request.getRequestedSessionId();
        }
        if (query != null) {
            // This is not optimal, but as this is not very common, it
            // shouldn't matter
            redirectPath = redirectPath + "?" + query;
        }
        response.sendRedirect(redirectPath);
        request.getContext().logAccess(request, response, 0, true);
        return false;
    }

    // Filter trace method
    // 2.5 如果当前Connector不允许追踪(allowTrace为false)且当前请求的Method为TRACE,则
    //返回响应码405。
    if (!connector.getAllowTrace()
            && req.method().equalsIgnoreCase("TRACE")) {
        Wrapper wrapper = request.getWrapper();
        String header = null;
        if (wrapper != null) {
            String[] methods = wrapper.getServletMethods();
            if (methods != null) {
                for (String method : methods) {
                    if ("TRACE".equals(method)) {
                        continue;
                    }
                    if (header == null) {
                        header = method;
                    } else {
                        header += ", " + method;
                    }
                }
            }
        }
        if (header != null) {
            res.addHeader("Allow", header);
        }
        response.sendError(405, "TRACE method is not allowed");
        // Safe to skip the remainder of this method.
        return true;
    }

    // 2.6 执行连接器的认证及授权。
    doConnectorAuthenticationAuthorization(req, request);

    return true;
}
复制代码

接下来将对每一步做一个详细介绍:

  1. 定义3个局部变量。

    • version:需要匹配的版本号,初始化为空,也就是匹配所有版本。
    • versionContext:用于暂存按照会话ID匹配的Context,初始化为空。
    • mapRequired:是否需要映射,用于控制映射匹配循环,初始化为true.
  2. 通过一个循环(mapRequired==true)来处理映射匹配,因为只通过一次处理并不能确保得到正确结果(第(3)步至第(8)步均为循环内处理)。

  3. 在循环第(1)步,调用Mapper.map()方法按照请求路径进行匹配,参数为serverName、url、version。因为version初始化时为空,所以第一次执行时,所有匹配该请求路径的Context均会返回,此时MappingData.contexts中存放了所有结果,而MappingData.contextr中存放了最新版本。

  4. 如果没有任何匹配结果,那么返回404响应码,匹配结束。

  5. 尝试从请求的URL、Cookie、SSL会话获取请求会话ID,并将mapRequired设置为false(当第3步执行成功后,默认不再执行循环,是否需要重新执行由后续步骤确定)。

  6. 如果version不为空,且MappingData.context与versionContext相等,即表明当前匹配结果是会话查询的结果,此时不再执行第(7)步。当前步骤仅用于重复匹配,第一次执行时,version和versionContext均为空,所以需要继续执行第(7)步,而重复执行时,已经指定了版本,可得到唯一的匹配结果。

  7. 如果不存在会话ID,那么第(3)步匹配结果即为最终结果(即使用匹配的最新版本)。否则,从MappingData.contexts中查找包含请求会话ID的最新版本,查询结果分如下情况。

    • 没有查询结果(即表明会话D过期)或者查询结果与第(3)步匹配结果相等,这时同样使用的是第(3)步的匹配结果。
    • 有查询结果且与第(3)步匹配结果不相等(表明当前会话使用的不是最新版本),将version设置为查询结果的版本,versionContext设置为查询结果,将mapRequired设置为true,重置MappingData。此种情况下,需要重复执行第3步(之所以需要重复执行,是因为虽然 通过会话ID查询到了合适的Context,但是MappingData中记录的Wrapperl以及相关的路径信息仍属于最新版本Context,是错误的),并明确指定匹配版本。指定版本后,第(3)步应只存在唯一的匹配结果。
  8. 如果mapRequired为false(即已找到唯一的匹配结果),但匹配的Context状态为暂停(如正在重新加载),此时等待1秒钟,并将mapRequired设置为true,重置MappingData。此种情况下,需要进行重新匹配,直到匹配到一个有效的Context或者无任何匹配结果为止。

通过上面的处理,Tomcat确保得到的Context符合如下要求。 - 匹配请求路径。 - 如果有有效会话,则为包含会话的最新版本。 - 如果没有有效会话,则为所有匹配请求的最新版本。 - Context必须是有效的(非暂停状态)。

2.2 Mapper.map

以上我们只讲解了Tomcat对于请求匹配结果的处理,接下来再看一下请求路径的具体匹配算法(即上图中加粗的部分)。

2.2.1 Mapper结构图

在讲解算法之前,有必要先了解一下Mappert的静态结构,这有助于我们加深对算法的理解。Mapper的静态结构如图所示: image.png

  • Mapper对于Host、Context、Wrapper均提供了对应的封装类,因此描述算法时,我们用MappedHost、MappedContext、MappedWrapper表示其封装对象,而用Host、Context、Wrapper表示Catalina中的组件。

  • MappedHost支持封装Host缩写。当封装的是一个Host缩写时,realHost即为其指向的真实Host封装对象。当封装的是一个Host且存在缩写时,aliases即为其对应缩写的封装对象

  • 为了支持Context的多版本,Mapper提供了MappedContext、ContextVersion两个封装类。当注册一个Context时,MappedContext名称为Context的路径,并且通过一个ContextVersion列表保存所有版本的Context。ContextVersion保存了单个版本的Context,名称为具体的版本号。

  • ContextVersion保存了一个具体Context及其包含的Wrapper封装对象,包括默认Wrapper、精确匹配的Wrapper、通配符匹配的Wrapper、通过扩展名匹配的Wrapper。

  • MappedWrapper保存了具体的Wrapper。

  • 所有注册组件按层级封装为一个MappedHost列表,并保存到Mapper。

在Mapper中,每一类Container按照名称的ASCII正序排序(注意排序规则,这会影响一些特殊情况下的匹配结果)。

以Context为例,下列名称均合法:/abbb/a、/abbb、/abb、/Abbb、/Abbb/a、/Abbb/ab,而在Mapperr中,它们的顺序为:/Abbb、/Abbb/a、/Abbb/ab、/abb、/abbb、/abbb/a,无论以何种顺序添加。

Mapper.map()方法的请求映射结果为org.apache.tomcat.util.http.mapper.MappingData对 象,保存在请求的mappingData属性中。

org.apache.tomcat.util.http.mapper.MappingData的结构如下,具体含义参见注释:

public class MappingData {

    // 匹配的Host
    public Host host = null;
    // 匹配的Context
    public Context context = null;
    // Context路径中'/'的数量
    public int contextSlashCount = 0;
    // 匹配的Context列表,只用于匹配过程,并非最终使用结果
    public Context[] contexts = null;
    // 匹配的Wrapper
    public Wrapper wrapper = null;
    // 对于JspServlet,其对应的匹配pattern是否包含通配符
    public boolean jspWildCard = false;
    @Deprecated
    public final MessageBytes contextPath = MessageBytes.newInstance(); // Context路径
    public final MessageBytes requestPath = MessageBytes.newInstance(); // 相对于Context的请求路径
    public final MessageBytes wrapperPath = MessageBytes.newInstance(); // Servlet路径
    public final MessageBytes pathInfo = MessageBytes.newInstance();   // 相对于Servlet的请求路径 

    public final MessageBytes redirectPath = MessageBytes.newInstance();// 重定向路径
    ...
}
复制代码

对于contexts属性,主要使用于多版本Web应用同时部署的情况,此时可以匹配请求路径的Context存在多个,需要进一步处理。而context属性始终存放的是匹配请求路径的最新版本(注意,匹配请求的最新版本并不代表是最后的匹配结果,具体参见算法讲解)。

Mapper.map的具体算法如图: image.png

Mapper.map()源码:

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

    // (1)一般情况下,需要查找的Host名称为请求的serverName。.但是,如果没有指定Host名称,
    //那么将使用默认Host名称。
    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);
}

private final void internalMap(CharChunk host, CharChunk uri,
        String version, MappingData mappingData) throws IOException {
    // 如果mappingData.host不为空,说明mappingData是有问题的。抛出异常
    if (mappingData.host != null) {
        // The legacy code (dating down at least to Tomcat 4.1) just
        // skipped all mapping work in this case. That behaviour has a risk
        // of returning an inconsistent result.
        // I do not see a valid use case for it.
        throw new AssertionError();
    }

    // Virtual host mapping
    MappedHost[] hosts = this.hosts;

    /** 查找的MappedHost,可以分为三步
    * 1. 根据当前的hostName查找具体的host
    * 2. 如果没有找到的话,就去掉第一个 "."之前的部分,再次查找。
    *      例如,当输入时 abc.bde.ef.com,时,如果没有找到相应匹配的host时,则查找bde.ef.com。
    *      因为看到在构建mapper,增加host上,当有会把host name的前导* 去掉的。
    * 3.如果还没有找到,则设置为 默认的host
     */
    // (2)按照host名称查询Mapper.Host(忽略大小写),如果没有找到匹配结果,且默认Host名称不
    // 为空,则按照默认Host名称精确查询。如果存在匹配结果,将其保存到MappingData的host属性。
    // 按照host名称,精确查找Mapper.Host,忽略大小写
    MappedHost mappedHost = exactFindIgnoreCase(hosts, host);
    if (mappedHost == null) {
        // Note: Internally, the Mapper does not use the leading * on a
        //       wildcard host. This is to allow this shortcut.
        int firstDot = host.indexOf('.');
        if (firstDot > -1) {
            int offset = host.getOffset();
            try {
                host.setOffset(firstDot + offset);
                // 按照host名称,精确查找Mapper.Host,不忽略大小写
                mappedHost = exactFindIgnoreCase(hosts, host);
            } finally {
                // Make absolutely sure this gets reset
                host.setOffset(offset);
            }
        }
        if (mappedHost == null) {
            mappedHost = defaultHost;
            if (mappedHost == null) {
                return;
            }
        }
    }
    // 将查找到的Host更新到MappingData
    mappingData.host = mappedHost.object;

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

    uri.setLimit(-1);

    // Context mapping
    // (3)按照url查找MapperdContextl最大可能匹配的位置pos(只限于第(2)步查找到的MappedHost
    //下的MappedContext)。之所以如此描述,与Tomcat的查找算法相关。
    ContextList contextList = mappedHost.contextList;
    MappedContext[] contexts = contextList.contexts;
    // 按照url查找Mapper.Context最大可能匹配的位置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等于context名称或者以context名称+ '/'开头
        if (uri.startsWith(context.name)) {
            length = context.name.length();
            if (uri.getLength() == length) {
                // true 跳出循环
                found = true;
                break;
            } else if (uri.startsWithIgnoreCase("/", length)) {
                found = true;
                break;
            }
        }
        // (4)当第(3)步查找的pos≥O时,得到对应的MappedContext,.如果url与MappedContext的路径
        //相等或者url以MappedContexth路径+“”开头,均视为找到匹配的MappedContext。否则,循环执
        // 行第(4)步,逐渐降低精确度以查找合适的MappedContext(具体可参见第(3)步的例子)。
        // 去除url最后一个'/'之后的部分
        if (lastSlash == -1) {
            lastSlash = nthSlash(uri, contextList.nesting + 1);
        } else {
            lastSlash = lastSlash(uri);
        }
        uri.setEnd(lastSlash);
        pos = find(contexts, uri);
    }
    uri.setEnd(uriEnd);
    //(5)如果循环结束后仍未找到合适的MappedContext,.那么会判断第0个MappedConext的名称
    //是否为空字符串。如果是,则将其作为匹配结果(即使用默认MappedContext)。
    if (!found) {
        // 如果第0个Mapper.Context名称为空字符串,则Context=context[0]
        if (contexts[0].name.equals("")) {
            context = contexts[0];
        } else {
            context = null;
        }
    }
    // 还没有找到,返回
    if (context == null) {
        return;
    }

    // (6)前面曾讲到MappedContext存放了路径相同的所有版本的Context(ContextVersion),因此
    //在第(S)步结束后,还需要对MappedContexth版本进行处理。如果指定了版本号,则返回版本号相
    //等的Context Version,否则返回版本号最大的。最后,将Context Version中维护的Context保存到
    //MappingData中。
    // 下面更新MappingData的contextPath、contexts、context
    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;

    // Wrapper mapping
    // (7)如果Context当前状态为有效(由图3-6可知,当Context处于暂停状态时,将会重新按照url
    //映射,此时MappedWrapperf的映射无意义),则映射对应的MappedWrapper。
    // 如果context有效,查找Wrapper
    if (!contextVersion.isPaused()) {
        internalMapWrapper(contextVersion, uri, mappingData);
    }

}
复制代码

为了简化流程图,部分处理细节并未展开描述(如查找Wrapper),因此我们仍对每一步做一个详细的讲解:

  1. 一般情况下,需要查找的Host名称为请求的serverName。但是,如果没有指定Host名称,那么将使用默认Host名称。

注意:默认Host名称通过按照Engine的defaultHost属性查找其Host子节点获取。
查找规则:Host名称与defaultHost相等或Host缩写名与defaultHost相等(忽略大小写)此处需要注意一个问题,由于Container在维护子节点时,使用的是HashMap,因此在得到其子节点列表时,顺序与名称的哈希码相关。例如,如果Engine中配置的defaultHost为“Server001”,而Tomcat中配置了“SERVER001”和“Server001”两个Host,此时默认Host名称为“SERVER001”。而如果我们将“Server001”换成“server001”,则结果就变成了“server001”。当然,实际配置过程中,应彻底避免此种命名。

  1. 按照host名称查询Mapper.Host(忽略大小写),如果没有找到匹配结果,且默认Host名称不为空,则按照默认Host名称精确查询。如果存在匹配结果,将其保存到MappingData的host属性。

此处有时候会让人产生疑惑,第(I)步在没有指定host名称时,已将host名称设置为默认Host名称,为什么第(2)步仍需要按照默认Host名称查找。这主要满足如下场景:当host不为空,且为无效名称时,Tomcat将会尝试返回默认Host,而非空值。

  1. 按照url查找MapperdContext最大可能匹配的位置pos(只限于第(2)步查找到的MappedHost下的MappedContext)。之所以如此描述,与Tomcat的查找算法相关。

    • 在Mapper中所有Container是有序的((按照名称的ASCI正序排列,因此Tomcat采用二分法进行查找。其返回结果存在如下两种情况。
      • -1:表明url比当前MappedHost下所有的MappedContext的名称都小,也就是没有匹配的MappedContext.

      • ≥0:可能是精确匹配的位置,也可能是列表中比url小的最大值的位置。即使没有精确匹配,也不代表最终没有匹配项,这需要进一步处理。

      • 如果比较难以理解,我们下面试举一例。例如我们配置了两个Context,路径分别为:myapp和myapp/appl,在Tomcat中,这两个是允许同时存在的。然后我们尝试输入请求路径htp:/127.0.0.1:8080 myapp/appl/index.jsp。此时url为/myapp/appl/index.jsp。很显然,url不可能和Context路径精确匹配,此时返回比其小的最大值的位置(即myapp/appl)。当Tomcat发现其非精确匹配时,会将url进行截取(裁取为myapp/appl)再进行匹配,此时将会精确匹配/myapp/appl。当然,如果我们输入的是htp:127.0.0.1:8080/myapp/app2/index.jsp,Tomcat将会继续截取,直到匹配到myapp。

      • 由此可见,Tomeat总是试图查找一个最精确的MappedContext(如上例使用/myapp/appl,而非/myapp,尽管这两个都是可以匹配的)。

  2. 当第(3)步查找的pos≥O时,得到对应的MappedContext,如果url与MappedContext的路径相等或者url以MappedContext路径+“/”开头,均视为找到匹配的MappedContext。否则,循环执行第(4)步,逐渐降低精确度以查找合适的MappedContext(具体可参见第(3)步的例子)。

  3. 如果循环结束后仍未找到合适的MappedContext,那么会判断第0个MappedConext的名称是否为空字符串。如果是,则将其作为匹配结果(即使用默认MappedContext)。

  4. 前面曾讲到MappedContext存放了路径相同的所有版本的Context(ContextVersion),因此在第(5)步结束后,还需要对MappedContext版本进行处理。如果指定了版本号,则返回版本号相等的ContextVersion,否则返回版本号最大的。最后,将ContextVersion中维护的Context保存到MappingData中。

  5. 如果Context当前状态为有效(由图3-6可知,当Context处于暂停状态时,将会重新按照url

映射,此时MappedWrapper的映射无意义),则映射对应的MappedWrapper。

2.3 MapperWrapper映射

我们知道ContextVersion中将MappedWrapper分为:默认Wrapper(defaultWrapper)、精确Wrapper(exactWrappers)、前缀加通配符匹配Wrapper(wildcardWrappers)和扩展名匹配Wrapper (extensionWrappers)。之所以分为这几类是因为它们之间是存在匹配优先级的.

此外,在ContextVersion中,并非每一个Wrapper对应一个MappedWrapper对象而是每个url-pattern对应一个。如果web.xml中的servlet-mapping配置如下:

<servlet-mapping>
    <servlet-name>example</servlet-name>
    <url-pattern>*.do</url-pattern>
    <url-pattern>*.action</url-pattern>
</servlet-mapping>
复制代码

那么,在ContextVersion中将存在两个MappedWrapper封装对象,分别指向同一个Wrapper实例。

将Wrapper添加到ContextVersion对应的MappedWrapper分类中,源码路径为: ContextConfig.configureContext()-> StandardContext.addServletMappingDecoded() -> wrapper.addMapping() -> fireContainerEvent(ADD_MAPPING_EVENT, mapping) -> MapperListener.containerEvent()->Mapper.addWrapper()如下:

protected void addWrapper(ContextVersion context, String path,
        Wrapper wrapper, boolean jspWildCard, boolean resourceOnly) {

    synchronized (context) {
        // 以'/*'开始的path加入到通配符wrapper当中
        if (path.endsWith("/*")) {
            // Wildcard wrapper
            String name = path.substring(0, path.length() - 2);
            MappedWrapper newWrapper = new MappedWrapper(name, wrapper,
                    jspWildCard, resourceOnly);
            MappedWrapper[] oldWrappers = context.wildcardWrappers;
            MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length + 1];
            if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                context.wildcardWrappers = newWrappers;
                int slashCount = slashCount(newWrapper.name);
                if (slashCount > context.nesting) {
                    context.nesting = slashCount;
                }
            }
        } else if (path.startsWith("*.")) { // 以'*.'开始的path加入到后缀wrapper当中
            // Extension wrapper
            String name = path.substring(2);
            MappedWrapper newWrapper = new MappedWrapper(name, wrapper,
                    jspWildCard, resourceOnly);
            MappedWrapper[] oldWrappers = context.extensionWrappers;
            MappedWrapper[] newWrappers =
                new MappedWrapper[oldWrappers.length + 1];
            if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                context.extensionWrappers = newWrappers;
            }
        } else if (path.equals("/")) { // 设置默认的DefaultWrapper
            // Default wrapper
            MappedWrapper newWrapper = new MappedWrapper("", wrapper,
                    jspWildCard, resourceOnly);
            context.defaultWrapper = newWrapper;
        } else { // 到这里都还没匹配到的就是精确的wrapper了
            // Exact wrapper
            final String name;
            if (path.length() == 0) {
                // Special case for the Context Root mapping which is
                // treated as an exact match
                name = "/";
            } else {
                name = path;
            }
            MappedWrapper newWrapper = new MappedWrapper(name, wrapper,
                    jspWildCard, resourceOnly);
            MappedWrapper[] oldWrappers = context.exactWrappers;
            MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length + 1];
            if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                context.exactWrappers = newWrappers;
            }
        }
    }
}
复制代码

Mapper按照如下规则将Wrapper添加到ContextVersion对应的MappedWrapper分类中去。

  • 如果url-pattern以“/*”结尾,则为前缀加通配符匹配wildcardWrappers。此时,MappedWrapper的名称为url-pattern去除结尾的“/*”

  • 如果url-pattern以“*.”结尾,则为扩展名匹配extensionWrappers。此时,MappedWrapper的名称为url-pattern去除开头的“*.”

  • 如果url-pattern等于“/”,则为defaultWrapper。此时,MappedWrapper的名称为空字符串。

  • 其他情况均为精确exactWrappers。如果url-pattern为空字符串,MappedWrapper的名称为“/”,否则为url-pattern的值。

MappedWrapper匹配源码:

private final void internalMapWrapper(ContextVersion contextVersion,
                                      CharChunk path,
                                      MappingData mappingData) throws IOException {

    /**
     * (1)依据url和Context路径计算MappedWrapper匹配路径。例如,如果Context路径为“/myapp”,
     * url为“/myapp/app1/index.jsp”,那么MappedWrapper的匹配路径为“/app1/index.jsp”;如果url为
     * “/myapp”,那么MappedWrapperl的匹配路径为“/”"。
     */
    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
    /**
     * (2)先精确查找exactWrappers。
     */
    MappedWrapper[] exactWrappers = contextVersion.exactWrappers;
    internalMapExactWrapper(exactWrappers, path, mappingData);

    // Rule 2 -- Prefix Match
    /**
     * (3)如果未找到,然后再按照前缀查找wildcardWrappers,.算法与MappedContext查找类似,
     * 逐步降低精度。
     */
    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
    /**
     * (4)如果未找到,然后按照扩展名查找extension Wrappers。
     */
    MappedWrapper[] extensionWrappers = contextVersion.extensionWrappers;
    if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
        internalMapExtensionWrapper(extensionWrappers, path, mappingData,
                true);
    }

    // Rule 4 -- Welcome resources processing for servlets
    /**
     * (5)如果未找到,则尝试匹配欢迎文件列表(web.xml中的welcome-file-list配置)。主要用于
     * 我们输入的请求路径是一个目录而非文件的情况,如:htp:/127.0.0.1:8080/myapp/appl/。此时,
     * 使用的匹配路径为“原匹配路径+welcome-file-ist中的文件名称”。欢迎文件匹配分为如下两步。
     */
    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);
                /**
                 * (5.1)对于每个欢迎文件生成的新的匹配路径,先查找exactWrappers,再查找wildcardWrappers。
                 * 如果该文件在物理路径中存在,则查找extension Wrappers,如extension Wrappers未找到,则使用defaultWrapper
                 */
                // 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
     */
    /**
     * (5.2) 对于每个欢迎文件生成的新的匹配路径,查找extensionWrappers
     */
    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
    /**
     * (6)如果未找到,则使用默认MappedWrapper(通过conf/web.xml,即使Web应用不显式地进
     * 行配置,也一定会存在一个默认的Wrapper)。因此,无论请求链接是什么,只要匹配到合适的
     * Context,.那么肯定会存在一个匹配的Wrapper。
     */
    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);
}
复制代码

接下来看一下MappedWrapper的详细匹配过程:

  1. 依据url和Context路径计算MappedWrapper匹配路径。例如,如果Context路径为“myapp”,url为“/myapp/appl/index.jsp”,那么MappedWrapper的匹配路径为“/appl/index.jsp”;如果url为“/myapp”,那么MappedWrapperl的匹配路径为“/"。

  2. 先精确查找exactWrappers。

  3. 如果未找到,然后再按照前缀查找wildcardWrappers,算法与MappedContexti查找类似,逐步降低精度。

  4. 如果未找到,然后按照扩展名查找extensionWrappers。

  5. 如果未找到,则尝试匹配欢迎文件列表(web.xml中的welcome-file-list配置)。主要用于我们输入的请求路径是一个目录而非文件的情况,如:http://127.0.0.1:8080/myapp/appl/。 此时,使用的匹配路径为“原匹配路径+welcome-file-list中的文件名称”。欢迎文件匹配分为如下两步。

    • ①对于每个欢迎文件生成的新的匹配路径,先查找exactWrappers,再查找wildcardWrappers。如果该文件在物理路径中存在,则查找extension Wrappers,如extension Wrappers.未找到,则使用defaultWrapper
    • ②对于每个欢迎文件生成的新的匹配路径,查找extension Wrappers。
      • 注意在第①步中,只有当存在物理路径时,才会查找extension Wrappers,并在找不到时使用defaultWrapper,而第②步则不判断物理路径,直接通过extension Wrappers查找。按照这种方式处理,如果我们的配置如下:
      • url-pattern配置为“*.do”;
      • welcome-file-list包括index.do、index.html.
      • 当我们输入的请求路径为htp:/127.0.0.l:8080/myapp/appl/,且在appl目录下存在index.html文件时,打开的是index.html,而非index,do,即便它位于前面(因为它不是个具体文件,而是由Web应用动态生成的)。
  6. 如果未找到,则使用默认MappedWrapper(通过conf/web.xml,即使Web应用不显式地进行配置,也一定会存在一个默认的Wrapper)。因此,无论请求链接是什么,只要匹配到合适的Context,那么肯定会存在一个匹配的Wrapper。

3. Catalina请求处理

Tomcat采用职责链模式来处理客户端请求,以提高Servlet容器的灵活性和可扩展性。Tomcat定义了Pipeline(管道)和Valve(阀)两个接口,前者用于构造职责链,后者代表职责链上的每个处理器。由于Tomeat每一层Container均维护了一个Pipeline实例,因此我们可以在任何层级添加Valve配置,以拦截客户端请求进行定制处理(如打印请求日志)。与javax.servlet.Filter相比,Valve更靠近Servlet容器,而非Web应用,因此可以获得更多信息。而且Valve可以添加到任意一级的Container(如Host),便于针对服务器进行统一处理,不像javax.servlet.Filter仅限于单独的Web应用。

Tomcat的每一级容器均提供了基础的Valve:实现以完成当前容器的请求处理过程(如StandardHost对应的基础Valve实现为StandardHostValve),而且基础Valve实现始终位于职责链的未尾,以确保最后执行

我们看一下一个典型的Valve实现:

class Samplevalve extends ValveBase
    @Override
    public final void invoke(Request request,Response response)
    throws IOException,ServletException {
        if (isok()){
            //do something
            getNext().invoke(request,response);
        else {
            log.error(“Bad request!”);
        }
    }
}
复制代码

由上可知,只要我们得到Pipeline中的第一个Valve即可以启动整个职责链的执行,这也是为什么在3.2节中执行Engine的第一个Valve便可以完成整个客户端请求处理的原因。

基于此种设计方案,在完成请求映射之后,Tomcat的请求处理过程如图所示:

image.png

从图中我们可以知道,每一级Container的基础Valve在完成自身处理的情况下,同时还要确保启动下一级Container的Valve链的执行。而且由于“请求映射”过程已经将映射结果保存到请求对象Request中因此Valve直接从请求中获取下级Container即可。比如StandardEngine.invoke():

public final void invoke(Request request, Response response)
    throws IOException, ServletException {

    // Select the Host to be used for this Request
    // 从Request中获取Host
    Host host = request.getHost();
    if (host == null) {
        // HTTP 0.9 or HTTP 1.0 request without a host when no default host
        // is defined.
        // Don't overwrite an existing error
        if (!response.isError()) {
            response.sendError(404);
        }
        return;
    }
    if (request.isAsyncSupported()) {
        request.setAsyncSupported(host.getPipeline().isAsyncSupported());
    }

    // Ask this Host to process this request
    // 引擎管道执行完成以后,会主动把请求交给Host管道的第一个阀门
    host.getPipeline().getFirst().invoke(request, response);
}
复制代码

我们重点看一下StandardWrapperValve.invoke()方法:

public final void invoke(Request request, Response response)
    throws IOException, ServletException {

    ...
    // Allocate a servlet instance to process this request
    try {
        // 创建Servlet对象
        if (!unavailable) {
            servlet = wrapper.allocate();
        }
    } catch (UnavailableException e) {
        ...
    }
    ...
    // 为当前请求创建一个filterChain
    ApplicationFilterChain filterChain =
            ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

    // Call the filter chain for this request
    // NOTE: This also calls the servlet's service() method
    Container container = this.container;
    try {
        if ((servlet != null) && (filterChain != null)) {
            // Swallow output if needed
            if (context.getSwallowOutput()) {
                try {
                    SystemLogHandler.startCapture();
                    if (request.isAsyncDispatching()) {
                        request.getAsyncContextInternal().doInternalDispatch();
                    } else {
                        // 执行filterChain
                        filterChain.doFilter(request.getRequest(),
                                response.getResponse());
                    }
                } finally {
                    String log = SystemLogHandler.stopCapture();
                    if (log != null && log.length() > 0) {
                        context.getLogger().info(log);
                    }
                }
            } else {
                if (request.isAsyncDispatching()) {
                    request.getAsyncContextInternal().doInternalDispatch();
                } else {
                    filterChain.doFilter
                        (request.getRequest(), response.getResponse());
                }
            }

        }
    } 
    ...
}
复制代码

我们来看一下filterChain.doFilter()->internalDoFilter(request,response)方法:

private void internalDoFilter(ServletRequest request,
                              ServletResponse response)
    throws IOException, ServletException {

    // Call the next filter if there is one
    // 拿到每一个Filter执行doFilter方法
    if (pos < n) {
        ApplicationFilterConfig filterConfig = filters[pos++];
        try {
            Filter filter = filterConfig.getFilter();

            if (request.isAsyncSupported() && "false".equalsIgnoreCase(
                    filterConfig.getFilterDef().getAsyncSupported())) {
                request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
            }
            if( Globals.IS_SECURITY_ENABLED ) {
                final ServletRequest req = request;
                final ServletResponse res = response;
                Principal principal =
                    ((HttpServletRequest) req).getUserPrincipal();

                Object[] args = new Object[]{req, res, this};
                SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
            } else {
                filter.doFilter(request, response, this);
            }
        } catch (IOException | ServletException | RuntimeException e) {
            throw e;
        } catch (Throwable e) {
            e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.filter"), e);
        }
        return;
    }

    // We fell off the end of the chain -- call the servlet instance
    try {
        if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
            lastServicedRequest.set(request);
            lastServicedResponse.set(response);
        }

        if (request.isAsyncSupported() && !servletSupportsAsync) {
            request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
                    Boolean.FALSE);
        }
        // Use potentially wrapped request from this point
        if ((request instanceof HttpServletRequest) &&
                (response instanceof HttpServletResponse) &&
                Globals.IS_SECURITY_ENABLED ) {
            final ServletRequest req = request;
            final ServletResponse res = response;
            Principal principal =
                ((HttpServletRequest) req).getUserPrincipal();
            Object[] args = new Object[]{req, res};
            SecurityUtil.doAsPrivilege("service",
                                       servlet,
                                       classTypeUsedInService,
                                       args,
                                       principal);
        } else {
            // FilterChain 执行完没有任何异常,然后就会执行service方法
            servlet.service(request, response);
        }
    } 
    ...
}
复制代码

继续servlet.service(request, response)->GenericServlet.service()->HttpServlet.service()->然后根据GET,PUT请求执行对应的方法:

protected void service(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {

    String method = req.getMethod();

    if (method.equals(METHOD_GET)) {
        long lastModified = getLastModified(req);
        if (lastModified == -1) {
            // servlet doesn't support if-modified-since, no reason
            // to go through further expensive logic
            // doGet方法
            doGet(req, resp);
        } else {
            long ifModifiedSince;
            try {
                ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            } catch (IllegalArgumentException iae) {
                // Invalid date header - proceed as if none was set
                ifModifiedSince = -1;
            }
            if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                // If the servlet mod time is later, call doGet()
                // Round down to the nearest second for a proper compare
                // A ifModifiedSince of -1 will always be less
                maybeSetLastModified(resp, lastModified);
                doGet(req, resp);
            } else {
                resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            }
        }

    } else if (method.equals(METHOD_HEAD)) {
        long lastModified = getLastModified(req);
        maybeSetLastModified(resp, lastModified);
        doHead(req, resp);

    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);

    } else if (method.equals(METHOD_PUT)) {
        doPut(req, resp);

    } else if (method.equals(METHOD_DELETE)) {
        doDelete(req, resp);

    } else if (method.equals(METHOD_OPTIONS)) {
        doOptions(req,resp);

    } else if (method.equals(METHOD_TRACE)) {
        doTrace(req,resp);

    } else {
        //
        // Note that this means NO servlet supports whatever
        // method was requested, anywhere on this server.
        //

        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[1];
        errArgs[0] = method;
        errMsg = MessageFormat.format(errMsg, errArgs);

        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
}
复制代码

StandardWrapperValve中(由于Wrapper为最低一级的Container,且该Valve处于职责链末端,因此它始终最后执行),Tomcat构造FilterChain实例完成javax.servlet.Filter责任链的执行,并执行Servlet.service()方法将请求交由应用程序进行分发处理(如果采用了如Spring MVC等Web框架的话,Servlet会进一步根据应用程序内部的配置将请求交由对应的控制器处理)。

至此,就可以执行到我们自己写的Servlet的doGet()方法了,请求就可以被我们处理了。

参考文章

tomcat-9.0.60-src源码解析 
Tomcat架构解析
tomcat系列
Tomcat剖析之源码篇
tomcat源码解读
Tomcat源码篇-请求映射Mapper

猜你喜欢

转载自juejin.im/post/7084574031562670110