欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
介绍完Catalina中Web应用的加载过程,本节再来看一下它是如何处理Web应用请求的。
1. 总体过程
Tomcat通过org.apache.tomcat.util.htp.mapper.Mapper维护请求链接与Host、Context、Wrapper等Container的映射
。同时,通过org.apache.catalina.connector.MapperListener监听器监听所有的Host、Context、Wrapper组件
,在相关组件启动、停止时注册或者移除相关映射
。
此外,通过org.apache.catalina.connector.CoyoteAdapter将Connector与Mapper、Container联系 起来
。当Connector接收到请求后,首先读取请求数据,然后调用CoyoteAdapter.service()方法完成请求处理。请求是如何到CoyoteAdapter.service()这里,我们在后面Coyote篇详细讲解。如下:
// CoyoteAdapte处理请求
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
if (request == null) {
// Create objects
// 1. 根据Connector的请求(org,apache.coyote.Request)和响应(org.apache.coyote.Response)
//对象创建Servlet请求(org.apache.catalina.connector.Request)和响应(org.apache,
//catalina.connector.Response
request = connector.createRequest();
// HttpServletRequest里面有真正的request
request.setCoyoteRequest(req);
response = connector.createResponse();
response.setCoyoteResponse(res);
// Link objects
request.setResponse(response);
response.setRequest(request);
// Set as notes
req.setNote(ADAPTER_NOTES, request);
res.setNote(ADAPTER_NOTES, response);
// Set query string encoding
req.getParameters().setQueryStringCharset(connector.getURICharset());
}
if (connector.getXpoweredBy()) {
response.addHeader("X-Powered-By", POWERED_BY);
}
boolean async = false;
boolean postParseSuccess = false;
req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get());
req.setRequestThread();
try {
// Parse and set Catalina and configuration specific
// request parameters 后置解析请求
// 2.转换请求参数并完成请求映射。
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//check valves if we support async
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container 调用容器,拿到Engine的pipeline
// 3.得到当前Enginef的第一个Valve并执行(invoke),以完成客户端请求处理。
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}
// 4. 如果是异步请求
if (request.isAsync()) {
async = true;
// 4.1 获取请求读取事件监听器ReadListener
ReadListener readListener = req.getReadListener();
if (readListener != null && request.isFinished()) {
// Possible the all data may have been read during service()
// method so this needs to be checked here
ClassLoader oldCL = null;
try {
oldCL = request.getContext().bind(false, null);
if (req.sendAllDataReadEvent()) {
// 4.2 如果请求读取已经结束,触发ReadListener.onAllDataRead()
req.getReadListener().onAllDataRead();
}
} finally {
request.getContext().unbind(false, oldCL);
}
}
Throwable throwable =
(Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
// If an async request was started, is not going to end once
// this container thread finishes and an error occurred, trigger
// the async error process
if (!request.isAsyncCompleting() && throwable != null) {
request.getAsyncContextInternal().setErrorState(throwable, true);
}
} else {
// 5.如果为同步请求
// Flush并关闭请求输入流
request.finishRequest();
// Flush 并关闭响应输出流
response.finishResponse();
}
} catch (IOException e) {
// Ignore
} finally {
...
}
}
复制代码
CoyoteAdapter..servicef的具体处理过程如下(只列出主要步骤):
-
根据Connector的请求(org.apache.coyote.Request)和响应(org.apache.coyote.Response)对象
创建Servlet请求
(org.apache.catalina.connector.Request)和响应(org.apache.catalina.connector.Response)。 -
转换请求参数并完成请求映射。
- 请求UI解码,初始化请求的路径参数。
- 检测URI是否合法,如果非法,则返回响应码400。
- 请求映射,具体算法参见3.5,2节,映射结果保存到org.apache.catalina.connector.Request.mappingData,类型为org.apache..tomcat.util.http.mapper.MappingData,请求映射处理最终会根据URI定位到一个有效的Wrapper。
- 如果映射结果MappingDatal的redirectPath/属性不为空(即为重定向请求),则调用org. apache.catalina.connector.Response.sendRedirect发送重定向并结束,
- 如果当前Connector.不允许追踪(allowTrace为false)且当前请求的Method为TRACE,则返回响应码405。
- 执行连接器的认证及授权。
-
得到当前Engine的第一个Valve并执行invoke
,以完成客户端请求处理
。
注意:由于Pipeline和Valve为职责链模式,因此执行第一个Valve即保证了整个Valve链的执行。
- 如果为异步请求:
- 获得请求读取事件监听器(ReadListener);
- 如果请求读取已经结束,触发ReadListener.onAllDataRead。.
- 如果为同步请求:
- Flush并关闭请求输入流;
- Fush并关闭响应输出流。
2. 请求映射
请求映射过程具体分为两部分,一部分位于CoyoteAdapter.postParseRequest,负责根据请求路径匹配的结果,按照会话等信息获取最终的映射结果(因为只根据请求路径匹配,结果可能为多个)
。第二部分位于Mapper.map,负责完成具体的请求路径的匹配
。
2.1 CoyoteAdapter.postRequest()
该方法中的映射处理算法如图所示:
从图中可以看出,请求映射算法非常复杂(该图还不包含请求路径的匹配一加粗部分),源码为:
protected boolean postParseRequest(org.apache.coyote.Request req, Request request,
org.apache.coyote.Response res, Response response) throws IOException, ServletException {
...
MessageBytes decodedURI = req.decodedURI();
if (undecodedURI.getType() == MessageBytes.T_BYTES) {
// Copy the raw URI to the decodedURI
decodedURI.duplicate(undecodedURI);
// Parse (and strip out) the path parameters
parsePathParameters(req, request);
// URI decoding
// %xx decoding of the URL
try {
// 2.1 请求URI解码,初始化请求的路径参数。
req.getURLDecoder().convert(decodedURI.getByteChunk(), connector.getEncodedSolidusHandlingInternal());
} catch (IOException ioe) {
response.sendError(400, "Invalid URI: " + ioe.getMessage());
}
// Normalization
if (normalize(req.decodedURI())) {
// Character decoding
convertURI(decodedURI, request);
// Check that the URI is still normalized
// Note: checkNormalize is deprecated because the test is no
// longer required in Tomcat 10 onwards and has been
// removed
// 2.2 检测URI是否合法,如果非法,则返回响应码400。
if (!checkNormalize(req.decodedURI())) {
response.sendError(400, "Invalid URI");
}
} else {
response.sendError(400, "Invalid URI");
}
} else {
/* The URI is chars or String, and has been sent using an in-memory
* protocol handler. The following assumptions are made:
* - req.requestURI() has been set to the 'original' non-decoded,
* non-normalized URI
* - req.decodedURI() has been set to the decoded, normalized form
* of req.requestURI()
*/
decodedURI.toChars();
// Remove all path parameters; any needed path parameter should be set
// using the request object rather than passing it in the URL
CharChunk uriCC = decodedURI.getCharChunk();
int semicolon = uriCC.indexOf(';');
if (semicolon > 0) {
decodedURI.setChars(uriCC.getBuffer(), uriCC.getStart(), semicolon);
}
}
// Request mapping.
// 2.3 请求映射,映射结果保存到org.apache.catalina.connector.Request..
//mappingData,类型为org.apache.tomcat.util.htp,mapper.MappingData,请求映射处理最终
//会根据URI定位到一个有效的Wrapper。
MessageBytes serverName;
if (connector.getUseIPVHosts()) {
serverName = req.localName();
if (serverName.isNull()) {
// well, they did ask for it
res.action(ActionCode.REQ_LOCAL_NAME_ATTRIBUTE, null);
}
} else {
serverName = req.serverName();
}
// Version for the second mapping loop and
// Context that we expect to get for that version
// (1) 定义三个局部变量
String version = null; // 需要匹配的版本号,初始化为空,也就是匹配所有版本
Context versionContext = null; // 用于暂存按照会话ID匹配的Context,初始化为空
boolean mapRequired = true; // 是否需要映射,用于控制映射匹配循环,初始化为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();
}
// (2) 通过一个循环(mapRequired==true)来处理映射匹配,因为只通过一次处理并不能确保
//得到正确结果。
while (mapRequired) {
// This will map the the latest version by default
// (3)在循环第(1)步,调用Mapper.map()方法按照请求路径进行匹配,参数为serverName、url、
//version。因为version初始化时为空,所以第一次执行时,所有匹配该请求路径的Context均会返
//回,此时MappingData.contexts中存放了所有结果,而MappingData.contextr中存放了最新版本
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
// If there is no context at this point, either this is a 404
// because no ROOT context has been deployed or the URI was invalid
// so no context could be mapped.
// (4)如果没有任何匹配结果,那么返回404响应码,匹配结束。
if (request.getContext() == null) {
// Allow processing to continue.
// If present, the rewrite Valve may rewrite this to a valid
// request.
// The StandardEngineValve will handle the case of a missing
// Host and the StandardHostValve the case of a missing Context.
// If present, the error reporting valve will provide a response
// body.
return true;
}
// Now we have the context, we can parse the session ID from the URL
// (if any). Need to do this before we redirect in case we need to
// include the session id in the redirect
String sessionID;
// (5)尝试从请求的URL、Cookie、SSL会话获取请求会话D,并将mapRequired设置为false(当
//第(3)步执行成功后,默认不再执行循环,是否需要重新执行由后续步骤确定)。
if (request.getServletContext().getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.URL)) {
// Get the session ID if there was one
sessionID = request.getPathParameter(
SessionConfig.getSessionUriParamName(
request.getContext()));
if (sessionID != null) {
request.setRequestedSessionId(sessionID);
request.setRequestedSessionURL(true);
}
}
// Look for session ID in cookies and SSL session
try {
parseSessionCookiesId(request);
} catch (IllegalArgumentException e) {
// Too many cookies
if (!response.isError()) {
response.setError();
response.sendError(400);
}
return true;
}
parseSessionSslId(request);
sessionID = request.getRequestedSessionId();
// 将mapRequired设置为false
mapRequired = false;
// (6)如果version不为空,且MappingData.context与versionContext相等,即表明当前匹配结果
//是会话查询的结果,此时不再执行第(7)步。当前步骤仅用于重复匹配,第一次执行时,version
//和versionContext均为空,所以需要继续执行第(7)步,而重复执行时,已经指定了版本,可得到
//唯一的匹配结果。
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
//(7)如果不存在会话ID,那么第(3)步匹配结果即为最终结果(即使用匹配的最新版本)。否则,
//从MappingData.contexts中查找包含请求会话ID的最新版本,查询结果分如下情况。
if (contexts != null && sessionID != null) {
// Find the context associated with the session
// 没有查询结果(即表明会话D过期)或者查询结果与第(3)步匹配结果相等,这时同样使
//用的是第(3)步的匹配结果。
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?
// 有查询结果且与第(3)步匹配结果不相等(表明当前会话使用的不是最新版本),将version
//设置为查询结果的版本,versionContext设置为查询结果,将mapRequired设置为true,重
//置MappingData。此种情况下,需要重复执行第(3)步(之所以需要重复执行,是因为虽然
//通过会话ID查询到了合适的Context,但是MappingData中记录的Wrapperl以及相关的路径
//信息仍属于最新版本Context,是错误的),并明确指定匹配版本。指定版本后,第(3)步应
//只存在唯一的匹配结果。
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;
}
}
}
}
//(8) 如果mapRequired为false(即已找到唯一的匹配结果),但匹配的Context状态为暂停(如
//正在重新加载),此时等待l秒钟,并将mapRequiredi设置为true,重置MappingData。此种情况下,
//需要进行重新匹配,直到匹配到一个有效的Context或者无任何匹配结果为止。
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;
}
}
// Possible redirect
// 2.4 如果映射结果MappingData的redirectPath/属性不为空(即为重定向请求),则调用org,
//apache,catalina.connector.Response,sendRedirect.发送重定向并结束。
MessageBytes redirectPathMB = request.getMappingData().redirectPath;
if (!redirectPathMB.isNull()) {
String redirectPath = URLEncoder.DEFAULT.encode(
redirectPathMB.toString(), StandardCharsets.UTF_8);
String query = request.getQueryString();
if (request.isRequestedSessionIdFromURL()) {
// This is not optimal, but as this is not very common, it
// shouldn't matter
redirectPath = redirectPath + ";" +
SessionConfig.getSessionUriParamName(
request.getContext()) +
"=" + request.getRequestedSessionId();
}
if (query != null) {
// This is not optimal, but as this is not very common, it
// shouldn't matter
redirectPath = redirectPath + "?" + query;
}
response.sendRedirect(redirectPath);
request.getContext().logAccess(request, response, 0, true);
return false;
}
// Filter trace method
// 2.5 如果当前Connector不允许追踪(allowTrace为false)且当前请求的Method为TRACE,则
//返回响应码405。
if (!connector.getAllowTrace()
&& req.method().equalsIgnoreCase("TRACE")) {
Wrapper wrapper = request.getWrapper();
String header = null;
if (wrapper != null) {
String[] methods = wrapper.getServletMethods();
if (methods != null) {
for (String method : methods) {
if ("TRACE".equals(method)) {
continue;
}
if (header == null) {
header = method;
} else {
header += ", " + method;
}
}
}
}
if (header != null) {
res.addHeader("Allow", header);
}
response.sendError(405, "TRACE method is not allowed");
// Safe to skip the remainder of this method.
return true;
}
// 2.6 执行连接器的认证及授权。
doConnectorAuthenticationAuthorization(req, request);
return true;
}
复制代码
接下来将对每一步做一个详细介绍:
-
定义3个局部变量。
- version:需要匹配的版本号,初始化为空,也就是匹配所有版本。
- versionContext:用于暂存按照会话ID匹配的Context,初始化为空。
- mapRequired:是否需要映射,用于控制映射匹配循环,初始化为true.
-
通过一个循环(mapRequired==true)来处理映射匹配,因为只通过一次处理并不能确保得到正确结果(第(3)步至第(8)步均为循环内处理)。
-
在循环第(1)步,
调用Mapper.map()方法按照请求路径进行匹配
,参数为serverName、url、version。因为version初始化时为空,所以第一次执行时,所有匹配该请求路径的Context均会返回,此时MappingData.contexts中存放了所有结果,而MappingData.contextr中存放了最新版本。 -
如果没有任何匹配结果,那么返回404响应码,匹配结束。
-
尝试
从请求的URL、Cookie、SSL会话获取请求会话ID
,并将mapRequired设置为false(当第3步执行成功后,默认不再执行循环,是否需要重新执行由后续步骤确定)。 -
如果version不为空,且MappingData.context与versionContext相等
,即表明当前匹配结果是会话查询的结果
,此时不再执行第(7)步。当前步骤仅用于重复匹配,第一次执行时,version和versionContext均为空,所以需要继续执行第(7)步,而重复执行时,已经指定了版本,可得到唯一的匹配结果。 -
如果不存在会话ID,那么第(3)步匹配结果即为最终结果(即使用匹配的最新版本)。否则,从MappingData.contexts中查找包含请求会话ID的最新版本,查询结果分如下情况。
- 没有查询结果(即表明会话D过期)或者查询结果与第(3)步匹配结果相等,这时同样使用的是第(3)步的匹配结果。
- 有查询结果且与第(3)步匹配结果不相等(表明当前会话使用的不是最新版本),将version设置为查询结果的版本,versionContext设置为查询结果,将mapRequired设置为true,重置MappingData。此种情况下,需要重复执行第3步(之所以需要重复执行,是因为虽然 通过会话ID查询到了合适的Context,但是MappingData中记录的Wrapperl以及相关的路径信息仍属于最新版本Context,是错误的),并明确指定匹配版本。指定版本后,第(3)步应只存在唯一的匹配结果。
-
如果mapRequired为false(即已找到唯一的匹配结果),但匹配的Context状态为暂停(如正在重新加载),此时等待1秒钟,并将mapRequired设置为true,重置MappingData。此种情况下,需要进行重新匹配,直到匹配到一个有效的Context或者无任何匹配结果为止。
通过上面的处理,Tomcat确保得到的Context符合如下要求。 - 匹配请求路径。 - 如果有有效会话,则为包含会话的最新版本。 - 如果没有有效会话,则为所有匹配请求的最新版本。 - Context必须是有效的(非暂停状态)。
2.2 Mapper.map
以上我们只讲解了Tomcat对于请求匹配结果的处理,接下来再看一下请求路径的具体匹配算法(即上图中加粗的部分)。
2.2.1 Mapper结构图
在讲解算法之前,有必要先了解一下Mappert的静态结构,这有助于我们加深对算法的理解。Mapper的静态结构如图所示:
-
Mapper对于Host、Context、Wrapper均提供了对应的封装类,因此描述算法时,我们用MappedHost、MappedContext、MappedWrapper表示其封装对象,而用Host、Context、Wrapper表示Catalina中的组件。
-
MappedHost支持封装Host缩写。当封装的是一个Host缩写时,realHost即为其指向的真实Host封装对象。当封装的是一个Host且存在缩写时,aliases即为其对应缩写的封装对象
-
为了支持Context的多版本,Mapper提供了MappedContext、ContextVersion两个封装类。当注册一个Context时,MappedContext名称为Context的路径,并且通过一个ContextVersion列表保存所有版本的Context。ContextVersion保存了单个版本的Context,名称为具体的版本号。
-
ContextVersion保存了一个具体Context及其包含的Wrapper封装对象,包括默认Wrapper、精确匹配的Wrapper、通配符匹配的Wrapper、通过扩展名匹配的Wrapper。
-
MappedWrapper保存了具体的Wrapper。
-
所有注册组件按层级封装为一个MappedHost列表,并保存到Mapper。
在Mapper中,每一类Container按照名称的ASCII正序排序
(注意排序规则,这会影响一些特殊情况下的匹配结果)。
以Context为例,下列名称均合法:/abbb/a、/abbb、/abb、/Abbb、/Abbb/a、/Abbb/ab,而在Mapperr中,它们的顺序为:/Abbb、/Abbb/a、/Abbb/ab、/abb、/abbb、/abbb/a,无论以何种顺序添加。
Mapper.map()方法的请求映射结果为org.apache.tomcat.util.http.mapper.MappingData对 象,保存在请求的mappingData属性中。
org.apache.tomcat.util.http.mapper.MappingData的结构如下,具体含义参见注释:
public class MappingData {
// 匹配的Host
public Host host = null;
// 匹配的Context
public Context context = null;
// Context路径中'/'的数量
public int contextSlashCount = 0;
// 匹配的Context列表,只用于匹配过程,并非最终使用结果
public Context[] contexts = null;
// 匹配的Wrapper
public Wrapper wrapper = null;
// 对于JspServlet,其对应的匹配pattern是否包含通配符
public boolean jspWildCard = false;
@Deprecated
public final MessageBytes contextPath = MessageBytes.newInstance(); // Context路径
public final MessageBytes requestPath = MessageBytes.newInstance(); // 相对于Context的请求路径
public final MessageBytes wrapperPath = MessageBytes.newInstance(); // Servlet路径
public final MessageBytes pathInfo = MessageBytes.newInstance(); // 相对于Servlet的请求路径
public final MessageBytes redirectPath = MessageBytes.newInstance();// 重定向路径
...
}
复制代码
对于contexts属性
,主要使用于多版本Web应用同时部署的情况
,此时可以匹配请求路径的Context存在多个,需要进一步处理。而context属性始终存放的是匹配请求路径的最新版本(注意,匹配请求的最新版本并不代表是最后的匹配结果,具体参见算法讲解)。
Mapper.map的具体算法如图:
Mapper.map()源码:
public void map(MessageBytes host, MessageBytes uri, String version,
MappingData mappingData) throws IOException {
// (1)一般情况下,需要查找的Host名称为请求的serverName。.但是,如果没有指定Host名称,
//那么将使用默认Host名称。
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);
}
private final void internalMap(CharChunk host, CharChunk uri,
String version, MappingData mappingData) throws IOException {
// 如果mappingData.host不为空,说明mappingData是有问题的。抛出异常
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,可以分为三步
* 1. 根据当前的hostName查找具体的host
* 2. 如果没有找到的话,就去掉第一个 "."之前的部分,再次查找。
* 例如,当输入时 abc.bde.ef.com,时,如果没有找到相应匹配的host时,则查找bde.ef.com。
* 因为看到在构建mapper,增加host上,当有会把host name的前导* 去掉的。
* 3.如果还没有找到,则设置为 默认的host
*/
// (2)按照host名称查询Mapper.Host(忽略大小写),如果没有找到匹配结果,且默认Host名称不
// 为空,则按照默认Host名称精确查询。如果存在匹配结果,将其保存到MappingData的host属性。
// 按照host名称,精确查找Mapper.Host,忽略大小写
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);
// 按照host名称,精确查找Mapper.Host,不忽略大小写
mappedHost = exactFindIgnoreCase(hosts, host);
} finally {
// Make absolutely sure this gets reset
host.setOffset(offset);
}
}
if (mappedHost == null) {
mappedHost = defaultHost;
if (mappedHost == null) {
return;
}
}
}
// 将查找到的Host更新到MappingData
mappingData.host = mappedHost.object;
if (uri.isNull()) {
// Can't map context or wrapper without a uri
return;
}
uri.setLimit(-1);
// Context mapping
// (3)按照url查找MapperdContextl最大可能匹配的位置pos(只限于第(2)步查找到的MappedHost
//下的MappedContext)。之所以如此描述,与Tomcat的查找算法相关。
ContextList contextList = mappedHost.contextList;
MappedContext[] contexts = contextList.contexts;
// 按照url查找Mapper.Context最大可能匹配的位置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等于context名称或者以context名称+ '/'开头
if (uri.startsWith(context.name)) {
length = context.name.length();
if (uri.getLength() == length) {
// true 跳出循环
found = true;
break;
} else if (uri.startsWithIgnoreCase("/", length)) {
found = true;
break;
}
}
// (4)当第(3)步查找的pos≥O时,得到对应的MappedContext,.如果url与MappedContext的路径
//相等或者url以MappedContexth路径+“”开头,均视为找到匹配的MappedContext。否则,循环执
// 行第(4)步,逐渐降低精确度以查找合适的MappedContext(具体可参见第(3)步的例子)。
// 去除url最后一个'/'之后的部分
if (lastSlash == -1) {
lastSlash = nthSlash(uri, contextList.nesting + 1);
} else {
lastSlash = lastSlash(uri);
}
uri.setEnd(lastSlash);
pos = find(contexts, uri);
}
uri.setEnd(uriEnd);
//(5)如果循环结束后仍未找到合适的MappedContext,.那么会判断第0个MappedConext的名称
//是否为空字符串。如果是,则将其作为匹配结果(即使用默认MappedContext)。
if (!found) {
// 如果第0个Mapper.Context名称为空字符串,则Context=context[0]
if (contexts[0].name.equals("")) {
context = contexts[0];
} else {
context = null;
}
}
// 还没有找到,返回
if (context == null) {
return;
}
// (6)前面曾讲到MappedContext存放了路径相同的所有版本的Context(ContextVersion),因此
//在第(S)步结束后,还需要对MappedContexth版本进行处理。如果指定了版本号,则返回版本号相
//等的Context Version,否则返回版本号最大的。最后,将Context Version中维护的Context保存到
//MappingData中。
// 下面更新MappingData的contextPath、contexts、context
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
// (7)如果Context当前状态为有效(由图3-6可知,当Context处于暂停状态时,将会重新按照url
//映射,此时MappedWrapperf的映射无意义),则映射对应的MappedWrapper。
// 如果context有效,查找Wrapper
if (!contextVersion.isPaused()) {
internalMapWrapper(contextVersion, uri, mappingData);
}
}
复制代码
为了简化流程图,部分处理细节并未展开描述(如查找Wrapper),因此我们仍对每一步做一个详细的讲解:
- 一般情况下,需要查找的Host名称为请求的serverName。但是,如果没有指定Host名称,那么将使用默认Host名称。
注意:默认Host名称通过
按照Engine的defaultHost属性
查找其Host子节点获取。
查找规则:Host名称与defaultHost相等或Host缩写名与defaultHost相等(忽略大小写)此处需要注意一个问题,由于Container在维护子节点时,使用的是HashMap,因此在得到其子节点列表时,顺序与名称的哈希码相关。例如,如果Engine中配置的defaultHost为“Server001”,而Tomcat中配置了“SERVER001”和“Server001”两个Host,此时默认Host名称为“SERVER001”。而如果我们将“Server001”换成“server001”,则结果就变成了“server001”。当然,实际配置过程中,应彻底避免此种命名。
- 按照host名称查询Mapper.Host(忽略大小写),如果没有找到匹配结果,且默认Host名称不为空,则按照默认Host名称精确查询。如果存在匹配结果,将其保存到MappingData的host属性。
此处有时候会让人产生疑惑,第(I)步在没有指定host名称时,已将host名称设置为默认Host名称,为什么第(2)步仍需要按照默认Host名称查找。这主要满足如下场景:当host不为空,且为无效名称时,Tomcat将会尝试返回默认Host,而非空值。
-
按照url查找MapperdContext最大可能匹配的位置pos
(只限于第(2)步查找到的MappedHost下的MappedContext)。之所以如此描述,与Tomcat的查找算法相关。- 在Mapper中所有Container是有序的((按照名称的ASCI正序排列,因此Tomcat采用二分法进行查找。其返回结果存在如下两种情况。
-
-1:表明url比当前MappedHost下所有的MappedContext的名称都小,也就是没有匹配的MappedContext.
-
≥0:可能是精确匹配的位置,也可能是列表中比url小的最大值的位置。即使没有精确匹配,也不代表最终没有匹配项,这需要进一步处理。
-
如果比较难以理解,我们下面试举一例。例如我们配置了两个Context,路径分别为:myapp和myapp/appl,在Tomcat中,这两个是允许同时存在的。然后我们尝试输入请求路径htp:/127.0.0.1:8080 myapp/appl/index.jsp。此时url为/myapp/appl/index.jsp。很显然,url不可能和Context路径精确匹配,此时返回比其小的最大值的位置(即myapp/appl)。当Tomcat发现其非精确匹配时,会将url进行截取(裁取为myapp/appl)再进行匹配,此时将会精确匹配/myapp/appl。当然,如果我们输入的是htp:127.0.0.1:8080/myapp/app2/index.jsp,Tomcat将会继续截取,直到匹配到myapp。
-
由此可见,
Tomeat总是试图查找一个最精确的MappedContext
(如上例使用/myapp/appl,而非/myapp,尽管这两个都是可以匹配的)。
-
- 在Mapper中所有Container是有序的((按照名称的ASCI正序排列,因此Tomcat采用二分法进行查找。其返回结果存在如下两种情况。
-
当第(3)步查找的pos≥O时,得到对应的MappedContext,
如果url与MappedContext的路径相等或者url以MappedContext路径+“/”开头,均视为找到匹配的MappedContext
。否则,循环执行第(4)步,逐渐降低精确度以查找合适的MappedContext(具体可参见第(3)步的例子)。 -
如果循环结束后仍未找到合适的MappedContext,那么会判断第0个MappedConext的名称是否为空字符串。如果是,则将其作为匹配结果(即使用默认MappedContext)。
-
前面曾讲到MappedContext存放了路径相同的所有版本的Context(ContextVersion),因此在第(5)步结束后,还需要对MappedContext版本进行处理。如果指定了版本号,则返回版本号相等的ContextVersion,否则返回版本号最大的。最后,将ContextVersion中维护的Context保存到MappingData中。
-
如果Context当前状态为有效(由图3-6可知,当Context处于暂停状态时,将会重新按照url
映射,此时MappedWrapper的映射无意义),则映射对应的MappedWrapper。
2.3 MapperWrapper映射
我们知道ContextVersion中将MappedWrapper分为:默认Wrapper(defaultWrapper)、精确Wrapper(exactWrappers)、前缀加通配符匹配Wrapper(wildcardWrappers)和扩展名匹配Wrapper (extensionWrappers)。之所以分为这几类是因为它们之间是存在匹配优先级
的.
此外,在ContextVersion中,并非每一个Wrapper对应一个MappedWrapper对象
,而是每个url-pattern对应一个
。如果web.xml中的servlet-mapping配置如下:
<servlet-mapping>
<servlet-name>example</servlet-name>
<url-pattern>*.do</url-pattern>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
复制代码
那么,在ContextVersion中将存在两个MappedWrapper封装对象,分别指向同一个Wrapper实例。
将Wrapper添加到ContextVersion对应的MappedWrapper分类中,源码路径为: ContextConfig.configureContext()-> StandardContext.addServletMappingDecoded() -> wrapper.addMapping() -> fireContainerEvent(ADD_MAPPING_EVENT, mapping) -> MapperListener.containerEvent()->Mapper.addWrapper()
如下:
protected void addWrapper(ContextVersion context, String path,
Wrapper wrapper, boolean jspWildCard, boolean resourceOnly) {
synchronized (context) {
// 以'/*'开始的path加入到通配符wrapper当中
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("*.")) { // 以'*.'开始的path加入到后缀wrapper当中
// 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("/")) { // 设置默认的DefaultWrapper
// Default wrapper
MappedWrapper newWrapper = new MappedWrapper("", wrapper,
jspWildCard, resourceOnly);
context.defaultWrapper = newWrapper;
} else { // 到这里都还没匹配到的就是精确的wrapper了
// 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;
}
}
}
}
复制代码
Mapper按照如下规则将Wrapper添加到ContextVersion对应的MappedWrapper分类中去。
-
如果
url-pattern以“/*”结尾
,则为前缀加通配符匹配wildcardWrappers。此时,MappedWrapper的名称为url-pattern去除结尾的“/*”
。 -
如果url-pattern
以“*.”结尾
,则为扩展名匹配extensionWrappers。此时,MappedWrapper的名称为url-pattern去除开头的“*.”
。 -
如果url-pattern等于“/”,则为defaultWrapper。此时,MappedWrapper的名称为空字符串。
-
其他情况均为精确exactWrappers。如果url-pattern为空字符串,MappedWrapper的名称为“/”,否则为url-pattern的值。
MappedWrapper匹配源码:
private final void internalMapWrapper(ContextVersion contextVersion,
CharChunk path,
MappingData mappingData) throws IOException {
/**
* (1)依据url和Context路径计算MappedWrapper匹配路径。例如,如果Context路径为“/myapp”,
* url为“/myapp/app1/index.jsp”,那么MappedWrapper的匹配路径为“/app1/index.jsp”;如果url为
* “/myapp”,那么MappedWrapperl的匹配路径为“/”"。
*/
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
/**
* (2)先精确查找exactWrappers。
*/
MappedWrapper[] exactWrappers = contextVersion.exactWrappers;
internalMapExactWrapper(exactWrappers, path, mappingData);
// Rule 2 -- Prefix Match
/**
* (3)如果未找到,然后再按照前缀查找wildcardWrappers,.算法与MappedContext查找类似,
* 逐步降低精度。
*/
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
/**
* (4)如果未找到,然后按照扩展名查找extension Wrappers。
*/
MappedWrapper[] extensionWrappers = contextVersion.extensionWrappers;
if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
internalMapExtensionWrapper(extensionWrappers, path, mappingData,
true);
}
// Rule 4 -- Welcome resources processing for servlets
/**
* (5)如果未找到,则尝试匹配欢迎文件列表(web.xml中的welcome-file-list配置)。主要用于
* 我们输入的请求路径是一个目录而非文件的情况,如:htp:/127.0.0.1:8080/myapp/appl/。此时,
* 使用的匹配路径为“原匹配路径+welcome-file-ist中的文件名称”。欢迎文件匹配分为如下两步。
*/
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);
/**
* (5.1)对于每个欢迎文件生成的新的匹配路径,先查找exactWrappers,再查找wildcardWrappers。
* 如果该文件在物理路径中存在,则查找extension Wrappers,如extension Wrappers未找到,则使用defaultWrapper
*/
// 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
*/
/**
* (5.2) 对于每个欢迎文件生成的新的匹配路径,查找extensionWrappers
*/
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
/**
* (6)如果未找到,则使用默认MappedWrapper(通过conf/web.xml,即使Web应用不显式地进
* 行配置,也一定会存在一个默认的Wrapper)。因此,无论请求链接是什么,只要匹配到合适的
* Context,.那么肯定会存在一个匹配的Wrapper。
*/
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);
}
复制代码
接下来看一下MappedWrapper的详细匹配过程:
-
依据url和Context路径计算MappedWrapper匹配路径。例如,如果Context路径为“myapp”,url为“/myapp/appl/index.jsp”,那么MappedWrapper的匹配路径为“/appl/index.jsp”;如果url为“/myapp”,那么MappedWrapperl的匹配路径为“/"。
-
先精确查找exactWrappers。
-
如果未找到,然后再按照前缀查找wildcardWrappers,算法与MappedContexti查找类似,逐步降低精度。
-
如果未找到,然后按照扩展名查找extensionWrappers。
-
如果未找到,则尝试匹配欢迎文件列表(web.xml中的welcome-file-list配置)。主要用于我们输入的请求路径是一个目录而非文件的情况,如:http://127.0.0.1:8080/myapp/appl/。 此时,使用的匹配路径为“原匹配路径+welcome-file-list中的文件名称”。欢迎文件匹配分为如下两步。
- ①对于每个欢迎文件生成的新的匹配路径,先查找exactWrappers,再查找wildcardWrappers。如果该文件在物理路径中存在,则查找extension Wrappers,如extension Wrappers.未找到,则使用defaultWrapper
- ②对于每个欢迎文件生成的新的匹配路径,查找extension Wrappers。
- 注意在第①步中,只有当存在物理路径时,才会查找extension Wrappers,并在找不到时使用defaultWrapper,而第②步则不判断物理路径,直接通过extension Wrappers查找。按照这种方式处理,如果我们的配置如下:
- url-pattern配置为“*.do”;
- welcome-file-list包括index.do、index.html.
- 当我们输入的请求路径为htp:/127.0.0.l:8080/myapp/appl/,且在appl目录下存在index.html文件时,打开的是index.html,而非index,do,即便它位于前面(因为它不是个具体文件,而是由Web应用动态生成的)。
-
如果未找到,则使用默认MappedWrapper(通过conf/web.xml,即使Web应用不显式地进行配置,也一定会存在一个默认的Wrapper)。因此,无论请求链接是什么,只要匹配到合适的Context,那么肯定会存在一个匹配的Wrapper。
3. Catalina请求处理
Tomcat采用职责链模式来处理客户端请求
,以提高Servlet容器的灵活性和可扩展性。Tomcat定义了Pipeline(管道)和Valve(阀)
两个接口,前者用于构造职责链,后者代表职责链上的每个处理器
。由于Tomeat每一层Container均维护了一个Pipeline实例,因此我们可以在任何层级添加Valve配置,以拦截客户端请求进行定制处理(如打印请求日志)。与javax.servlet.Filter相比,Valve更靠近Servlet容器,而非Web应用,因此可以获得更多信息。而且Valve可以添加到任意一级的Container(如Host),便于针对服务器进行统一处理,不像javax.servlet.Filter仅限于单独的Web应用。
Tomcat的每一级容器均提供了基础的Valve:实现以完成当前容器的请求处理过程(如StandardHost对应的基础Valve实现为StandardHostValve),而且基础Valve实现始终位于职责链的未尾
,以确保最后执行
。
我们看一下一个典型的Valve实现:
class Samplevalve extends ValveBase
@Override
public final void invoke(Request request,Response response)
throws IOException,ServletException {
if (isok()){
//do something
getNext().invoke(request,response);
else {
log.error(“Bad request!”);
}
}
}
复制代码
由上可知,只要我们得到Pipeline中的第一个Valve即可以启动整个职责链的执行
,这也是为什么在3.2节中执行Engine的第一个Valve便可以完成整个客户端请求处理的原因。
基于此种设计方案,在完成请求映射之后,Tomcat的请求处理过程如图所示:
从图中我们可以知道,每一级Container的基础Valve在完成自身处理的情况下,同时还要确保启动下一级Container的Valve链的执行。而且由于“请求映射”过程已经将映射结果保存到请求对象Request中
,因此Valve直接从请求中获取下级Container即可
。比如StandardEngine.invoke():
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Host to be used for this Request
// 从Request中获取Host
Host host = request.getHost();
if (host == null) {
// HTTP 0.9 or HTTP 1.0 request without a host when no default host
// is defined.
// Don't overwrite an existing error
if (!response.isError()) {
response.sendError(404);
}
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// Ask this Host to process this request
// 引擎管道执行完成以后,会主动把请求交给Host管道的第一个阀门
host.getPipeline().getFirst().invoke(request, response);
}
复制代码
我们重点看一下StandardWrapperValve.invoke()方法:
public final void invoke(Request request, Response response)
throws IOException, ServletException {
...
// Allocate a servlet instance to process this request
try {
// 创建Servlet对象
if (!unavailable) {
servlet = wrapper.allocate();
}
} catch (UnavailableException e) {
...
}
...
// 为当前请求创建一个filterChain
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
// Call the filter chain for this request
// NOTE: This also calls the servlet's service() method
Container container = this.container;
try {
if ((servlet != null) && (filterChain != null)) {
// Swallow output if needed
if (context.getSwallowOutput()) {
try {
SystemLogHandler.startCapture();
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
// 执行filterChain
filterChain.doFilter(request.getRequest(),
response.getResponse());
}
} finally {
String log = SystemLogHandler.stopCapture();
if (log != null && log.length() > 0) {
context.getLogger().info(log);
}
}
} else {
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter
(request.getRequest(), response.getResponse());
}
}
}
}
...
}
复制代码
我们来看一下filterChain.doFilter()->internalDoFilter(request,response)
方法:
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
// 拿到每一个Filter执行doFilter方法
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();
if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.filter"), e);
}
return;
}
// We fell off the end of the chain -- call the servlet instance
try {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
if (request.isAsyncSupported() && !servletSupportsAsync) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
Boolean.FALSE);
}
// Use potentially wrapped request from this point
if ((request instanceof HttpServletRequest) &&
(response instanceof HttpServletResponse) &&
Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res};
SecurityUtil.doAsPrivilege("service",
servlet,
classTypeUsedInService,
args,
principal);
} else {
// FilterChain 执行完没有任何异常,然后就会执行service方法
servlet.service(request, response);
}
}
...
}
复制代码
继续servlet.service(request, response)->GenericServlet.service()->HttpServlet.service()->
然后根据GET,PUT请求执行对应的方法:
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
// doGet方法
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
复制代码
在StandardWrapperValve中(由于Wrapper为最低一级的Container,且该Valve处于职责链末端,因此它始终最后执行
),Tomcat构造FilterChain实例完成javax.servlet.Filter责任链的执行,并执行Servlet.service()方法将请求交由应用程序进行分发处理
(如果采用了如Spring MVC等Web框架的话,Servlet会进一步根据应用程序内部的配置将请求交由对应的控制器处理)。
至此,就可以执行到我们自己写的Servlet的doGet()方法了,请求就可以被我们处理了。
参考文章
tomcat-9.0.60-src源码解析
Tomcat架构解析
tomcat系列
Tomcat剖析之源码篇
tomcat源码解读
Tomcat源码篇-请求映射Mapper