九、Spring Web URL 解析常见错误
一.当 @PathVariable 遇到 /
1、代码
在解析一个 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 中对应的值。
当访问
http://localhost:8080/hi1/xiao/ming
假设这个 name 中含有特殊字符 / 时,这个接口不会为 name 获取任何值,而是直接报 Not Found 错误。当然这里的“找不到”并不是指 name 找不到,而是指服务于这个特殊请求的接口。
当访问
http://localhost:8080/hi1/xiaoming/
当访问这个服务的时候,xiaoming之后的/会被自动的省略掉。
2、案例解析
实际上,这两种错误都是 URL 匹配执行方法的相关问题,所以有必要先了解下 URL 匹配执行方法的大致过程。
@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()) {
//没有精确匹配上,尝试根据请求来匹配
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.根据 Path 进行精确匹配(匹配的是不带参数的路径)
查询 urlLookup 是一个精确匹配 Path 的过程。很明显,http://localhost:8080/hi1/xiao/ming 的 lookupPath 是"/hi1/xiao/ming",并不能得到任何精确匹配。这里需要补充的是,"/hi1/{name}"这种定义本身也没有出现在 urlLookup 中。
2.假设 Path 没有精确匹配上,则执行模糊匹配.
在步骤 1 匹配失败时,会根据请求来尝试模糊匹配,待匹配的匹配方法可参考下图:
匹配会查询所有的信息,例如 Header、Body 类型以及 URL 等。如果有一项不符合条件,则不匹配。
3.根据匹配情况返回结果
如果找到匹配的方法,则返回方法;如果没有,则返回 null。在本案例中,http://localhost:8080/hi1/xiao/ming 因为找不到匹配方法最终报 404 错误。追根溯源就是 AntPathMatcher 匹配不了"/hi1/xiao/ming"和"/hi1/{name}"。
另外,我们再回头思考 http://localhost:8080/hi1/xiaoming/ 为什么没有报错而是直接去掉了 /。
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;
}
3、问题修正
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);
};
二.错误使用 @RequestParam、@PathVarible 等注解
1、代码分析
@RequestMapping(path = "/hi1", method = RequestMethod.GET)
public String hi1(@RequestParam("name") String name){
return name;
};
@RequestMapping(path = "/hi2", method = RequestMethod.GET)
public String hi2(@RequestParam String name){
return name;
};
将
@RequestParam("name") String name
写成了
@RequestParam String name
这个样做可能会导致,换了一个项目后提示无法匹配。
2、案例解析
就是关闭掉某些参数之后,能会导致无法识别。
3、问题修正
不要偷懒,乖乖写好。
必须显式在 @RequestParam 中指定请求参数名。
@RequestParam("name") String name
三.未考虑参数是否可选
1、代码分析
@RequestMapping(path = "/hi4", method = RequestMethod.GET)
public String hi4(@RequestParam("name") String name, @RequestParam("address") String address){
return name + ":" + address;
};
访问以下代码不会出现问题,因为接口的参数是两个,入参也是两个。
http://localhost:8080/hi4?name=xiaoming&address=beijing
但是访问一下链接就会出现问题,参数少了一个,写代码的时候要考虑某些参数是否是必须的。
http://localhost:8080/hi4?name=xiaoming
2、问题分析
当缺少请求参数的时候,通常我们会按照以下几个步骤进行处理。
1.查看 namedValueInfo 的默认值,如果存在则使用它
查看是不是有默认值
2.在 @RequestParam 没有指明默认值时,会查看这个参数是否必须,如果必须,则按错误处理
查看是不是必选参数,若要判定一个参数是否是必须的,需要同时满足两个条件:条件 1 是 @RequestParam 指明了必须(即属性 required 为 true,默认值为ture),条件 2 是要求 @RequestParam 标记的参数本身不是可选的。
3. 如果不是必须,则按 null 去做具体处理
3、问题修正
1.设置 @RequestParam 的默认值
@RequestParam(value = "address", defaultValue = "no address") String address
2.设置 @RequestParam 的 required 值
@RequestParam(value = "address", required = false) String address)
3. 标记任何名为 Nullable 且 RetentionPolicy 为 RUNTIME 的注解
@RequestParam(value = "address") @Nullable String address
4.修改参数类型为 Optional
@RequestParam(value = "address") Optionaladdress
四.请求参数格式错误