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