Servlet映射路徑匹配解析詳解
開(kāi)頭
servlet是javaweb用來(lái)處理請(qǐng)求和響應(yīng)的重要對(duì)象,本文將從源碼的角度分析tomcat內(nèi)部是如何根據(jù)請(qǐng)求路徑匹配得到處理請(qǐng)求的servlet的
假設(shè)有一個(gè)request請(qǐng)求路徑為/text/servlet/get,并且在web.xml中配置了4個(gè)servlet,代碼如下,那么該請(qǐng)求調(diào)用的是哪一個(gè)servlet呢?
<servlet> <servlet-name>servlet01</servlet-name> <servlet-class>com.monian.study.servlet.Servlet01</servlet-class> </servlet> <servlet-mapping> <servlet-name>servlet01</servlet-name> <url-pattern>/test/servlet/get</url-pattern> </servlet-mapping> <servlet> <servlet-name>servlet02</servlet-name> <servlet-class>com.monian.study.servlet.Servlet02</servlet-class> </servlet> <servlet-mapping> <servlet-name>servlet02</servlet-name> <url-pattern>/test/servlet/*</url-pattern> </servlet-mapping> <servlet> <servlet-name>servlet03</servlet-name> <servlet-class>com.monian.study.servlet.Servlet03</servlet-class> </servlet> <servlet-mapping> <servlet-name>servlet03</servlet-name> <url-pattern>/test/*</url-pattern> </servlet-mapping> <servlet> <servlet-name>servlet04</servlet-name> <servlet-class>com.monian.study.servlet.Servlet04</servlet-class> </servlet> <servlet-mapping> <servlet-name>servlet04</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <servlet> <servlet-name>servlet05</servlet-name> <servlet-class>com.monian.study.servlet.Servlet05</servlet-class> </servlet> <servlet-mapping> <servlet-name>servlet05</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping>
相應(yīng)各個(gè)servlet的代碼,代碼很簡(jiǎn)單,調(diào)用哪一個(gè)servlet就輸出哪個(gè)servlet的名稱(chēng):
servlet代碼
public class Servlet01 extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("Servlet01"); } } public class Servlet02 extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("Servlet02"); } } public class Servlet03 extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("Servlet03"); } } public class Servlet04 extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("Servlet04"); } } public class Servlet05 extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("Servlet05"); } }
源碼
org.apache.catalina.mapper.Mapper#internalMapWrapper
// 在本例子中 path = '/zxq/test/servlet/get',用offset和end來(lái)控制路徑部分長(zhǎng)度 // contextPath = '/zxq' private final void internalMapWrapper(ContextVersion contextVersion, CharChunk path, MappingData mappingData) throws IOException { int pathOffset = path.getOffset(); int pathEnd = path.getEnd(); boolean noServletPath = false; // contextVersion.path = '/zxq' int length = contextVersion.path.length(); if (length == (pathEnd - pathOffset)) { noServletPath = true; } int servletPath = pathOffset + length; // path = '/text/servlet/get' path.setOffset(servletPath); // 規(guī)則1:先開(kāi)始精確匹配 MappedWrapper[] exactWrappers = contextVersion.exactWrappers; internalMapExactWrapper(exactWrappers, path, mappingData); // 規(guī)則2:前綴匹配,也就是路徑匹配 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(); // 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); }
匹配路徑代碼
org.apache.catalina.mapper.Mapper#find(org.apache.catalina.mapper.Mapper.MapElement[], org.apache.tomcat.util.buf.CharChunk, int, int)
// 從map找到一個(gè)最與路徑匹配的 private static final <T> int find(MapElement<T>[] map, CharChunk name, int start, int end) { int a = 0; int b = map.length - 1; // Special cases: -1 and 0 if (b == -1) { return -1; } // -1表示完全不匹配,直接返回 if (compare(name, start, end, map[0].name) < 0 ) { return -1; } // 完全匹配或部分匹配,且只有一個(gè)待匹配的servlet直接返回 if (b == 0) { return 0; } // 類(lèi)似于二分查找,找到一個(gè)最長(zhǎng)路徑匹配 int i = 0; while (true) { i = (b + a) >>> 1; int result = compare(name, start, end, map[i].name); if (result == 1) { a = i; } else if (result == 0) { return i; } else { b = i; } if ((b - a) == 1) { int result2 = compare(name, start, end, map[b].name); if (result2 < 0) { return a; } else { return b; } } } } private static final int compare(CharChunk name, int start, int end, String compareTo) { int result = 0; char[] c = name.getBuffer(); int len = compareTo.length(); if ((end - start) < len) { len = end - start; } // 比較url-pattern與 請(qǐng)求路徑path,若有一個(gè)字符不相等退出循環(huán) for (int i = 0; (i < len) && (result == 0); i++) { if (c[i + start] > compareTo.charAt(i)) { result = 1; } else if (c[i + start] < compareTo.charAt(i)) { result = -1; } } // 都相等的話(huà)再比較長(zhǎng)度,請(qǐng)求路徑長(zhǎng)度比待匹配部分長(zhǎng) if (result == 0) { if (compareTo.length() > (end - start)) { result = -1; } else if (compareTo.length() < (end - start)) { result = 1; } } // result=0代表完全匹配, result=-1代表不匹配,result=1代表開(kāi)頭部分匹配 return result; }
針對(duì)上述的匹配舉個(gè)例子,假設(shè)有兩個(gè)servlet都是通配符匹配的,url-pattern為 /test/one/* 和/test/* ,tomcat解析的時(shí)候會(huì)去掉通配符再排序['/test', 'test/one'],之后再去匹配數(shù)據(jù)中的元素也就是map[i].name,匹配路徑 '/test/one/two'會(huì)返回url-parttern=/test/one/* 的這個(gè)servlet,這就是最長(zhǎng)路徑匹配
精確匹配
可以看到符合精確匹配的只有servlet01,且name就是它配置的url-pattern值,然后與requestPath進(jìn)行匹配
private final void internalMapExactWrapper (MappedWrapper[] wrappers, CharChunk path, MappingData mappingData) { // 找到一個(gè)與path精確匹配的wrapper MappedWrapper wrapper = exactFind(wrappers, path); if (wrapper != null) { mappingData.requestPath.setString(wrapper.name); mappingData.wrapper = wrapper.object; if (path.equals("/")) { // Special handling for Context Root mapped servlet mappingData.pathInfo.setString("/"); mappingData.wrapperPath.setString(""); // This seems wrong but it is what the spec says... mappingData.contextPath.setString(""); mappingData.matchType = MappingMatch.CONTEXT_ROOT; } else { mappingData.wrapperPath.setString(wrapper.name); mappingData.matchType = MappingMatch.EXACT; } } } private static final <T, E extends MapElement<T>> E exactFind(E[] map, CharChunk name) { // find方法會(huì)返回部分匹配或完全匹配的map int pos = find(map, name); if (pos >= 0) { E result = map[pos]; // 完全匹配 if (name.equals(result.name)) { return result; } } return null; }
顯而易見(jiàn)的開(kāi)頭那個(gè)request與servlet01的url-pattern是精確匹配的
通配符匹配 (路徑匹配)
接下來(lái)web.xml去掉servlet01的配置,只剩下4個(gè)servlet,從前面來(lái)看,精確匹配肯定是失敗的因?yàn)楝F(xiàn)在去掉servlet01已經(jīng)沒(méi)有符合要求的servlet去精確匹配了,只能進(jìn)行路徑匹配了,而路徑匹配符合要求的有兩個(gè)servlet
/** * Wildcard mapping. */ private final void internalMapWildcardWrapper (MappedWrapper[] wrappers, int nesting, CharChunk path, MappingData mappingData) { int pathEnd = path.getEnd(); int lastSlash = -1; int length = -1; // 找一個(gè)最匹配path路徑的,根據(jù)上面的匹配代碼可以得到servlet02 int pos = find(wrappers, path); if (pos != -1) { boolean found = false; while (pos >= 0) { if (path.startsWith(wrappers[pos].name)) { length = wrappers[pos].name.length(); if (path.getLength() == length) { found = true; break; // path不以/開(kāi)頭,則重新找 } else if (path.startsWithIgnoreCase("/", length)) { found = true; break; } } // 獲取path最后一個(gè)/ 所在的位置 if (lastSlash == -1) { lastSlash = nthSlash(path, nesting + 1); } else { lastSlash = lastSlash(path); } path.setEnd(lastSlash); pos = find(wrappers, path); } path.setEnd(pathEnd); if (found) { mappingData.wrapperPath.setString(wrappers[pos].name); if (path.getLength() > length) { mappingData.pathInfo.setChars (path.getBuffer(), path.getOffset() + length, path.getLength() - length); } mappingData.requestPath.setChars (path.getBuffer(), path.getOffset(), path.getLength()); mappingData.wrapper = wrappers[pos].object; mappingData.jspWildCard = wrappers[pos].jspWildCard; mappingData.matchType = MappingMatch.PATH; } } }
因此servlet02是匹配的,輸出
若再web.xml去掉servlet02,那么匹配的就是servlet03了
另外我們可以從上面的代碼得到若請(qǐng)求路徑path = '/test/servlet/get', 則 '/*' 、 '/test/*' 、 '/test/servlet/*' 、 '/test/servlet/get/*' 與之匹配,'/test/serv/*' 這種不匹配
路徑匹配是能匹配請(qǐng)求路徑以 .jsp 、.html結(jié)尾的request的
擴(kuò)展名匹配(后綴匹配)
web.xml中注釋servlet02和servlet03后,再次訪(fǎng)問(wèn).jsp后綴結(jié)尾的請(qǐng)求就會(huì)直接報(bào)404了,可以看后續(xù)的匹配邏輯雖然能匹配到處理.jsp的servlet但我們并沒(méi)有在相應(yīng)路徑下配置jsp文件,那么自然報(bào)404錯(cuò)誤了
下圖可以看到后綴匹配的servlet有三個(gè),一個(gè)我們自定義的后綴為do,另外兩個(gè)jsp和jspx是tomcat內(nèi)置的默認(rèn)處理jsp的servlet
/** * Extension mappings. * * @param wrappers Set of wrappers to check for matches * @param path Path to map * @param mappingData Mapping data for result * @param resourceExpected Is this mapping expecting to find a resource */ private final void internalMapExtensionWrapper(MappedWrapper[] wrappers, CharChunk path, MappingData mappingData, boolean resourceExpected) { char[] buf = path.getBuffer(); int pathEnd = path.getEnd(); int servletPath = path.getOffset(); int slash = -1; for (int i = pathEnd - 1; i >= servletPath; i--) { if (buf[i] == '/') { slash = i; break; } } if (slash >= 0) { int period = -1; for (int i = pathEnd - 1; i > slash; i--) { if (buf[i] == '.') { period = i; break; } } if (period >= 0) { // 截取到后綴的字符位置 匹配 path.setOffset(period + 1); path.setEnd(pathEnd); MappedWrapper wrapper = exactFind(wrappers, path); if (wrapper != null && (resourceExpected || !wrapper.resourceOnly)) { mappingData.wrapperPath.setChars(buf, servletPath, pathEnd - servletPath); mappingData.requestPath.setChars(buf, servletPath, pathEnd - servletPath); mappingData.wrapper = wrapper.object; mappingData.matchType = MappingMatch.EXTENSION; } path.setOffset(servletPath); path.setEnd(pathEnd); } } }
根據(jù)find的匹配邏輯可以匹配到我們自定義的servlet05,輸出
首頁(yè)welcome資源匹配
若上述匹配都失敗了則嘗試尋找默認(rèn)的資源文件,默認(rèn)有三個(gè),也可以自定義配置
假設(shè)請(qǐng)求路徑為http://localhost:8082/zxq/ 以'/'結(jié)尾,那么會(huì)嘗試將文件名加到path后面,以index.jsp為例,加完后路徑為'/zxq/index.jsp',之后會(huì)以此新路徑再去嘗試精確匹配、路徑匹配、物理文件查找再進(jìn)行擴(kuò)展名匹配順序查找,直到找到能處理此path的servlet
在webapp目錄下加一個(gè)index.jsp文件之后能成功訪(fǎng)問(wèn)到,執(zhí)行此請(qǐng)求的就是tomcat默認(rèn)的jsp servlet
默認(rèn)匹配
<url-pattern>/</url-pattern> '/'就是默認(rèn)匹配,當(dāng)上述匹配都失敗的時(shí)候,則啟用這個(gè)servlet,也就是本文中的servlet04
以上就是Servlet映射路徑匹配解析詳解的詳細(xì)內(nèi)容,更多關(guān)于Servlet映射路徑匹配的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Springboot把外部依賴(lài)包納入Spring容器管理的兩種方式
這篇文章主要給大家介紹了Springboot把外部依賴(lài)包納入Spring容器管理的兩種方式,Spring.factories和org.springframework.boot.autoconfigure.AutoConfiguration.imports,有感興趣的小伙伴可以參考閱讀本文2023-07-07Mybatis?sqlMapConfig.xml中的mappers標(biāo)簽使用
這篇文章主要介紹了Mybatis?sqlMapConfig.xml中的mappers標(biāo)簽使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教。2022-01-01利用Java搭建個(gè)簡(jiǎn)單的Netty通信實(shí)例教程
這篇文章主要給大家介紹了關(guān)于如何利用Java搭建個(gè)簡(jiǎn)單的Netty通信,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05Spring Boot將項(xiàng)目打包成war包的操作方法
這篇文章主要介紹了Spring Boot將項(xiàng)目打包成war包的操作方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-09-09RocketMQ設(shè)計(jì)之主從復(fù)制和讀寫(xiě)分離
這篇文章主要介紹了RocketMQ設(shè)計(jì)之主從復(fù)制和讀寫(xiě)分離,RocketMQ提高消費(fèi)避免Broker發(fā)生單點(diǎn)故障引起B(yǎng)roker上的消息無(wú)法及時(shí)消費(fèi),下文關(guān)于了RocketMQ的相關(guān)內(nèi)容,需要的小伙伴可以參考一下2022-03-03簡(jiǎn)單談?wù)凷truts動(dòng)態(tài)表單(DynamicForm)
下面小編就為大家?guī)?lái)一篇簡(jiǎn)單談?wù)凷truts動(dòng)態(tài)表單(DynamicForm)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08