先记录下@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注解的。
而请求参数绑定到 方法入参的方式: