做網(wǎng)站找云無限seo查詢在線
1. 問題復(fù)現(xiàn)
-
在解析一個(gè) URL 時(shí),我們經(jīng)常會(huì)使用 @PathVariable 這個(gè)注解。例如我們會(huì)經(jīng)常見到如下風(fēng)格的代碼:
@RestController @Slf4j public class HelloWorldController {@RequestMapping(path = "/hi1/{name}", method = RequestMethod.GET)public String hello1(@PathVariable("name") String name){return name;}; }
-
當(dāng)我們使用 http://localhost:8080/hi1/xiaoming 訪問這個(gè)服務(wù)時(shí),會(huì)返回"xiaoming",即 Spring 會(huì)把 name 設(shè)置為 URL 中對(duì)應(yīng)的值。
-
看起來順風(fēng)順?biāo)?#xff0c;但是假設(shè)這個(gè) name 中含有特殊字符 / 時(shí)(例如http://localhost:8080/hi1/xiao/ming ),會(huì)如何?如果我們不假思索,或許答案是"xiao/ming"?然而稍微敏銳點(diǎn)的程序員都會(huì)判定這個(gè)訪問是會(huì)報(bào)錯(cuò)的,具體錯(cuò)誤參考:
-
如圖所示,當(dāng) name 中含有 /,這個(gè)接口不會(huì)為 name 獲取任何值,而是直接報(bào) Not Found 錯(cuò)誤。當(dāng)然這里的“找不到”并不是指 name 找不到,而是指服務(wù)于這個(gè)特殊請求的接口。
-
實(shí)際上,這里還存在另外一種錯(cuò)誤,即當(dāng) name 的字符串以 / 結(jié)尾時(shí),/ 會(huì)被自動(dòng)去掉。例如我們訪問 http://localhost:8080/hi1/xiaoming/,Spring 并不會(huì)報(bào)錯(cuò),而是返回 xiaoming。
-
針對(duì)這兩種類型的錯(cuò)誤,應(yīng)該如何理解并修正呢?
2. 案例解析
- 實(shí)際上,這兩種錯(cuò)誤都是 URL 匹配執(zhí)行方法的相關(guān)問題,所以我們有必要先了解下 URL 匹配執(zhí)行方法的大致過程。參考 AbstractHandlerMethodMapping#lookupHandlerMethod:
@Nullable protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {List<Match> matches = new ArrayList<>();//嘗試按照 URL 進(jìn)行精準(zhǔn)匹配List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);if (directPathMatches != null) {//精確匹配上,存儲(chǔ)匹配結(jié)果addMatchingMappings(directPathMatches, matches, request);}if (matches.isEmpty()) {//沒有精確匹配上,嘗試根據(jù)請求來匹配addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);}if (!matches.isEmpty()) {Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));matches.sort(comparator);Match bestMatch = matches.get(0);if (matches.size() > 1) {//處理多個(gè)匹配的情況}//省略其他非關(guān)鍵代碼return bestMatch.handlerMethod;}else {//匹配不上,直接報(bào)錯(cuò)return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);}
- 大體分為這樣幾個(gè)基本步驟。
1. 根據(jù) path 進(jìn)行精確匹配
- 這個(gè)步驟執(zhí)行的代碼語句是
this.mappingRegistry.getMappingsByUrl(lookupPath)
,實(shí)際上,它是查詢MappingRegistry#urlLookup
,它的值可以用調(diào)試視圖查看,如下圖所示:
- 查詢 urlLookup 是一個(gè)精確匹配 Path 的過程。很明顯,http://localhost:8080/hi1/xiao/ming 的 lookupPath 是"/hi1/xiao/ming",并不能得到任何精確匹配。這里需要補(bǔ)充的是,"/hi1/{name}"這種定義本身也沒有出現(xiàn)在 urlLookup 中。
2. 假設(shè) path 沒有精確匹配上,則執(zhí)行模糊匹配
- 在步驟 1 匹配失敗時(shí),會(huì)根據(jù)請求來嘗試模糊匹配,待匹配的匹配方法可參考下圖:
- 顯然,"/hi1/{name}"這個(gè)匹配方法已經(jīng)出現(xiàn)在待匹配候選中了。具體匹配過程可以參考方法
RequestMappingInfo#getMatchingCondition
:public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);if (methods == null) {return null;}ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);if (params == null) {return null;}//省略其他匹配條件PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);if (patterns == null) {return null;}//省略其他匹配條件return new RequestMappingInfo(this.name, patterns,methods, params, headers, consumes, produces, custom.getCondition()); }
- 現(xiàn)在我們知道匹配會(huì)查詢所有的信息,例如 Header、Body 類型以及 URL 等。如果有一項(xiàng)不符合條件,則不匹配。
- 在我們的案例中,當(dāng)使用 http://localhost:8080/hi1/xiaoming 訪問時(shí),其中 patternsCondition 是可以匹配上的。實(shí)際的匹配方法執(zhí)行是通過 AntPathMatcher#match 來執(zhí)行,判斷的相關(guān)參數(shù)可參考以下調(diào)試視圖:
- 但是當(dāng)我們使用 http://localhost:8080/hi1/xiao/ming 來訪問時(shí),AntPathMatcher 執(zhí)行的結(jié)果是"/hi1/xiao/ming"匹配不上"/hi1/{name}"。
3. 根據(jù)匹配情況返回結(jié)果
-
如果找到匹配的方法,則返回方法;如果沒有,則返回 null。
-
在本案例中,http://localhost:8080/hi1/xiao/ming 因?yàn)檎也坏狡ヅ浞椒ㄗ罱K報(bào) 404 錯(cuò)誤。追根溯源就是 AntPathMatcher 匹配不了"/hi1/xiao/ming"和"/hi1/{name}"。
-
另外,我們再回頭思考 http://localhost:8080/hi1/xiaoming/ 為什么沒有報(bào)錯(cuò)而是直接去掉了 /。這里我直接貼出了負(fù)責(zé)執(zhí)行 AntPathMatcher 匹配的
PatternsRequestCondition#getMatchingPattern
方法的部分關(guān)鍵代碼:private String getMatchingPattern(String pattern, String lookupPath) {//省略其他非關(guān)鍵代碼if (this.pathMatcher.match(pattern, lookupPath)) {return pattern;}//嘗試加一個(gè)/來匹配if (this.useTrailingSlashMatch) {if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) {return pattern + "/";}}return null; }
-
在這段代碼中,AntPathMatcher 匹配不了"/hi1/xiaoming/“和”/hi1/{name}",所以不會(huì)直接返回。進(jìn)而,在 useTrailingSlashMatch 這個(gè)參數(shù)啟用時(shí)(默認(rèn)啟用),會(huì)把 Pattern 結(jié)尾加上 / 再嘗試匹配一次。如果能匹配上,在最終返回 Pattern 時(shí)就隱式自動(dòng)加 /。
-
很明顯,我們的案例符合這種情況,等于說我們最終是用了"/hi1/{name}/“這個(gè) Pattern,而不再是”/hi1/{name}"。所以自然 URL 解析 name 結(jié)果是去掉 / 的。
3. 問題修正
-
針對(duì)這個(gè)案例,有了源碼的剖析,我們可能會(huì)想到可以先用"**"匹配上路徑,等進(jìn)入方法后再嘗試去解析,這樣就可以萬無一失吧。具體修改代碼如下:
@RequestMapping(path = "/hi1/**", method = RequestMethod.GET) public String hi1(HttpServletRequest request){String requestURI = request.getRequestURI();return requestURI.split("/hi1/")[1]; };
-
但是這種修改方法還是存在漏洞,假設(shè)我們路徑的 name 中剛好又含有"/hi1/",則 split 后返回的值就并不是我們想要的。實(shí)際上,更合適的修訂代碼示例如下:
private AntPathMatcher antPathMatcher = new AntPathMatcher();@RequestMapping(path = "/hi1/**", method = RequestMethod.GET) public String hi1(HttpServletRequest request){String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);//matchPattern 即為"/hi1/**"String matchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); return antPathMatcher.extractPathWithinPattern(matchPattern, path); };
-
經(jīng)過修改,兩個(gè)錯(cuò)誤都得以解決了。當(dāng)然也存在一些其他的方案,例如對(duì)傳遞的參數(shù)進(jìn)行 URL 編碼以避免出現(xiàn) /,或者干脆直接把這個(gè)變量作為請求參數(shù)、Header 等,而不是作為 URL 的一部分。你完全可以根據(jù)具體情況來選擇合適的方案。