九、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 匹配失败时,会根据请求来尝试模糊匹配,待匹配的匹配方法可参考下图:

spring获取全部的url spring url_默认值


匹配会查询所有的信息,例如 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

四.请求参数格式错误