深入理解Tomcat系列之七 詳解URL請求
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
前言
這裡分析一個實際的請求是如何在Tomcat中被處理的,以及最後是怎麼樣找到要處理的Servlet的?當我們在瀏覽器中輸入http://hostname:port/contextPath/servletPath
在Tomcat7(本文也是基於Tomcat7)中主要通過一個對映來完成的,這個對映的工作交給org.apache.tomcat.util.http.mapper.Mapper類來完成的,這個類儲存了Container容器所有子容器的資訊,在請求從Connector交給Container容器之前,Mapper會根據hostname和port將host容器與context容器設定到Request的mappingData屬性中,這樣在Connector的請求進入Container容器之前就知道了交給哪個容器了。
這段程式碼如下:
程式碼清單5-4:
// Virtual host mapping if (mappingData.host == null) { Host[] hosts = this.hosts; int pos = findIgnoreCase(hosts, host); if ((pos != -1) && (host.equalsIgnoreCase(hosts[pos].name))) { mappingData.host = hosts[pos].object; contexts = hosts[pos].contextList.contexts; nesting = hosts[pos].contextList.nesting; } else { if (defaultHostName == null) { return; } pos = find(hosts, defaultHostName); if ((pos != -1) && (defaultHostName.equals(hosts[pos].name))) { mappingData.host = hosts[pos].object; contexts = hosts[pos].contextList.contexts; nesting = hosts[pos].contextList.nesting; } else { return; } } } // Context mapping if (mappingData.context == null) { int pos = find(contexts, uri); if (pos == -1) { return; } int lastSlash = -1; int uriEnd = uri.getEnd(); int length = -1; boolean found = false; while (pos >= 0) { if (uri.startsWith(contexts[pos].name)) { length = contexts[pos].name.length(); if (uri.getLength() == length) { found = true; break; } else if (uri.startsWithIgnoreCase("/", length)) { found = true; break; } } if (lastSlash == -1) { lastSlash = nthSlash(uri, 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 = contexts[pos]; } if (context != null) { mappingData.contextPath.setString(context.name); } } if (context != null) { ContextVersion[] contextVersions = context.versions; int versionCount = contextVersions.length; if (versionCount > 1) { Object[] contextObjects = new Object[contextVersions.length]; for (int i = 0; i < contextObjects.length; i++) { contextObjects[i] = contextVersions[i].object; } mappingData.contexts = contextObjects; } if (version == null) { // Return the latest version contextVersion = contextVersions[versionCount - 1]; } else { int pos = find(contextVersions, version); if (pos < 0 || !contextVersions[pos].name.equals(version)) { // Return the latest version contextVersion = contextVersions[versionCount - 1]; } else { contextVersion = contextVersions[pos]; } } mappingData.context = contextVersion.object; mappingData.contextSlashCount = contextVersion.slashCount; } // Wrapper mapping if ((contextVersion != null) && (mappingData.wrapper == null)) { internalMapWrapper(contextVersion, uri, mappingData); }
- 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
下面啟動伺服器,在瀏覽器中輸入http://localhost:8080/examples/jsp/jsp2/el/composite.jsp,斷點除錯可以mappingData.host屬性為localhost
,mappingData.contextPath.setString(context.name)中context.name為examples
,mappingData.wrapperPath為/jsp/jsp2/el/composite.jsp
,這驗證了mappingData屬性的有效性,那麼mappingData屬性是如何設定到Request物件的屬性中的呢?
通過org.apache.catalina.connector.Request的原始碼可以知道,其是通過setContextPath方法與setHost方法設定進去的,其原始碼如下:
程式碼清單5-5:
public void setHost(Host host) { mappingData.host = host; } public void setContextPath(String path) { if (path == null) { mappingData.contextPath.setString(""); } else { mappingData.contextPath.setString(path); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
由於請求是從Connector傳過來的,而CoyoteAdapter是Connector中處理請求的最後一個類,那麼設定這兩個屬性的程式碼肯定在CoyoteAdapter類中,果不其然:
程式碼清單5-6:
// This will map the the latest version by default connector.getMapper().map(serverName, decodedURI, version, request.getMappingData()); request.setContext((Context) request.getMappingData().context); request.setWrapper((Wrapper) request.getMappingData().wrapper); //Mapper的map方法 public void map(MessageBytes host, MessageBytes uri, String version, MappingData mappingData) throws Exception { if (host.isNull()) { host.getCharChunk().append(defaultHostName); } host.toChars(); uri.toChars(); internalMap(host.getCharChunk(), uri.getCharChunk(), version, mappingData); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
intenalMap方法執行就是程式碼清單5-4的內容,這樣就把從Connector傳入請求,並設定Request物件的mappingData屬性的整個流程就打通了。還有一個疑問是為什麼Mapper類中可以擁有Container所有子容器的資訊呢?答案需要回到Tomcat啟動過程圖的第21步的startIntenal方法了:
程式碼清單5-7:
public void startInternal() throws LifecycleException { setState(LifecycleState.STARTING); // Find any components that have already been initialized since the // MBean listener won't be notified as those components will have // already registered their MBeans findDefaultHost(); Engine engine = (Engine) connector.getService().getContainer(); 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); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
這段程式碼就是將MapperListener作為一個監聽者加到整個Container容器的每一個子容器中,這樣任何一個子容器發生變化,MapperListener都將被通知,響應的mappingData屬性也會改變。最後可以總結訪問請求地址為http://localhost:8080/examples/composite.jsp的處理過程:
- 在埠8080啟動Server,並通知Service完成啟動,Service通知Connector完成初始化和啟動的過程
- Connector首先收到這個請求,會呼叫ProtocolHandler完成http協議的解析,然後交給SocketProcessor處理,解析請求頭,再交給CoyoteAdapter解析請求行和請求體,並把解析資訊封裝到Request和Response物件中
- 把請求(此時應該是Request物件,這裡的Request物件已經封裝了Http請求的資訊)交給Container容器
- Container容器交給其子容器——Engine容器,並等待Engine容器的處理結果
- Engine容器匹配其所有的虛擬主機,這裡匹配到Host
- 請求被移交給hostname為localhost的Host容器,host匹配其所有子容器Context,這裡找到contextPath為/examples的Context容器。如果匹配不到就把該請求交給路徑名為”“的Context去處理
- 請求再次被移交給Context容器,Context繼續匹配其子容器Wrapper,由Wrapper容器載入composite.jsp對應的servlet,這裡編譯的servlet是basic_002dcomparisons_jsp.class檔案
- Context容器根據字尾匹配原則*.jsp找到composite.jsp編譯的java類的class檔案
- Connector構建一個org.apache.catalina.connector.Request以及org.apache.catalina.connector.Response物件,使用反射呼叫Servelt的service方法
- Context容器把封裝了響應訊息的Response物件返回給Host容器
- Host容器把Response返回給Engine容器
- Engine容器返回給Connector
- Connetor容器把Response返回給瀏覽器
- 瀏覽器解析Response報文
- 顯示資源內容
根據前面的內容,其中的對映關係是由MapperListener類完成的。