关于`@RequestParam`和`@PathVariable`的浅层思考

338 阅读8分钟

关于@RequestParam@PathVariable的浅层思考

做为一名后端工程师,在开发过程中有很多困扰我挺久的问题,其中之一便是有了@RequestParam注解,为什么有@PathVariable,以及为什么要有@PathVariable,显而易见的是@PathVariable是为了支持RESTful风格API的一种便利方式,那么又抛出来一个问题,我们为什么需要有RESTful风格API呢?接下来我们就展开看下

两个注解的功能

首先我们来看下这两个注解的功能,下面是使用 @RequestParam@PathVariable 注解的示例:

使用 @RequestParam 注解:

/**
 * 获取用户详情
 *
 * @param id 用户 ID
 * @return 用户信息
 */
@GetMapping("/users")
public User getUserById(@RequestParam Long id) {
  // 根据 ID 获取用户信息
  return userService.getById(id);
}

使用 @PathVariable 注解:

/**
 * 获取用户详情
 *
 * @param id 用户 ID
 * @return 用户信息
 */
@GetMapping("/users/{id}")
public User getUserById(@PathVariable Long id) {
  // 根据 ID 获取用户信息
  return userService.getById(id);
}

在这两个示例中,我们使用了 @GetMapping 注解来声明这是一个 HTTP GET 请求,并使用 @RequestParam@PathVariable 注解将请求参数和路径变量绑定到方法参数上。

两者的联系与区别

  • 使用 @RequestParam 注解:完整的示例 URL:http://localhost:8080/users?id=1
  • 使用 @PathVariable 注解:完整的示例 URL:http://localhost:8080/users/1(RESTful)风格

区别:在上述例子上,我们发现 @RequestParam@PathVariable 的功能其实一样,区别只是在于前者接收参数的方式不构成URL本身,而后者的参数构成URL本身。

联系:两者都是接收参数的一种方法,而且,可以看到在实际开发中使用两者都支持常用的GET,POST,DELETE,PUT(也就是两种方法都支持诸如@GetMapping的注解)。而注重区分请求的方法(method)类型正是RESTful风格的一大特点。

使用注意

但是需要注意到的是在上述代码中,我们可以通过注解的参数,修改请求的参数是否为可选的,也就是将代码变为如下的代码同时去掉请求的参数

  • @RequestParam ,我们同样使用 http://localhost:8080/users进行请求时候,会走到if (id == null)到判空逻辑

    @GetMapping("/users")
    public User getUserById(@RequestParam(required = false) Long id) {
        // 判断用户id是否为空
        if (id == null){
            // 抛出异常
            throw new RuntimeException("用户ID为空");
        }
        // 根据 ID 获取用户信息
        return userService.getById(id);
    }
    
  • @PathVariable ,而在这里http://localhost:8080/users,此时并无法走到判空逻辑,而是直接会404报错误,因为之前的参数值1应该本身作为URL的一部分,没有参数的URL是不完整的

    @GetMapping("/users/{id}")
    public User getUserById(@PathVariable(required = false) Long id) {
        // 此时并不会进入到该方法
        if (id == null) {
            throw new RuntimeException("用户ID为空");
        }
        // 根据 ID 获取用户信息
        return userService.getById(id);
    }
    

当然,诸如此类我们也有很多解决的办法,但是这些办法可能会带来一些副作用,比如在参数注解中,我们可以传入多个值,让无参URL也能进行匹配,不过很明显的是这增加了我们的一些工作量,并且带来了额外的思考成本。

@GetMapping({"/users/{id}", "/users"})
public User getUserById(@PathVariable(required = false) Long id) {
    // 此时并不会进入到该方法
    if (id == null) {
        throw new RuntimeException("用户ID为空");
    }
    // 根据 ID 获取用户信息
    return userService.getById(id);
}

不仅如此@RequestParam 还可以提供默认值,在有分页需求的时候,可以使用defaultValue给分页参数赋默认值

风格对比

既然两个注解主要的区别在于是否支持RESTful,我们来对比一下两个风格的主要差别,这里是两个RESTful风格和传统风格的请求示例:通过用户ID对用户进行增删改查

  • RESTful风格:

    • 获取用户信息:GET http://localhost:8080/users/12
    • 创建新用户:POST http://localhost:8080/users
    • 更新用户信息:PUT http://localhost:8080/users/12
    • 删除用户:DELETE http://localhost:8080/users/12
  • 传统风格:

    • 获取用户信息:GET http://localhost:8080/getUser?id=12
    • 创建新用户:POST http://localhost:8080/createUser
    • 更新用户信息:POST http://localhost:8080/updateUser?id=12
    • 删除用户:POST http://localhost:8080/deleteUser?id=12

使用POST,DELETE,PUT和GET四种请求方式分别对指定的URL资源进行增删改查操作。 因此,RESTful是通过URI实现对资源的管理及访问,具有扩展性强、结构清晰的特点

在上述示例,我们可以看到,RESTful通过请求的方法类型,描述了API的行为(动作),而传统风格主要依靠URL本身,另外则是参数的传递方式,这个有在上述讲到。

结论

一个新事物的出现,通常是为了解决旧事物的痛点,显而易见传统风格在于对API的定义上比较混乱,结构不够清晰,依赖URL传达行为并不直观,甚至很多时候API很长的情况下显得更为混乱。而显而易见,RESTful风格对于API的描述更加清晰明了。

新增一个商城模块下商品评论的回复,这样的需求我们就很难定义,甚至不同的程序员定义出不同风格的APi:

  1. POST http://localhost:8080/store/discuss/create-reply
  2. POST http://localhost:8080/store/discuss/createReply
  3. POST http://localhost:8080/create/store/discuss/reply
  4. POST http://localhost:8080/store/discuss/reply/create

如果改为RESTful风格,则一般是这种结果:POST http://localhost:8080/store/discuss/reply

在一个团队中,使用同一种风格的代码、定义同一种风格的API重要性这里不再赘述。

当然有同学会说,传统风格中,我们也可以规范出一种风格。没错这当然是可以的,无非增加一些我们一些讨论成本罢了,更重要的是很有可能在如果我们往一个更好用的风格的目的去讨论的话,那么说不定我们也讨论不出一种比RESTful更好的风格。

当然啦,适合自己团队的风格才是最好的风格,无论我们是否采用RESTful都可以将它作为一种指导,它的出现只是为了解决传统风格的一些痛点。

综上:之所以需要@PathVariable,是为了支持RESTful风格API,之所以需要RESTful是因为它解决了传统风格API接口定义结构不够清晰,容易混乱的痛点。但是@PathVariable支持RESTful并非不需要付出任何代价,如在使用注意里讲到,在一些情况下有404的风险。

可能更佳的方案

显而易见的RESTful风格是一个很好的方案,但是在我看来@PathVariable注解并不是一个方便的注解,至少相当于@RequestParam它丧失了默认参数的能力,在根据参数是否为null进行逻辑判断时候还有404的风险。但不幸的是@PathVariable是支持RESTful的必要不充分条件,这像是一个鱼和熊掌的问题。

那么我不禁考虑,我们是否可以放弃一些RESTful的特性将两者结合呢?不难发现

  1. @PathVariable只是进行参数接收的区别
  2. 接口的方法类型是由诸如@GetMapping来定义
  3. 接口的URL由开发者自行定义

那么我们完全可以通过放弃第1点,第2、3点按照RESTful的方式来定义API。

如删除用户接口POST http://localhost:8080/deleteUser?id=12,我们不妨改为DELETE http://localhost:8080/users?id=12

这样,既保留了部分RESTful风格,还可以利用@RequestParam来方便编码,使用其特性

小拓展

两个注解都是接收参数的,而根据ID获取详情是一个非常常见的需求,如在商城下我们常有根据商品ID获取商品详情的需求,同时为了防止缓存穿透,我们需要在接收到参数后,进行参数验证,验证该商品ID是否存在。通常的验证方法是使用布隆过滤器,这里不再展开。我想聊的是关于这个需求的设计方案。

  • 在拦截器中进行

    1. 将所有商品ID缓存到布隆过滤器

    2. 定义一个注解,在需要验证的方法上面标注该注解

    3. 定义一个拦截器,在请求接口时候,通过反射获取是否存在该注解

    4. 若存在注解,则将参数放进布隆过滤器验证

      1. 此时如果我们此时使用的是@RequestParam注解,就可以很容易的通过参数名的方式获取请求
      2. 如果你使用的是@PathVariable,则稍微麻烦一些,具体方法这里不展开
  • 使用AOP在请求接口拿到参数后进行验证

    1. 将所有商品ID缓存到布隆过滤器
    2. 定义一个注解,在需要验证的方法上面标注该注解
    3. 定义一个切面,切入标注了注解的方法,并织入验证逻辑
    4. 在携带商品ID请求相应接口(标注该注解的接口)时候,会先进行切面的验证逻辑
    5. 此时我们可以通过AOP的特性中获取方法请求的参数
    6. 将参数放进布隆过滤器验