前言
在Tomcat 源码解读——启动篇中我们讲解到一个概念:
Mapper
,它是用来匹配用户的请求,并将请求路由到指定的容器进行处理的关键组件,本文将通过源码分析来了解这个组件的生命周期及运作原理。
设想一下,当用户通过 URL (http://www.doleje.com/petstore/hello.jsp) 请求我们的应用时,Tomcat该如何将这个请求正确地路由到我们指定的Servlet
上呢?我们按照Tomcat的组件关系,可以将这个 URL 拆分成如下的几个部分:
Host
:www.doleje.comContext
: petstoreServletWrapper
:Hello
换句话来说,就是Tomcat是如何将这个请求转到对应的 Host
-> Context
-> Servlet
上的呢?我们来慢慢分析。
Mapper
相关的模型及概念
从名称来看,这是一个映射关系,确实,Tomcat给它的定义也是将一个名称绑定到组件上,通过名称就能找到对应的组件。实际上,它是Tomcat对于Servlet规范中Servlet Rules的一个实现,它规范了资源如何映射的规则。
MapElement
是构成映射关系的基本元素,它简单地定义了一个名称对应的组件是什么,源码如下:
1 | protected abstract static class MapElement<T> { |
由它派生出来的则有:MappedHost
表示 Host
的映射关系,MappedContext
表示Context
的映射关系以及MappedWrapper
表示一个 ServletWrapper
的映射(至于Servlet
为什么叫Wapper
后文再述)。具体的源码都就不帖出来了,感兴趣的同学可以自行去翻阅,它们的关系如下图所示:
可以看到,它其实跟我们的Tomcat容器的层次结构是一样的,每个组件都唯一的路径+名称来标识,这样,当用户的请求过来时,各个层次分别根据当前的环境和自己维护的映射关系,一层一层的往下查找,直到找到对应的MappedWrapper
或者抛出404。
路由过程
为了有一个更直观的认知,我们先暂时跳过Mapper
的初始化构建过程,来看看当Mapper
构建完成后,Tomcat是怎么根据用户的请求依次路由定位到这个Mapper
找到最终的处理单元的。
在文章Tomcat源码解读:Connector之一个请求的完整历程中我们讲解了,当一个请求到达Tomcat时,是由Connector
创建了一个CoyoteAdapter
用于处理请求的,以Http11Processor
为例,在其service()
方法中相关的代码如下:
1 |
|
可以看到,最终请求是交给了CoyoteAdapter.service
去处理,其中关于Mapper
的代码如下所示:
1 | public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) |
Mapper
的路由过程是在Request的头信息解析完成之后进行的,在postParseRequest(req, request, res, response)
方法中我们可以找到相关的代码:
1 | protected boolean postParseRequest(org.apache.coyote.Request req, Request request, |
核心代码就在于connector.getService().getMapper().map(serverName, decodedURI, version, request.getMappingData())
,我们进入这个方法看看:
1 | private final void internalMap(CharChunk host, CharChunk uri, |
过程描述如下:
根据请求的Host去
Mapper
表中查找MappedHost
,这个过程其实就是简单的字符串匹配。之后根据请求的Context信息在
MappedHost
中的ContextList
中去匹配,找到对应的MappedContext
。在该方法的最后,进一步的在
ContextVersion
中去查找ServletWrapper
的Mapper
。
关于
ContextVersion
的概念可能大家平时关注得比较少,这个是Tomcat对于Parallel Deployment的体现,就是同一个应用可以有多个版本在部署并服务。试想一下,如果一个正在运行的服务需要新的功能或者有BUG FIX需要重新部署,但是又不能停机的时候该怎么办呢?在现代的集群时代,可以采用蓝绿发布或者金丝雀发现就可以很好的解决,但是在单例时代该怎么办呢?Tomcat给出的答案就是Parallel Deployment,可以同时部署应用的多个版本,而且请求的path保持一致。这真是个好消息。对于部署新版本之后到达的请求,默认会使用新版本来处理,对于旧的请求,由于session中已经包含请求数据,所以会继续处理,直到完成。
直到这时,Tomcat仍然只是将我们的请求定位到了Context
这一层,也就是我们的一个应用。再往下就是如何定位到我们处理请求的Servlet,这个过程在internalMapWrapper(contextVersion, uri, mappingData)
中。这个过程代码比较长,因为场景比较多,总结起来就是如下的五个步骤(四个子嵌套),每一步找到了就直接返回,否则进入下一步:
- 根据path严格匹配,比如/user/list。
- 根据前缀查找,比如:*/user/**。
- 根据扩展后缀查找,比如:\.do*。
- 查找是否是欢迎页面,也就是我们在
web.xml
中配置的<welcome-file-list>
节点内容,这里又分为如下三种情况:- 精确匹配
- 前缀匹配
- 指定文件
- 如果指定的文件不存在,则进行后缀扩展查找
- 如果还没有找到对应的
Mapper
,则返回默认的,初始化的时候会讲解默认的ServletMapper
来源。
另外,在由于在创建Mapper
的时候,Tomcat将所有的候选项都放入了一个排序好的数组中,所以在这个查找过程中采用了 二分法 进行查找,以提高效率,感兴趣的同学可以去find()
方法中查看。
注:以上的匹配过程是有严格的先后顺序的,我们在配置或者调试的时候需要注意。同时,我们也可以看到
<welcome-file-list>
的配置也是有先后顺序的。
初始化过程
说完了根据请求查找具体Mapper
的过程,我们再来分析看看这个这个映射关系的数据结构是如何构建的。我们在Tomcat 源码解读——启动篇中已经提到了一点,就是MapperListener
的初始化及启动过程,代码只有一行:mapperListener.start();
。我们看看这个方法的核心代码如下所示:
1 | public void startInternal() throws LifecycleException { |
首先获取当前的引擎以及默认的Host
,对应server.xml
中的如下配置:<Engine name="Catalina" defaultHost="localhost">
。
接着监听这个Engine
,addListeners(engine)
代码如下,除了监听Engine
本身外,还监听了所有的孩子组件,主要就是Host
:
1 | private void addListeners(Container container) { |
这里我们有必要介绍一下MapperListener
的继承关系图,如下所示,它继承了LifecycleListener
,LifecycleMBeanBase
, ContainerListener
,可以看出它除了能监听组件本身外,如果组件是一个容器类的,那么它还能监听容器的相关操作。
注册Host
具体监听做了什么事情我们晚点再分析,接着分析代码就是遍历所有的Host
并执行registerHost(host)
方法,我们看看这个方法做了什么:
1 | private void registerHost(Host host) { |
先是直接调用mapper.addHost
将当前的Host
注册到Mapper
中去,具体的注册过程代码就不贴出来了,就是维护了一个MappedHost
的数组用于保存这种映射关系。需要注意的是这里的MappedHost
数组是排序的,好处是在路由查找的时候可以进行二分法来提高效率。
注册 Context
接下来就是遍历该Host
下的Context
并依次注册过程,我们看看registerContext(context)
的方法又有哪些不同,代码如下:
1 | private void registerContext(Context context) { |
其实和registerHost
的流程是差不多的,在addContextVersion
中只不过多了一个查找对应 Host
的过程,关于ContextVersion
的概念前面已经提过了,大部分情况下是可以解理为一个应用的。
注册 Wrapper
看到这里我们惊奇地发现整个注册过程结束了,为什么没有看到具体的ServletWrapper
的注册过程?如果真的的至此结束,请求是如何找到对应的Servlet
的?很显然还有其它的操作未完成。
其实Wrapper
的注册过程是在监听器过程中完成的,还记得我们之前提到的监听器层次模型,以及通过addListeners
方法添加的监听器吗?我们现在看看这些监听器是做什么的。
在此,我们先回顾一下在Tomcat 源码解读——启动篇中服务启动 `Server.start()这一节中关于扫描web.xml
并添加Servlet
的过程。关键代码如下:
1 | public void addServletMappingDecoded(String pattern, String name, |
我们进入这个addMapping
方法看看:
1 | public void addMapping(String mapping) { |
除了常规的添加 Servlet 映射关系外还有非常重要的一点是发布了一个事件:ADD_MAPPING_EVENT
,这个事件的刚好就是为了今天我们讲解的Wrapper
注册过程服务的。在MapperListener
的事件监听代码中对于这个事件的处理如下(省略了其它事件的处理代码):
1 | public void containerEvent(ContainerEvent event) { |
可以看到这里解析了这个ServletMapping
相关的信息后,将这个映射关系注册到了Mapper
中去了,具体的过程在mapper.addWrapper
中,我们看看这个过程又是怎样的:
1 | protected void addWrapper(ContextVersion context, String path, |
同样,根据 前缀,后缀,默认值 以及 精确匹配 的几种不同场景,维护一个排序好的 MappedWrapper
数组。
总结
本文从Mapper
的概念模型开始,介绍了Tomcat在接收到用户请求时的路由过程,以及这个路由表的建设过程,了解到Host
,Context
以及Wrapper
的映射关系。同时也介绍了多ContextVersion
的来历,以及Tomcat是如何处理多版本的应用路由规则。