先記錄下@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注解的。
而請求參數綁定到 方法入參的方式: