在分析 Tomcat 實現以前,首先看一下 Servlet 規範是如何規定容器怎麼把請求映射到一個 servlet。本文首發於(微信公衆號:頓悟源碼)html
收到客戶端請求後,容器根據請求 URL 的上下文名稱匹配 Web 應用程序,而後根據去除上下文路徑和路徑參數的路徑,按如下規則順序匹配,而且只使用第一個匹配的 Servlet,後續再也不嘗試匹配:java
容器在匹配時區分大小寫。web
在 web.xml 部署描述符中,使用如下語法定義映射:數組
假設有如下映射配置:微信
/foo/bar/* servlet1 /baz/* servlet2 /catalog servlet3 *.bop servlet4
那麼如下請求路徑的匹配狀況是:app
/foo/bar/index.html servlet1 /foo/bar/index.bop servlet1 /baz servlet2 /baz/index.html servlet2 /catalog servlet3 /catalog/index.html default servlet /catalog/racecar.bop servlet4 /index.bop servlet4
注意,在 /catalog/index.html 和 /catalog/racecar.bop 的狀況下,不使用映射到 /catalog 的 servlet,是由於不是徹底匹配。webapp
實現請求映射的通常方法是,首先構建一個路由表,而後按照規範進行匹配,最後返回匹配結果。Tomcat 就是如此,與請求映射相關的類有三個,分別是:jsp
這裏使用的源碼版本是 6.0.53,此版本 MapperListener 是經過 JMX 查詢 Host、Context、Wrapper,而後加入到 Mapper 的路由表中。而在高版本,如7和8中,則使用的是 containerEvent 和 lifecycleEvent 容器和生命週期事件進行構建。post
Mapper 內部設計了路由表的組成結構,相關的類圖以下:url
上圖包含了各種的核心成員變量和方法,也直觀的體現了類之間的關係。
Mapper 在構建路由時,addHost 和 addContext 比較簡單,都是對數組的操做,這裏着重對 addWrapper 的源碼進行分析。
從類圖中可看出 Context 內部有四種 Wrapper,對應着處理不一樣映射規則的 Servlet,分別是:
addWrapper 就是以這種規則,根據請求 path 按條件將 Wrapper 插入對應的數組中,核心源碼以下:
protected void addWrapper(Context context, String path, Object wrapper, boolean jspWildCard) { synchronized (context) { Wrapper newWrapper = new Wrapper(); newWrapper.object = wrapper; // StandardWrapper 對象 newWrapper.jspWildCard = jspWildCard; // 是不是 JspServlet if (path.endsWith("/*")) { // Wildcard wrapper 模糊匹配,最長前綴路徑匹配 // 存儲名稱時去除 /* 字符 newWrapper.name = path.substring(0, path.length() - 2); ... // 插入到 context 處理模糊匹配的 Wrapper 數組中 context.wildcardWrappers = newWrappers; } else if (path.startsWith("*.")) { // Extension wrapper 擴展名匹配 newWrapper.name = path.substring(2); // 存儲名稱時去除 *. 字符 ... // 插入到 context 處理擴展名匹配的 Wrapper 數組中 context.extensionWrappers = newWrappers; } else if (path.equals("/")) { // Default wrapper 默認 Servlet newWrapper.name = ""; // 名稱爲空字符串 context.defaultWrapper = newWrapper; } else { // Exact wrapper 徹底匹配 newWrapper.name = path; ... // 插入到 context 處理徹底匹配的 Wrapper 數組中 context.exactWrappers = newWrappers; } } }
上文的 Servlet 映射實例的配置,在內存中,存儲狀況以下:
觸發映射請求的動做是 CoyoteAdapter 的 postParseRequest() 方法,最終由 Mapper 內部的 internalMap 和 internalMapWrapper 兩個方法完成。
internalMap 根據 name 字符串匹配 Host 和 Context,其中 Host 不區分大小寫,Context 區分。internalMapWrapper 實現的就是 Servlet 規範描述的 URL 匹配規則。
有一點須要注意,在遍歷數組查找 Host、Context、Wrapper 時,使用的是二分查找,比較的是字符串,在返回結果時,返回的是與參數儘量接近或相等的元素下標,其中的一個 find 源碼以下:
private static final int find(MapElement[] map, String name) { int a = 0; int b = map.length - 1; // 若是數組爲空 if (b == -1) { return -1; } // 或者小於數組的第一個元素,那麼返回 -1 表示沒找到 if (name.compareTo(map[0].name) < 0) { return -1; } // 或者大於數組的第一個元素,且數組長度爲 1,返回下標 0 if (b == 0) { return 0; } // 二分查找等於或長度最接近 name 的數組元素下標 int i = 0; while (true) { i = (b + a) / 2; // 中間元素下標 int result = name.compareTo(map[i].name); if (result > 0) { // 大於 map[i] a = i; // 從中間日後開始查找 } else if (result == 0) { return i; // 等於,直接返回 i } else { // 小於,從中間往前開始查找 b = i; } if ((b - a) == 1) {// 若是下次比較的元素就剩兩個 int result2 = name.compareTo(map[b].name); if (result2 < 0) { return a; // 小於返回下標 a } else { return b; // 大於等於返回下標 b } } } }
以上文映射實例的配置爲例,分析 /foo/bar/index.html 映射 Servlet 的源碼實現,注意這裏使用的路徑,要去除上下文路徑和路徑參數。
首先嚐試徹底匹配:
// Rule 1 -- Exact Match Wrapper[] exactWrappers = context.exactWrappers; // 獲取處理徹底匹配的 Wrapper 數組,這裏是 [servlet3(/catalog)] internalMapExactWrapper(exactWrappers, path, mappingData); private final void internalMapExactWrapper(...) { int pos = find(wrappers, path); // 查找 path 長度最相近或相等的 wrapper if ((pos != -1) && (path.equals(wrappers[pos].name))) { // 若是匹配成功,設置匹配數據,直接返回,後續再也不匹配 mappingData.requestPath.setString(wrappers[pos].name); mappingData.wrapperPath.setString(wrappers[pos].name); mappingData.wrapper = wrappers[pos].object; } }
若是徹底匹配失敗,而後嘗試最長路徑的模糊匹配,核心代碼以下:
// Rule 2 -- Prefix Match boolean checkJspWelcomeFiles = false; // 獲取處理路徑匹配的 Wrapper 數組,這裏是 [servlet1(/foo/bar),servlet2(/baz)] Wrapper[] wildcardWrappers = context.wildcardWrappers; // 確保徹底匹配失敗 if (mappingData.wrapper == null) { internalMapWildcardWrapper(wildcardWrappers, path,...); } private final void internalMapWildcardWrapper(...) { ... int pos = find(wrappers, path); boolean found = false; while (pos >= 0) { // 若是以 path 以 /foo/bar 開頭 if (path.startsWith(wrappers[pos].name)) { length = wrappers[pos].name.length(); if (path.getLength() == length) { // 長度正好相等,則匹配成功 found = true; break; } else if (path.startsWithIgnoreCase("/", length)) { // 或者跳過這個開頭而且以 "/" 開始,也匹配成功 found = true; break; } } } // 這裏的 path 是 /foo/bar/index.html,符合第二個 if if (found) { mappingData.wrapperPath.setString mappingData.pathInfo.setChars ... } }
此時已經成功匹配到 Servlet,後續的匹配將不會不執行。簡單對後面的匹配進行分析,擴展名匹配比較簡單,首先會從 path 中找到擴展名的值,而後在 extensionWrappers 數組中查找便可;若是前面都沒匹配成功,那麼就返回默認的 Wrapper
在返回的 MappingData 結果中,有幾個 path 須要注意一下,它們分別在如下位置:
|-- Context Path --|-- Servlet Path -|--Path Info--| http://localhost:8080 /webapp /helloServlet /hello |-------- Request URI ----------------------------|
看源碼時,發現 Tomcat 寫了大量的代碼,那是由於,它爲了減小內存拷貝,設計了一個 CharChunk,在一個 char[] 數組視圖上,實現了相似 String 的一些比較方法。