【分析】
在Servlet
中,规定的Url
组成为:ip:port/context/servlet-path
因此,想要找到对应的Servlet
,必须首先解析context,然后再找到对应的servlet-path
同时,servlet协议规定,servlet-path有4中不同的匹配规则:
- 精确匹配
- 路径匹配
- 扩展匹配
- 默认匹配
因此,需要不同的匹配方式
【Tomcat】
Tomcat
底层实现确实是上面那样,不过在实际的实现中,更加复杂。在Servlet
与Tomcat
配置中,可一个配置多个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
启动的时候,会调用Mapper
的addHost()
添加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
,然后根据Request
的uri
一一匹配Host
和Context
,如果匹配成功,则进入internalMapWrapper
这里可以看到,很多时候ContextName是允许为空的,那应该怎么配置才能让ContextName为空呢?
答案是在server.xml中添加Context配置
<Context path="" docBase="F:\tomcat\webapps\study" debug="0"> </Context>
最重要的便是将
path
设置为空即可,这里path
还可以自定义其他名字。为什么可以这样配置呢?
代码比较多,这里简单说下:
Tomcat
中,负责扫描Context
的模块为HostConfig
,HostConfig
所执行的对象为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
包名或者文件夹名赋值的,当初始化完成便会加入Host
的child
,而如果直接在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
优先查找