先記錄下@PathVariable的用法吧:
@RequestMapping("/demo/{id}")
@ResponseBody
public User getUser(@PathVariable("id")Integer id, HttpServletRequest request){
System.out.println(request.getAttribute(RequestMappingHandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE));
List<User> list=new ArrayList<>();
list.add(new User(0,"A"));
list.add(new User(1,"B"));
list.add(new User(2,"C"));
list.add(new User(3,"D"));
User user = list.get(id);
return user;
}
使用方式一:就像上面那樣{}代表占位符,匹配URL中/ /兩個之間的內容,通過@PathVariable進行解析
使用方式二:通過request的RequestMappingHandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE這個屬性獲取到一個Map,然后根據上面的key進行取值
有時候很好奇Spring @Pathvariable怎么解析的,好像無論多少個 {} 都能正確的映射,看起來好像沒那么難。 但是我腦子不太行,嘗試分析分析看看吧。
就像以前做筆試題:正確答案在下面,雖然我肯定寫不出來,但是能看懂也挺為難我哈哈哈哈。

就像給定兩個輸入, pattern是標准路徑 ,就像 /url/{id} , 而lookupPath就是請求路徑,就像/url/19 ;
如果pattern和lookupPath一樣,就直接返回,這個不難理解,常規URL映射都是這么映射的;
考慮到實際情況以及簡化分析,useSuffixPatternMatch 默認為 true , fileExtensions 默認為空 ,以不帶后綴名形式分析 ,那就會進入AntPathMatcher分析;
protected boolean doMatch(String pattern, String path, boolean fullMatch, Map<String, String> uriTemplateVariables) {
//pattern為標准路徑 /url/{id} path為請求request路徑/url/19
//fullMatch默認為true ; uriTemplateVariables默認為null
//pathSeparator默認為 /
//path和pattern剛開始都是/ 開頭, 肯定是false ; 這一步算是規則校驗 路徑不以/開頭的 直接返回false
if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
return false;
}
//標准路徑/url/{id}分隔成字符串數組 pattDirs
String[] pattDirs = tokenizePattern(pattern);
//isPotentialMatch方法:
//請求路徑 和 @RequestMapping路徑匹配 從頭匹配剛開始就不相等直接返回false
//解析過程前面相等遇到 { * ?類型返回true 這里邏輯等等再具體描述
if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) {
return false;
}
//請求路徑拆分成字符串數組pathDirs
String[] pathDirs = tokenizePath(path);
int pattIdxStart = 0;
int pattIdxEnd = pattDirs.length - 1;
int pathIdxStart = 0;
int pathIdxEnd = pathDirs.length - 1;
//循環遍歷是否 請求路徑字符數組 和 @RequestMapping路徑數組 正則匹配
//{id}的正則表達式被解析為 (.*) 肯定可以匹配上
//字符數組只要有一個元素沒匹配上就返回false
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
String pattDir = pattDirs[pattIdxStart];
if ("**".equals(pattDir)) {
break;
}
if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
return false;
}
pattIdxStart++;
pathIdxStart++;
}
//上面如果匹配完成,pathIdxStart=pathIdxEnd+1 pattIdxStart=pattIdxEnd+1
if (pathIdxStart > pathIdxEnd) {
// Path is exhausted, only match if rest of pattern is * or **'s
if (pattIdxStart > pattIdxEnd) {
// /url/{id} /url/19 就匹配上了到這里 返回true
return (pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) :
!path.endsWith(this.pathSeparator));
}
if (!fullMatch) {
return true;
}
if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {
return true;
}
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
else if (pattIdxStart > pattIdxEnd) {
// String not exhausted, but pattern is. Failure.
return false;
}
else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
// Path start definitely matches due to "**" part in pattern.
return true;
}
// up to last '**'
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
String pattDir = pattDirs[pattIdxEnd];
if (pattDir.equals("**")) {
break;
}
if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
return false;
}
pattIdxEnd--;
pathIdxEnd--;
}
if (pathIdxStart > pathIdxEnd) {
// String is exhausted
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
int patIdxTmp = -1;
for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
if (pattDirs[i].equals("**")) {
patIdxTmp = i;
break;
}
}
if (patIdxTmp == pattIdxStart + 1) {
// '**/**' situation, so skip one
pattIdxStart++;
continue;
}
// Find the pattern between padIdxStart & padIdxTmp in str between
// strIdxStart & strIdxEnd
int patLength = (patIdxTmp - pattIdxStart - 1);
int strLength = (pathIdxEnd - pathIdxStart + 1);
int foundIdx = -1;
strLoop:
for (int i = 0; i <= strLength - patLength; i++) {
for (int j = 0; j < patLength; j++) {
String subPat = pattDirs[pattIdxStart + j + 1];
String subStr = pathDirs[pathIdxStart + i + j];
if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
continue strLoop;
}
}
foundIdx = pathIdxStart + i;
break;
}
if (foundIdx == -1) {
return false;
}
pattIdxStart = patIdxTmp;
pathIdxStart = foundIdx + patLength;
}
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
isPotentialMatch:進一步的過濾規則
private boolean isPotentialMatch(String path, String[] pattDirs) {
// path 請求路徑 pattDirs標准路徑以 / 分隔出來的字符數組
if (!this.trimTokens) {
//請求路徑轉成char數組
char[] pathChars = path.toCharArray();
int pos = 0;
for (String pattDir : pattDirs) {
//請求路徑中第一次從0開始找到/的位置,下次就從上次找到的位置開始找下一個/
int skipped = skipSeparator(path, pos, this.pathSeparator);
pos += skipped;
//skipSegment從pathChars找出跳過pattDir的長度
skipped = skipSegment(pathChars, pos, pattDir);
//skipped最理想情況等於pattDir的長度 但是通常通配符形式這里都是小於
//比如映射中包含demo,路徑為do,這時候skipped也是2 也會返回true,但是之后的正則表達式校驗無法通過
if (skipped < pattDir.length()) {
if (skipped > 0) {
return true;
}
return (pattDir.length() > 0) && isWildcardChar(pattDir.charAt(0));
}
//skipped ==pattDir長度,全匹配上直接匹配下一個/之后內容
pos += skipped;
}
}
return true;
}
//函數作用 待匹配路徑字符數組 pos 代表 /所在的下一個位置 prefix標准路徑
//標准路徑包含wildcardChar { ? * 返回skipped 其他都會返回0
private int skipSegment(char[] chars, int pos, String prefix) {
int skipped = 0;
for (char c : prefix.toCharArray()) {
if (isWildcardChar(c)) {
return skipped;
}
else if (pos + skipped >= chars.length) {
return 0;
}
else if (chars[pos + skipped] == c) {
skipped++;
}
}
return skipped;
}
Spring考慮的很全面,最簡單的 /url/{id} /url/19這種類型匹配完成了;
轉換完成以后,存入request域:以HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE作為KEY存儲
代碼位置:org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping#handleMatch

另外還一種特殊寫法:{name:正則表達式校驗} 比如我只希望URL中id為純數字 \d*即可

補充:如果正則匹配不了,拋出的錯誤是404頁面找不到. 帶:與不帶:的區別在於,不帶:就默認使用 .* 匹配,其他用法沒差別.

差點忘記記錄Spring如何解析@PathVariable注解?
Spring專門的接口HandlerMethodArgumentResolver用來解析方法入參,而PathVariableMethodArgumentResolver就是用來解析 @PathVariable注解的。

而請求參數綁定到 方法入參的方式:
可以看到也是從request屬性域HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE中取值,key就是@PathVariable(“yourname”)中的yourname取值.

