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;
}
}
// 都相等的話再比較長(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后,再次訪問(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文件之后能成功訪問(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)文章
springAI結(jié)合ollama簡(jiǎn)單實(shí)現(xiàn)小結(jié)
本文主要介紹了springAI結(jié)合ollama簡(jiǎn)單實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-03-03
Java中兩個(gè)大數(shù)之間的相關(guān)運(yùn)算及BigInteger代碼示例
這篇文章主要介紹了Java中兩個(gè)大數(shù)之間的相關(guān)運(yùn)算及BigInteger代碼示例,通過(guò)biginteger類(lèi)實(shí)現(xiàn)大數(shù)的運(yùn)算代碼,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11
解決Spring Cloud Feign 請(qǐng)求時(shí)附帶請(qǐng)求頭的問(wèn)題
這篇文章主要介紹了解決Spring Cloud Feign 請(qǐng)求時(shí)附帶請(qǐng)求頭的問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10
Java生成PDF文檔兩個(gè)超實(shí)用的庫(kù)( iText和Apache PDFBox)
這篇文章主要介紹了Java生成PDF文檔兩個(gè)超實(shí)用的庫(kù),分別是用 iText庫(kù)以及用Apache PDFBox庫(kù)生成PDF,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-02-02
java中的通用權(quán)限管理設(shè)計(jì)(推薦)
下面小編就為大家推薦一篇java中的通用權(quán)限管理設(shè)計(jì),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2017-12-12
Java實(shí)現(xiàn)獲取控制臺(tái)輸出結(jié)果轉(zhuǎn)換為變量的詳細(xì)操作
在Java編程中,有時(shí)需將控制臺(tái)的輸出捕獲為字符串,以便于后續(xù)的處理或測(cè)試,這種需求在日志記錄、單元測(cè)試或調(diào)試時(shí)尤為常見(jiàn),下面,將通過(guò)詳細(xì)步驟來(lái)介紹如何使用ByteArrayOutputStream和PrintStream來(lái)實(shí)現(xiàn)這一功能,需要的朋友可以參考下2024-06-06
java實(shí)現(xiàn)響應(yīng)重定向發(fā)送post請(qǐng)求操作示例
這篇文章主要介紹了java實(shí)現(xiàn)響應(yīng)重定向發(fā)送post請(qǐng)求操作,結(jié)合實(shí)例形式分析了java請(qǐng)求響應(yīng)、重定向及數(shù)據(jù)處理相關(guān)操作技巧,需要的朋友可以參考下2020-04-04
JDK基于CAS實(shí)現(xiàn)原子類(lèi)盤(pán)點(diǎn)解析
這篇文章主要為大家介紹了JDK基于CAS實(shí)現(xiàn)原子類(lèi)盤(pán)點(diǎn)解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
spring boot+jwt實(shí)現(xiàn)api的token認(rèn)證詳解
這篇文章主要給大家介紹了關(guān)于spring boot+jwt實(shí)現(xiàn)api的token認(rèn)證的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一學(xué)習(xí)學(xué)習(xí)吧2018-12-12
Java搭建簡(jiǎn)單Netty開(kāi)發(fā)環(huán)境入門(mén)教程
這篇文章主要介紹了Java搭建簡(jiǎn)單Netty開(kāi)發(fā)環(huán)境入門(mén)教程,有詳細(xì)的代碼展示和maven依賴,能夠幫助你快速上手Netty開(kāi)發(fā)框架,需要的朋友可以參考下2021-06-06

