Tomcat源码解读——Mapper的路由游戏

前言

Tomcat 源码解读——启动篇中我们讲解到一个概念:Mapper,它是用来匹配用户的请求,并将请求路由到指定的容器进行处理的关键组件,本文将通过源码分析来了解这个组件的生命周期及运作原理。

​ 设想一下,当用户通过 URL (http://www.doleje.com/petstore/hello.jsp) 请求我们的应用时,Tomcat该如何将这个请求正确地路由到我们指定的Servlet上呢?我们按照Tomcat的组件关系,可以将这个 URL 拆分成如下的几个部分:

  1. Hostwww.doleje.com

  2. Context: petstore

  3. ServletWrapper:Hello

​ 换句话来说,就是Tomcat是如何将这个请求转到对应的 Host -> Context -> Servlet 上的呢?我们来慢慢分析。

Mapper 相关的模型及概念

​ 从名称来看,这是一个映射关系,确实,Tomcat给它的定义也是将一个名称绑定到组件上,通过名称就能找到对应的组件。实际上,它是Tomcat对于Servlet规范Servlet Rules的一个实现,它规范了资源如何映射的规则。

MapElement是构成映射关系的基本元素,它简单地定义了一个名称对应的组件是什么,源码如下:

1
2
3
4
5
6
7
8
9
protected abstract static class MapElement<T> {
public final String name;
public final T object;

public MapElement(String name, T object) {
this.name = name;
this.object = object;
}
}

​ 由它派生出来的则有:MappedHost表示 Host的映射关系,MappedContext表示Context的映射关系以及MappedWrapper表示一个 ServletWrapper的映射(至于Servlet为什么叫Wapper后文再述)。具体的源码都就不帖出来了,感兴趣的同学可以自行去翻阅,它们的关系如下图所示:

mapper组件关系图

​ 可以看到,它其实跟我们的Tomcat容器的层次结构是一样的,每个组件都唯一的路径+名称来标识,这样,当用户的请求过来时,各个层次分别根据当前的环境和自己维护的映射关系,一层一层的往下查找,直到找到对应的MappedWrapper或者抛出404

路由过程

​ 为了有一个更直观的认知,我们先暂时跳过Mapper的初始化构建过程,来看看当Mapper构建完成后,Tomcat是怎么根据用户的请求依次路由定位到这个Mapper找到最终的处理单元的。

​ 在文章Tomcat源码解读:Connector之一个请求的完整历程中我们讲解了,当一个请求到达Tomcat时,是由Connector创建了一个CoyoteAdapter用于处理请求的,以Http11Processor为例,在其service()方法中相关的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Override
public SocketState service(SocketWrapperBase<?> socketWrapper) throws IOException {

// 省略非关注点代码...

while (!getErrorState().isError() && keepAlive && !isAsync() && upgradeToken == null &&
sendfileState == SendfileState.DONE && !protocol.isPaused()) {

// 省略非关注点代码...

// Process the request in the adapter
if (getErrorState().isIoAllowed()) {
try {
rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE);

// 交由 adapter 去处理请求
getAdapter().service(request, response);
// Handle when the response was committed before a serious
// error occurred. Throwing a ServletException should both
// set the status to 500 and set the errorException.
// If we fail here, then the response is likely already
// committed, so we can't try and set headers.
if(keepAlive && !getErrorState().isError() && !isAsync() &&
statusDropsConnection(response.getStatus())) {
setErrorState(ErrorState.CLOSE_CLEAN, null);
}
} catch (InterruptedIOException e) {
setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e);
} catch (HeadersTooLargeException e) {
log.error(sm.getString("http11processor.request.process"), e);
// The response should not have been committed but check it
// anyway to be safe
if (response.isCommitted()) {
setErrorState(ErrorState.CLOSE_NOW, e);
} else {
response.reset();
response.setStatus(500);
setErrorState(ErrorState.CLOSE_CLEAN, e);
response.setHeader("Connection", "close"); // TODO: Remove
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("http11processor.request.process"), t);
// 500 - Internal Server Error
response.setStatus(500);
setErrorState(ErrorState.CLOSE_CLEAN, t);
getAdapter().log(request, response, 0);
}
}

// 省略非关注点代码...
}

// 省略非关注点代码...
}

​ 可以看到,最终请求是交给了CoyoteAdapter.service去处理,其中关于Mapper的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {

...

try {
...

// 注意看这里,Mapper 的设置入口就是在这里
postParseSuccess = postParseRequest(req, request, res, response);

if (postParseSuccess) {
//check valves if we support async
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}

...

} catch (IOException e) {
// Ignore
} finally {
...
}
}

Mapper的路由过程是在Request的头信息解析完成之后进行的,在postParseRequest(req, request, res, response)方法中我们可以找到相关的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
protected boolean postParseRequest(org.apache.coyote.Request req, Request request,
org.apache.coyote.Response res, Response response) throws IOException, ServletException {

...

boolean mapRequired = 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();
}

while (mapRequired) {

// 通过 Service 的 Mapper 来设置 request mappering data
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());

...

mapRequired = false;
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
if (contexts != null && sessionID != null) {
// Find the context associated with the session
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?
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;
}
}
}
}

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;
}
}
...
}

​ 核心代码就在于connector.getService().getMapper().map(serverName, decodedURI, version, request.getMappingData()),我们进入这个方法看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
private final void internalMap(CharChunk host, CharChunk uri,
String version, MappingData mappingData) throws IOException {

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 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);
mappedHost = exactFindIgnoreCase(hosts, host);
} finally {
// Make absolutely sure this gets reset
host.setOffset(offset);
}
}
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 mapping
ContextList contextList = mappedHost.contextList;
MappedContext[] contexts = contextList.contexts;
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];
if (uri.startsWith(context.name)) {
length = context.name.length();
if (uri.getLength() == length) {
found = true;
break;
} else if (uri.startsWithIgnoreCase("/", length)) {
found = true;
break;
}
}
if (lastSlash == -1) {
lastSlash = nthSlash(uri, contextList.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 = 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;

// Wrapper mapping
if (!contextVersion.isPaused()) {
internalMapWrapper(contextVersion, uri, mappingData);
}

}

​ 过程描述如下:

  1. 根据请求的HostMapper表中查找MappedHost,这个过程其实就是简单的字符串匹配。

  2. 之后根据请求的Context信息在MappedHost中的ContextList中去匹配,找到对应的MappedContext

  3. 在该方法的最后,进一步的在ContextVersion中去查找ServletWrapperMapper

关于 ContextVersion 的概念可能大家平时关注得比较少,这个是Tomcat对于Parallel Deployment的体现,就是同一个应用可以有多个版本在部署并服务。试想一下,如果一个正在运行的服务需要新的功能或者有BUG FIX需要重新部署,但是又不能停机的时候该怎么办呢?

在现代的集群时代,可以采用蓝绿发布或者金丝雀发现就可以很好的解决,但是在单例时代该怎么办呢?Tomcat给出的答案就是Parallel Deployment,可以同时部署应用的多个版本,而且请求的path保持一致。这真是个好消息。对于部署新版本之后到达的请求,默认会使用新版本来处理,对于旧的请求,由于session中已经包含请求数据,所以会继续处理,直到完成。

​ 直到这时,Tomcat仍然只是将我们的请求定位到了Context这一层,也就是我们的一个应用。再往下就是如何定位到我们处理请求的Servlet,这个过程在internalMapWrapper(contextVersion, uri, mappingData)中。这个过程代码比较长,因为场景比较多,总结起来就是如下的五个步骤(四个子嵌套),每一步找到了就直接返回,否则进入下一步:

  1. 根据path严格匹配,比如/user/list
  2. 根据前缀查找,比如:*/user/**
  3. 根据扩展后缀查找,比如:\.do*
  4. 查找是否是欢迎页面,也就是我们在web.xml中配置的<welcome-file-list>节点内容,这里又分为如下三种情况:
    • 精确匹配
    • 前缀匹配
    • 指定文件
    • 如果指定的文件不存在,则进行后缀扩展查找
  5. 如果还没有找到对应的Mapper,则返回默认的,初始化的时候会讲解默认的 ServletMapper来源。

​ 另外,在由于在创建Mapper的时候,Tomcat将所有的候选项都放入了一个排序好的数组中,所以在这个查找过程中采用了 二分法 进行查找,以提高效率,感兴趣的同学可以去find()方法中查看。

注:以上的匹配过程是有严格的先后顺序的,我们在配置或者调试的时候需要注意。同时,我们也可以看到<welcome-file-list>的配置也是有先后顺序的。

初始化过程

​ 说完了根据请求查找具体Mapper的过程,我们再来分析看看这个这个映射关系的数据结构是如何构建的。我们在Tomcat 源码解读——启动篇中已经提到了一点,就是MapperListener的初始化及启动过程,代码只有一行:mapperListener.start();。我们看看这个方法的核心代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void startInternal() throws LifecycleException {

setState(LifecycleState.STARTING);

Engine engine = service.getContainer();
if (engine == null) {
return;
}

findDefaultHost();

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

​ 首先获取当前的引擎以及默认的Host,对应server.xml中的如下配置:<Engine name="Catalina" defaultHost="localhost">

​ 接着监听这个EngineaddListeners(engine)代码如下,除了监听Engine本身外,还监听了所有的孩子组件,主要就是Host

1
2
3
4
5
6
7
private void addListeners(Container container) {
container.addContainerListener(this);
container.addLifecycleListener(this);
for (Container child : container.findChildren()) {
addListeners(child);
}
}

​ 这里我们有必要介绍一下MapperListener的继承关系图,如下所示,它继承了LifecycleListenerLifecycleMBeanBase, ContainerListener,可以看出它除了能监听组件本身外,如果组件是一个容器类的,那么它还能监听容器的相关操作。

mapper-listener继承关系

注册Host

​ 具体监听做了什么事情我们晚点再分析,接着分析代码就是遍历所有的Host并执行registerHost(host)方法,我们看看这个方法做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void registerHost(Host host) {

String[] aliases = host.findAliases();
mapper.addHost(host.getName(), aliases, host);

for (Container container : host.findChildren()) {
if (container.getState().isAvailable()) {
registerContext((Context) container);
}
}

// Default host may have changed
findDefaultHost();

if(log.isDebugEnabled()) {
log.debug(sm.getString("mapperListener.registerHost",
host.getName(), domain, service));
}
}

​ 先是直接调用mapper.addHost将当前的Host注册到Mapper中去,具体的注册过程代码就不贴出来了,就是维护了一个MappedHost的数组用于保存这种映射关系。需要注意的是这里的MappedHost数组是排序的,好处是在路由查找的时候可以进行二分法来提高效率

注册 Context

​ 接下来就是遍历该Host下的Context并依次注册过程,我们看看registerContext(context)的方法又有哪些不同,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void registerContext(Context context) {

String contextPath = context.getPath();
if ("/".equals(contextPath)) {
contextPath = "";
}
Host host = (Host)context.getParent();

WebResourceRoot resources = context.getResources();
String[] welcomeFiles = context.findWelcomeFiles();
List<WrapperMappingInfo> wrappers = new ArrayList<>();

for (Container container : context.findChildren()) {
prepareWrapperMappingInfo(context, (Wrapper) container, wrappers);

if(log.isDebugEnabled()) {
log.debug(sm.getString("mapperListener.registerWrapper",
container.getName(), contextPath, service));
}
}

mapper.addContextVersion(host.getName(), host, contextPath,
context.getWebappVersion(), context, welcomeFiles, resources,
wrappers);

if(log.isDebugEnabled()) {
log.debug(sm.getString("mapperListener.registerContext",
contextPath, service));
}
}

​ 其实和registerHost的流程是差不多的,在addContextVersion中只不过多了一个查找对应 Host 的过程,关于ContextVersion的概念前面已经提过了,大部分情况下是可以解理为一个应用的。

注册 Wrapper

​ 看到这里我们惊奇地发现整个注册过程结束了,为什么没有看到具体的ServletWrapper的注册过程?如果真的的至此结束,请求是如何找到对应的Servlet的?很显然还有其它的操作未完成。

​ 其实Wrapper的注册过程是在监听器过程中完成的,还记得我们之前提到的监听器层次模型,以及通过addListeners方法添加的监听器吗?我们现在看看这些监听器是做什么的。

​ 在此,我们先回顾一下在Tomcat 源码解读——启动篇服务启动 `Server.start()这一节中关于扫描web.xml并添加Servlet的过程。关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void addServletMappingDecoded(String pattern, String name,
boolean jspWildCard) {
// Validate the proposed mapping
if (findChild(name) == null)
throw new IllegalArgumentException
(sm.getString("standardContext.servletMap.name", name));
String adjustedPattern = adjustURLPattern(pattern);
if (!validateURLPattern(adjustedPattern))
throw new IllegalArgumentException
(sm.getString("standardContext.servletMap.pattern", adjustedPattern));

// Add this mapping to our registered set
synchronized (servletMappingsLock) {
String name2 = servletMappings.get(adjustedPattern);
if (name2 != null) {
// Don't allow more than one servlet on the same pattern
Wrapper wrapper = (Wrapper) findChild(name2);
wrapper.removeMapping(adjustedPattern);
}
servletMappings.put(adjustedPattern, name);
}
Wrapper wrapper = (Wrapper) findChild(name);

// 在这里添加 Servlet 的映射关系
wrapper.addMapping(adjustedPattern);

fireContainerEvent("addServletMapping", adjustedPattern);
}

​ 我们进入这个addMapping方法看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void addMapping(String mapping) {

mappingsLock.writeLock().lock();
try {
mappings.add(mapping);
} finally {
mappingsLock.writeLock().unlock();
}

// 发布映射关系添加的事件
if(parent.getState().equals(LifecycleState.STARTED))
fireContainerEvent(ADD_MAPPING_EVENT, mapping);

}

​ 除了常规的添加 Servlet 映射关系外还有非常重要的一点是发布了一个事件:ADD_MAPPING_EVENT,这个事件的刚好就是为了今天我们讲解的Wrapper注册过程服务的。在MapperListener的事件监听代码中对于这个事件的处理如下(省略了其它事件的处理代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void containerEvent(ContainerEvent event) {
...
if (Wrapper.ADD_MAPPING_EVENT.equals(event.getType())) {
// Handle dynamically adding wrappers
Wrapper wrapper = (Wrapper) event.getSource();
Context context = (Context) wrapper.getParent();
String contextPath = context.getPath();
if ("/".equals(contextPath)) {
contextPath = "";
}
String version = context.getWebappVersion();
String hostName = context.getParent().getName();
String wrapperName = wrapper.getName();
String mapping = (String) event.getData();
boolean jspWildCard = ("jsp".equals(wrapperName)
&& mapping.endsWith("/*"));

// 注册 ServletWrapper 到 Mapper 中
mapper.addWrapper(hostName, contextPath, version, mapping, wrapper,
jspWildCard, context.isResourceOnlyServlet(wrapperName));
}
...
}

​ 可以看到这里解析了这个ServletMapping相关的信息后,将这个映射关系注册到了Mapper中去了,具体的过程在mapper.addWrapper中,我们看看这个过程又是怎样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
protected void addWrapper(ContextVersion context, String path,
Wrapper wrapper, boolean jspWildCard, boolean resourceOnly) {

synchronized (context) {
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("*.")) {
// 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("/")) {
// Default wrapper
MappedWrapper newWrapper = new MappedWrapper("", wrapper,
jspWildCard, resourceOnly);
context.defaultWrapper = newWrapper;
} else {
// 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;
}
}
}
}

​ 同样,根据 前缀后缀默认值 以及 精确匹配 的几种不同场景,维护一个排序好的 MappedWrapper数组。

总结

​ 本文从Mapper的概念模型开始,介绍了Tomcat在接收到用户请求时的路由过程,以及这个路由表的建设过程,了解到HostContext以及Wrapper的映射关系。同时也介绍了多ContextVersion的来历,以及Tomcat是如何处理多版本的应用路由规则。