本篇文章将结合源代码介绍servlet-mapping的映射机制
1 引言
在Web应用中,经常会涉及到字符串匹配的问题。比如使用AbstractAnnotationConfigDispatcherServletInitializer
的方式启动Web应用时,我们需要重写这个方法:
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
这里面的/
是啥意思?为什么有时候还会出现/*
和/**
的写法,这些都是用来干啥的?
我们下面便来回答这个问题。
2 Tomcat核心概念
我们先对Tomcat的核心概念进行介绍,Tomcat中主要有如下这些核心概念:
-
Server
:代表整个Tomcat的服务器,StandardServer
是它的一个标准实现; -
Service
:由一个或多个Connector
组成的一组服务,这组服务共享同一个Engine
,用于处理HTTP请求,StandardService
是它的一个标准实现; -
Engine
:是用于处理整个服务器响应的容器对象,StandardEngine
是它的一个标准实现; -
Host
:是一个处理特定请求的虚拟容器对象,StandardHost
是它的一个标准实现; -
Context
:是一个表示Servlet上下文的容器对象,StandardContext
是它的一个标准实现; -
ServletContext
:定义一系列和容器交互的方法的对象,这才是我们在Spring Web应用常说的servlet上下文对象; -
Connector
:对网络连接的抽象,表示一个网络连接; -
ProtocolHandler
:表示一个协议,比如HTTP11Protocol
指1.1版本的HTTP协议; -
AbstractEndpoint
:用于建立网络请求,Tomcat中使用的是它的一个名为JIoEndpoint
实现类,是阻塞类型的IO对象。
这些对象相互引用组成了整个Tomcat服务器。
Server
包含一个或多个Service
,在Tomcat框架中,对服务的抽象是由Service
完成的。
Service
中包含一个或多个Connector
,这说明一个Tomcat进程可以启用多个监听端口(一般应用中只开一个端口,比如8080)。
Service
中包含一个Engine
对象,Engine
是处理请求的引擎。Service
中只能有一个Engine
对象。这说明同一个Service
下,不管有多少个Connector
,也不管这些Connect
实际启用了多少端口,但最终请求都会由同一个Engine
对象来处理。
Engine
中包含一个或多个Host
,Host
可以理解是一组Servlet的集合。所以同一个Engine
下面可能会有多个不同的Host
的。
Host
下面可以包含一个或多个Context
对象,一个Context
对象可以简单认为就是一个Servlet,Servlet是真正处理HTTP请求的实体,不同Servlet处理不同类型的请求(通常由请求的url地址区分)
从上面的关系可以看出,Tomcat本身还是非常灵活的,在各个层级都能支持自定义配置。但是这种灵活,实际现实中关注的人极少。目前真正能做到对Tomcat做深度定制化的更是少之又少。
Connector
对象内部包含了一个ProtocolHandler
对象,ProtocolHandler
对象包含了一个AbstractEndpoint
对象。真正打开网络端口并进行监听的其实是AbstractEndpoint
的具体实现类。
上面这些,便是Tomcat中的核心概念,以及这些对象是如何构建起完整的Web容器的。
3 Tomcat请求处理过程
根据上面所述,Tomcat启动后,会由AbstractEndpoint
的具体实现类打开网络端口,并持续在端口上监听网络请求。这部分由JIoEndpoint$Acceptor
类来完成,相关代码如下:
class Acceptor extends AbstractEndpoint.Acceptor {
@Override
public void run() {
while (running) {
...
try {
Socket socket = null;
try {
socket = serverSocketFactory.acceptSocket(serverSocket);
} catch (IOException ioe) {
// 处理异常情况
}
if (running && !paused && setSocketOptions(socket)) {
if (!processSocket(socket)) { // 处理请求
// Handler Exception
}
} else {
// 处理异常情况
}
} catch (...) {
// 处理异常情况
}
}
}
}
这段代码中,我去掉了很多无关紧要的部分。主要的逻辑是通过调用serverSocketFactory.acceptSocket(serverSocket)
将线程阻塞。等到有网络请求到来后,执行processSocket(socket)
方法启动处理。
后续的处理过程是根据socket数据,构造request和response,然后根据上面提到的各个类之间的关系,最终找到能处理该请求的servlet对象。
在根据request查询能处理该请求的servlet对象的过程中,有这样一个方法:
private final void internalMapWrapper(Mapper.ContextVersion contextVersion, CharChunk path, MappingData mappingData) throws Exception {
...
// Rule 1 -- Exact Match
Mapper.Wrapper[] exactWrappers = contextVersion.exactWrappers;
internalMapExactWrapper(exactWrappers, path, mappingData);
// Rule 2 -- Prefix Match
boolean checkJspWelcomeFiles = false;
Mapper.Wrapper[] wildcardWrappers = contextVersion.wildcardWrappers;
...
// Rule 3 -- Extension Match
Mapper.Wrapper[] 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] == '/');
}
...
}
/* 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;
...
}
// Rule 7 -- Default servlet
if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
...
}
path.setOffset(pathOffset);
path.setEnd(pathEnd);
}
上面这段代码就是重点了,也是导致这几个配置/
、/*
、/**
产生不同效果的根本原因。
4 后续
这片文章就到这吧,下一篇文章中,我们将对上面那段代码进行详细解释。并实际测试验证这几个配置/
、/*
、/**
会产生什么效果,以及产生这些效果的原因。