在Tomcat中接收到具體的http請求,請求最后被一個(gè)具體的service處理,中間有一系列操作,有service的初始化、監(jiān)聽、過濾器等等操作,今天主要說的是service和URL的映射以及URL的匹配規(guī)則。和所有的web框架類似,URL肯定是有一個(gè)地方設(shè)置,然后關(guān)聯(lián)具體的service,其他web框架有可能是使用正則(例如Django),Tomcat卻是使用web.xml關(guān)聯(lián),接下來就講講映射的具體細(xì)節(jié)以及如何匹配到tomcat的wrap上
以下涉及到的源碼版本:java8、Tomcat8.5.4
Tomcat 基礎(chǔ)了解
先了解下Tomcat的各個(gè)組件的關(guān)系吧
Tomcat是有個(gè)核心類叫做catalina,在啟動(dòng)的時(shí)候就是根據(jù)不同的參數(shù)加載catalina的不同模塊的功能,例如生命周期、事件監(jiān)聽、組件管理等等。
Tomcat的server服務(wù)器可以包含多個(gè)service服務(wù),每一個(gè)service服務(wù)都存在一個(gè)組件connector接收http請求,然后交給container組件去進(jìn)行下一步的處理。如圖包含了engine、host、context、wrapper四種組件,其中wrapper就是包含了用戶實(shí)際開發(fā)的servlet服務(wù)。此外pipeline做為管道,一個(gè)組件只持有一個(gè)管道,然后管道上可以加上各種各樣的閥門valve,通過動(dòng)態(tài)配置valve,我們就可以實(shí)現(xiàn)數(shù)據(jù)的修改,監(jiān)控等等各種操作。
在servlet規(guī)范
中有明確的規(guī)定,servlet的服務(wù)時(shí)是使用ServletContext
來傳遞上下文,在Tomcat中是實(shí)現(xiàn)了ApplicationContext
去綁定service和context組件的,通過這樣的綁定就可以間接的綁定connector,使得整個(gè)的上下文都可以持有。
public ApplicationContext(StandardContext context) {
super();
this.context = context;
this.service = ((Engine) context.getParent().getParent()).getService();
this.sessionCookieConfig = new ApplicationSessionCookieConfig(context);
// Populate session tracking modes
populateSessionTrackingModes();
}
URL配置
URL配置是通過配置web.xml的URL-pattern設(shè)置URL到servlet類的映射關(guān)系的,如下樣例
<servlet>
<servlet-name>ExactServlet</servlet-name>
<servlet-class>org.test.ExactServlet</servlet-class>
<!-- init參數(shù)以及l(fā)oad-on-startup等暫時(shí)忽略-->
</servlet>
<servlet-mapping>
<servlet-name>ExactServlet</servlet-name>
<url-pattern>/exact.action</url-pattern>
</servlet-mapping>
先明確好具體的servlet類,其中servlet-name表示一個(gè)servlet的名稱,不允許重復(fù),和具體的servlet-mapping對應(yīng),url-pattern就是我們關(guān)心的URL,現(xiàn)在一個(gè)*****/exact.action
的URL請求過來,會(huì)被關(guān)聯(lián)到org.test.ExactServlet
類上。
URL有三種配置方法
- 完全匹配
/index.html
- 目錄匹配
/news/*
- 后綴匹配
*.do
URL讀取
在不看源碼之前,如果我們實(shí)現(xiàn)該功能,第一步肯定是解析xml文件,找到具體的映射之間的關(guān)系,然后具體請求就根據(jù)某些規(guī)則匹配出最合適的servlet服務(wù),如果匹配失敗就會(huì)提示404錯(cuò)誤。
現(xiàn)在就看看源碼具體的細(xì)節(jié)是如何操作的,其實(shí)Tomcat的具體實(shí)現(xiàn)和我們說的也基本類似(基本上的URL映射都是這個(gè)套路吧),入口是mbeanfactory類的createStandardContext方法,其中context加上了ContextConfig監(jiān)聽者
然后就是StandardContext類的啟動(dòng),按照鏈路分析,發(fā)現(xiàn)在startInternal方法中有進(jìn)行CONFIG_START_EVENT
的監(jiān)聽事件觸發(fā)
又因?yàn)樯厦婕尤氲谋O(jiān)聽者是ContextConfig類,那么最后就進(jìn)入到該類的事件處理方法上
configStart這個(gè)方法的名字就很直觀,表示的是屬性開始配置,又來到webConfig方法
通過WebXmlParse類的即系以及WebXml數(shù)據(jù)的存儲(chǔ),依舊是采用了digester的方式解析xml數(shù)據(jù),中間通過各種操作,最后把數(shù)據(jù)存儲(chǔ)到了StandardContext的servletMappings鍵值對中。
通過上述操作,就完成了URL從xml文件中到context的過渡工作。下面這個(gè)圖簡要的介紹下上面的整個(gè)流程(有些細(xì)節(jié)還未完全處理好)
URL映射
這一節(jié)介紹接收到一個(gè)http請求,URL是如何從socket被解析出來,映射到具體的servlet的一整個(gè)過程,包括了URL匹配的細(xì)節(jié)
作為一個(gè)http容器,必然存在接受socket套接字的數(shù)據(jù),我們可以看到在Tomcat的啟動(dòng)時(shí)候的代碼
其中g(shù)etConnector()方法就是創(chuàng)建一個(gè)connector,使用配置好的端口號(hào),http1.1的協(xié)議,并且把這個(gè)連接器組件綁定到service上。再回看上面的框架圖,肯定知道在service中包含了一個(gè)engine組件
,重點(diǎn)是在Mapper,關(guān)于解析socket數(shù)據(jù)不在此次介紹中。
Mapper的作用就是通過一系列的規(guī)則,最后匹配到合適的servlet去執(zhí)行相應(yīng)功能,具體的調(diào)用是由MapperListener監(jiān)聽器完成。在MapperListener類中監(jiān)聽到容器事件
@Override
public void containerEvent(ContainerEvent event) {
if (Container.ADD_CHILD_EVENT.equals(event.getType())) {
Container child = (Container) event.getData();
addListeners(child);
// 添加子容器
if (child.getState().isAvailable()) {
if (child instanceof Host) {
registerHost((Host) child);
} else if (child instanceof Context) {
registerContext((Context) child);
} else if (child instanceof Wrapper) {
// Only if the Context has started. If it has not, then it
// will have its own "after_start" life-cycle event later.
if (child.getParent().getState().isAvailable()) {
registerWrapper((Wrapper) child);
}
}
}
} else if (Container.REMOVE_CHILD_EVENT.equals(event.getType())) {
Container child = (Container) event.getData();
removeListeners(child);
// No need to unregister - life-cycle listener will handle this when
// the child stops
} else if (Host.ADD_ALIAS_EVENT.equals(event.getType())) {
// Handle dynamically adding host aliases
mapper.addHostAlias(((Host) event.getSource()).getName(),
event.getData().toString());
} else if (Host.REMOVE_ALIAS_EVENT.equals(event.getType())) {
// Handle dynamically removing host aliases
mapper.removeHostAlias(event.getData().toString());
} else 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("/*"));
mapper.addWrapper(hostName, contextPath, version, mapping, wrapper,
jspWildCard, context.isResourceOnlyServlet(wrapperName));
} else if (Wrapper.REMOVE_MAPPING_EVENT.equals(event.getType())) {
// Handle dynamically removing 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 mapping = (String) event.getData();
mapper.removeWrapper(hostName, contextPath, version, mapping);
} else if (Context.ADD_WELCOME_FILE_EVENT.equals(event.getType())) {
// Handle dynamically adding welcome files
Context context = (Context) event.getSource();
String hostName = context.getParent().getName();
String contextPath = context.getPath();
if ("/".equals(contextPath)) {
contextPath = "";
}
String welcomeFile = (String) event.getData();
mapper.addWelcomeFile(hostName, contextPath,
context.getWebappVersion(), welcomeFile);
} else if (Context.REMOVE_WELCOME_FILE_EVENT.equals(event.getType())) {
// Handle dynamically removing welcome files
Context context = (Context) event.getSource();
String hostName = context.getParent().getName();
String contextPath = context.getPath();
if ("/".equals(contextPath)) {
contextPath = "";
}
String welcomeFile = (String) event.getData();
mapper.removeWelcomeFile(hostName, contextPath,
context.getWebappVersion(), welcomeFile);
} else if (Context.CLEAR_WELCOME_FILES_EVENT.equals(event.getType())) {
// Handle dynamically clearing welcome files
Context context = (Context) event.getSource();
String hostName = context.getParent().getName();
String contextPath = context.getPath();
if ("/".equals(contextPath)) {
contextPath = "";
}
mapper.clearWelcomeFiles(hostName, contextPath,
context.getWebappVersion());
}
}
仔細(xì)看這個(gè)代碼沒發(fā)現(xiàn)什么異常,可以細(xì)看會(huì)覺得有些不對勁,MapperListener作為service的監(jiān)聽器怎么可能接收到添加wrapper的,中間還嵌套了engine、host等容器,按照邏輯肯定是不能掛載wrapper的。回過頭來再看StandardService類的啟動(dòng)方法
protected void startInternal() throws LifecycleException {
if(log.isInfoEnabled())
log.info(sm.getString("standardService.start.name", this.name));
setState(LifecycleState.STARTING);
// Start our defined Container first
if (engine != null) {
synchronized (engine) {
engine.start();
// 啟動(dòng)了engine
}
}
synchronized (executors) {
for (Executor executor: executors) {
executor.start();
}
}
mapperListener.start();
// 當(dāng)前standardservice的監(jiān)聽器也啟動(dòng)了
上述代碼可知,在engine.start()
的時(shí)候,engine以及engine的子容器,子容器的子容器也都順利啟動(dòng)了,各個(gè)組件的嵌套關(guān)系也很明確,細(xì)看mapperListener.start()
public void startInternal() throws LifecycleException {
setState(LifecycleState.STARTING);
Engine engine = service.getContainer();
if (engine == null) {
return;
}
findDefaultHost();
addListeners(engine);
// 比較關(guān)鍵的一步,加上監(jiān)聽器,也是我們當(dāng)前關(guān)注的重點(diǎn)
Container[] conHosts = engine.findChildren();
// 開始處理engine的子容器
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組件
}
}
}
private void addListeners(Container container) {
container.addContainerListener(this);
// 把該mapperlistener加入到容器的監(jiān)聽者中
container.addLifecycleListener(this);
for (Container child : container.findChildren()) {
// 針對engine而言子容器就是host,給每個(gè)host加上該mapperlistener
// 遍歷所有的容器組件,給每個(gè)組件都加上mapperlistener監(jiān)聽器
addListeners(child);
}
}
上述代碼已經(jīng)很清楚的告訴我們,每一個(gè)容器都持有同一個(gè)mapperlistener監(jiān)聽器對象,所以上述的可以添加wrapper容器也可以很好的解釋了,每一個(gè)組件都可以調(diào)用該方法,自然就存在掛載wrapper的情況了。
該函數(shù)內(nèi)容較多,大部分事件都是在容器內(nèi)插入新的子容器,以及插入子容器后續(xù)的事情,就以addMapping為例子
// 調(diào)用該addMapping的方法在StandardContext的addServletMapping方法內(nèi)
// fireContainerEvent("addServletMapping", decodedPattern);
} else 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("/*"));
mapper.addWrapper(hostName, contextPath, version, mapping, wrapper,
jspWildCard, context.isResourceOnlyServlet(wrapperName));
// mapper的addwrapper方法
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;
}
}
}
}
這里,我們可以看到路徑分成4類,其中包含了我們上面說的三種分類情況,其實(shí)細(xì)看源碼會(huì)發(fā)現(xiàn),不同匹配規(guī)則會(huì)被放到不同類型的wrapper中,其中有
- /* 放在wildcardWrappers中
- *. 放在extensionWrappers中
- / 放在defaultWrapper中
- 其他 放在exactWrappers中
以上就完成了wrapper以及一系列容器的關(guān)聯(lián)嵌套。
HTTP處理
一個(gè)新來的http請求,也需要找到合適的engine、host、context、wrapper進(jìn)行處理,在接收到新的請求之后,在CoyoteAdapt類中調(diào)用map方法,再調(diào)用internalMap方法,這個(gè)方法中可以為mappingdata設(shè)置整個(gè)鏈路的容器(除了wrapper),最后的internalMapWrapper 明確最后的wrapper
// CoyoteAdapt
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
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();
}
uri.setLimit(-1);
// Virtual host mapping
MappedHost[] hosts = this.hosts;
MappedHost mappedHost = exactFindIgnoreCase(hosts, host);
if (mappedHost == null) {
if (defaultHostName == null) {
return;
}
mappedHost = exactFind(hosts, defaultHostName);
if (mappedHost == null) {
return;
}
}
mappingData.host = mappedHost.object;
// 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);
}
}
// 根據(jù)URL匹配具體的wrapper規(guī)則
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();
WebResource file;
// Handle context root
if (pathStr.length() == 0) {
file = contextVersion.resources.getResource("/");
} else {
file = contextVersion.resources.getResource(pathStr);
}
if (file != null && file.isDirectory() &&
contextVersion.object.getMapperDirectoryRedirectEnabled()) {
// 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);
}
}
}
path.setOffset(pathOffset);
path.setEnd(pathEnd);
}
以上過程也知道了,只要一個(gè)http請求解析出http協(xié)議的字段信息,就立馬明確了其整個(gè)的執(zhí)行鏈路過程,鏈路數(shù)據(jù)是存儲(chǔ)在mappingdata中,這點(diǎn)和Tomcat4有些不一樣