深入理解Tomcat系列之七:详解URL请求
阅读原文时间:2021年04月26日阅读:1

前言

这里分析一个实际的请求是如何在Tomcat中被处理的,以及最后是怎么样找到要处理的Servlet的?当我们在浏览器中输入http://hostname:port/contextPath/servletPath,前面的hostname与port用于建立tcp连接,由于Http也是基于Tcp协议的,所以这里涉及TCP连接的三次握手。后面的contextPath与servletPath则是与服务器进行请求的信息,contextPath指明了与服务器中哪个Context容器进行交互,服务器会根据这个URL与对应的Context容器建立连接,那么这个过程是如何实现的呢?

在Tomcat7(本文也是基于Tomcat7)中主要通过一个映射来完成的,这个映射的工作交给org.apache.tomcat.util.http.mapper.Mapper类来完成的,这个类保存了Container容器所有子容器的信息,在请求从Connector交给Container容器之前,Mapper会根据hostname和port将host容器与context容器设置到Request的mappingData属性中,这样在Connector的请求进入Container容器之前就知道了交给哪个容器了。
这段代码如下:

代码清单5-4:

// Virtual host mapping
        if (mappingData.host == null) {
            Host[] hosts = this.hosts;
            int pos = findIgnoreCase(hosts, host);
            if ((pos != -1) && (host.equalsIgnoreCase(hosts[pos].name))) {
                mappingData.host = hosts[pos].object;
                contexts = hosts[pos].contextList.contexts;
                nesting = hosts[pos].contextList.nesting;
            } else {
                if (defaultHostName == null) {
                    return;
                }
                pos = find(hosts, defaultHostName);
                if ((pos != -1) && (defaultHostName.equals(hosts[pos].name))) {
                    mappingData.host = hosts[pos].object;
                    contexts = hosts[pos].contextList.contexts;
                    nesting = hosts[pos].contextList.nesting;
                } else {
                    return;
                }
            }
        }
        // Context mapping
        if (mappingData.context == null) {
            int pos = find(contexts, uri);
            if (pos == -1) {
                return;
            }
            int lastSlash = -1;
            int uriEnd = uri.getEnd();
            int length = -1;
            boolean found = false;
            while (pos >= 0) {
                if (uri.startsWith(contexts[pos].name)) {
                    length = contexts[pos].name.length();
                    if (uri.getLength() == length) {
                        found = true;
                        break;
                    } else if (uri.startsWithIgnoreCase("/", length)) {
                        found = true;
                        break;
                    }
                }
                if (lastSlash == -1) {
                    lastSlash = nthSlash(uri, nesting + 1);
                } else {
                    lastSlash = lastSlash(uri);
                }
                uri.setEnd(lastSlash);
                pos = find(contexts, uri);
            }
            uri.setEnd(uriEnd);
            if (!found) {
                if (contexts[0].name.equals("")) {
                    context = contexts[0];
                }
            } else {
                context = contexts[pos];
            }
            if (context != null) {
                mappingData.contextPath.setString(context.name);
            }
        }
        if (context != null) {
            ContextVersion[] contextVersions = context.versions;
            int versionCount = contextVersions.length;
            if (versionCount > 1) {
                Object[] contextObjects = new Object[contextVersions.length];
                for (int i = 0; i < contextObjects.length; i++) {
                    contextObjects[i] = contextVersions[i].object;
                }
                mappingData.contexts = contextObjects;
            }
            if (version == null) {
                // Return the latest version
                contextVersion = contextVersions[versionCount - 1];
            } else {
                int pos = find(contextVersions, version);
                if (pos < 0 || !contextVersions[pos].name.equals(version)) {
                    // Return the latest version
                    contextVersion = contextVersions[versionCount - 1];
                } else {
                    contextVersion = contextVersions[pos];
                }
            }
            mappingData.context = contextVersion.object;
            mappingData.contextSlashCount = contextVersion.slashCount;
        }
        // Wrapper mapping
        if ((contextVersion != null) && (mappingData.wrapper == null)) {
            internalMapWrapper(contextVersion, uri, mappingData);
        }

下面启动服务器,在浏览器中输入http://localhost:8080/examples/jsp/jsp2/el/composite.jsp,断点调试可以mappingData.host属性为localhost,mappingData.contextPath.setString(context.name)中context.name为examples,mappingData.wrapperPath为/jsp/jsp2/el/composite.jsp,这验证了mappingData属性的有效性,那么mappingData属性是如何设置到Request对象的属性中的呢?

通过org.apache.catalina.connector.Request的源码可以知道,其是通过setContextPath方法与setHost方法设置进去的,其源码如下:

代码清单5-5:

    public void setHost(Host host) {
        mappingData.host = host;
    }
    public void setContextPath(String path) {
        if (path == null) {
            mappingData.contextPath.setString("");
        } else {
            mappingData.contextPath.setString(path);
        }
    }

由于请求是从Connector传过来的,而CoyoteAdapter是Connector中处理请求的最后一个类,那么设置这两个属性的代码肯定在CoyoteAdapter类中,果不其然:

代码清单5-6:

 // This will map the the latest version by default
    connector.getMapper().map(serverName, decodedURI, version,
                              request.getMappingData());
    request.setContext((Context) request.getMappingData().context);
    request.setWrapper((Wrapper) request.getMappingData().wrapper);

    //Mapper的map方法
    public void map(MessageBytes host, MessageBytes uri, String version,
                    MappingData mappingData)
        throws Exception {
        if (host.isNull()) {
            host.getCharChunk().append(defaultHostName);
        }
        host.toChars();
        uri.toChars();
        internalMap(host.getCharChunk(), uri.getCharChunk(), version,
                mappingData);
    }

intenalMap方法执行就是代码清单5-4的内容,这样就把从Connector传入请求,并设置Request对象的mappingData属性的整个流程就打通了。还有一个疑问是为什么Mapper类中可以拥有Container所有子容器的信息呢?答案需要回到Tomcat启动过程图的第21步的startIntenal方法了:

代码清单5-7:

    public void startInternal() throws LifecycleException {
        setState(LifecycleState.STARTING);
        // Find any components that have already been initialized since the
        // MBean listener won't be notified as those components will have
        // already registered their MBeans
        findDefaultHost();
        Engine engine = (Engine) connector.getService().getContainer();
        addListeners(engine);
        Container[] conHosts = engine.findChildren();
        for (Container conHost : conHosts) {
            Host host = (Host) conHost;
            if (!LifecycleState.NEW.equals(host.getState())) {
                // Registering the host will register the context and wrappers
                registerHost(host);
            }
        }
    }

这段代码就是将MapperListener作为一个监听者加到整个Container容器的每一个子容器中,这样任何一个子容器发生变化,MapperListener都将被通知,响应的mappingData属性也会改变。最后可以总结访问请求地址为http://localhost:8080/examples/composite.jsp的处理过程:

  1. 在端口8080启动Server,并通知Service完成启动,Service通知Connector完成初始化和启动的过程
  2. Connector首先收到这个请求,会调用ProtocolHandler完成http协议的解析,然后交给SocketProcessor处理,解析请求头,再交给CoyoteAdapter解析请求行和请求体,并把解析信息封装到Request和Response对象中
  3. 把请求(此时应该是Request对象,这里的Request对象已经封装了Http请求的信息)交给Container容器
  4. Container容器交给其子容器——Engine容器,并等待Engine容器的处理结果
  5. Engine容器匹配其所有的虚拟主机,这里匹配到Host
  6. 请求被移交给hostname为localhost的Host容器,host匹配其所有子容器Context,这里找到contextPath为/examples的Context容器。如果匹配不到就把该请求交给路径名为”“的Context去处理
  7. 请求再次被移交给Context容器,Context继续匹配其子容器Wrapper,由Wrapper容器加载composite.jsp对应的servlet,这里编译的servlet是basic_002dcomparisons_jsp.class文件
  8. Context容器根据后缀匹配原则*.jsp找到composite.jsp编译的java类的class文件
  9. Connector构建一个org.apache.catalina.connector.Request以及org.apache.catalina.connector.Response对象,使用反射调用Servelt的service方法
  10. Context容器把封装了响应消息的Response对象返回给Host容器
  11. Host容器把Response返回给Engine容器
  12. Engine容器返回给Connector
  13. Connetor容器把Response返回给浏览器
  14. 浏览器解析Response报文
  15. 显示资源内容

根据前面的内容,其中的映射关系是由MapperListener类完成的。