【Spring】RequestParam/PathVariable/RequestBody详解

写代码时突发奇想,为什么要分开 @RequestParam / @PathVariable 两套注解

  • @RequestParam 是:url?a=1&b=2

    • 以问号?开始是查询参数,多个参数之间用&分隔,格式为 key=value
  • @PathVariable 是:url/1

    • /后面是路径参数

为了帮大家回忆一下,先带大家看几段代码:

我们通常接收参数可能直接写形参,而不会写上@RequestParam。

因为底层:简单类型(String、Integer、Long 等)无注解时,SpringMVC 使用RequestParamMethodArgumentResolver解析,效果类似于 @RequestParam

java 复制代码
@GetMapping("/users")
public Result search(Integer id){ // 没有写@RequestParam
}

那什么时候需要写@RequestParam呢?

  1. 当请求参数名与形参变量名不一致时

如下,请求参数名为 userId,形参变量名为 id,两者不一样

java 复制代码
@GetMapping("/users")
public Result search(@RequestParam("userId") Integer id){
}

// 也可以写成如下,多写个name属性
@GetMapping("/users")
public Result search(@RequestParam(name = "userId") Integer id){
}
  1. 当变量不一定会传入时
  • Integer id 类似于 @RequestParam(required = false) Integer id,此时参数可以选择性传入,不传则值为null;

  • @RequestParam Integer id 类似于 @RequestParam(required = true) Integer id,此时参数必须传入,不然报错400。

java 复制代码
@GetMapping("/users")
public Result search(@RequestParam(required = false) Integer id){
}
  1. 如果没传参数,就给默认值
java 复制代码
@GetMapping("/users")
public Result search(@RequestParam(defaultValue = "1") Integer id){
}
  1. 请求参数的类型是一个数组(前端没有集合,只有数组),当要用一个集合来接收数据的时候,因为类型不一致所以这个时候需要用@RequestParam 来帮助识别

至于底层原理,涉及到 SpringMVC 参数解析器,是个面试考点,前两天刷面经还看到了,所以我会单独出篇博客去讲解,这里就不过深涉及。

java 复制代码
// 用数组接收不需要写@RequestParam
public Result delete(Integer[] ids){}
java 复制代码
// 用集合接收需要写@RequestParam
public Result delete(@RequestParam List<Integer> ids){}

已经帮大家回忆了 @RequestParam 的用法,那回归主题,我的问题是:

为什么不统一写成 url?id=1,全部丢给查询参数,而是要分成查询参数和路径参数?它们分别的适用场景是什么?

  • @PathVariable

适用场景:用于定位某一个具体资源 ,或资源存在层级 / 父子关系时,就适合用该注解

参数个数:参数一般较少,一般建议1-3个

例如:

复制代码
1. /orders/88,查询 id 为88的订单,定位到唯一资源
2. /user/3/article/10,体现用户 - 文章的层级

位置:路径参数是URL路径的一部分

知识补充第一趴

@PathVariable(required = false),很多人认为写上 (required = false) 就可以不传路径参数,这是个很大的误区,这与 @RequestParam(required = false)不一样。

举个例子:

java 复制代码
@GetMapping("/users/{id}")
public Result search(@PathVariable(required = false) Long id){}

如果传 id,匹配路径为 "/users/{id}" 查询单个用户;

如果不传 id,那匹配路径为 "/users" 查询列表。

但实际上不传 id,会直接报错 404,因为找不到 "/users" 路由

解决方法:

  1. 写两个不同的接口(推荐

如果有传 id,就找第一个接口;

如果没有传 id,就找第二个接口。

java 复制代码
@GetMapping("/users/{id}")
public Result search(@PathVariable(required = false) Long id){}

@GetMapping("/users")
public Result searchList(){}

但这个解决方法写不写 required = false 都没有区别,因为 required = false 压根没有起作用

  1. 同一个 @GetMapping 映射模板同时覆盖 "有路径参数" 和 "没有路径参数" 的两个URL地址,多个地址映射。

这样 required = false 就会起作用:某个路径变量在 URI 中缺失时,是否允许参数为 null。

required = false 影响的是 "路径参数解析" 而不是 "匹配路径",匹配路径成功后才会参数解析。

java 复制代码
@GetMapping({"/users", "/users/{id}"})
public Result search(@PathVariable Long id) {
    if (id == null) {
        // 处理/users的请求,例如查询列表
    } else {
        // 处理/users/1的请求,例如查询单个用户
    }
}

如果你没看懂上面的话,那我们来看个例子。

假设不写 required = false

java 复制代码
@GetMapping({"/users", "/users/{id}"})
public Result search(@PathVariable Long id) { //不写required = false
    if (id == null) {
        // 处理/users的请求,例如查询列表
    } else {
        // 处理/users/1的请求,例如查询单个用户
    }
}

发请求 /users/1 会匹配到 "/users/{id}" 这个映射,成功进入 else 分支;

发请求 /users 会匹配到 "/users" 这个映射,但 @PathVariable Long id 找不到路径变量,会报错500,不会进入 if (id == null) 分支。

如果加了 required = false ,就会把 id 设置为 null,而不会抛异常。

此方法的问题:类型不安全、代码丑、易出错,所以一般不建议

知识补充第一趴2.0

SpringMVC 有个强大的功能,路径变量可以和正则表达式结合在一起,使用正则进行参数校验。

语法:参数:正则表达式

例如:

java 复制代码
// 只匹配纯数字的 id,如 /users/123 可以,/users/abc 则返回 404
@GetMapping("/users/{id:\\d+}")
public Result getUser(@PathVariable Long id) {}
  • @RequestParam

适用场景:用于传递额外的信息或过滤条件,用来进一步细化请求,如筛选、排序或分页等,定位的资源可能并不唯一。

作用:

  1. 多用于Get请求 参数,url 地址传参

    位置:查询参数是附加在URL末尾的,位于URL路径之后

    例如:/orders?page=1&pageSize=5&status=1,查询分页大小为5,status 为 1的第一页订单列表

  2. POST请求 ,通常是表单传参

    位置:放在请求体 body里,URL 不带参数

    Content-Type 一般为 application/x-www-form-urlencoded,上传文件时则是 multipart/form-data

注意:URL 查询参数 和 表单参数 可以同时存在,Spring 会合并两组参数,@RequestParam 可以同时拿到两边的值。

参数:参数可以多个,通常可选

  • @RequestBody

既然前两个注解都讲了,那把@RequestBody也讲一下吧,保证知识的完整性。

@RequestBody 用于接收 HTTP 请求体中的数据 ,请求头包含 Content-Type: application/json。Spring 会通过消息转换器(如 Jackson)将前端传来的 JSON 或 XML 数据自动反序列化为后端的 Java 对象。

适用场景:参数较多、结构复杂的场景,例如:新增用户、修改资料、用户登录、传递嵌套的复杂对象等。

GET 请求不要用 @RequestBody:虽然 HTTP 协议没有绝对禁止,但在实际开发中,GET 请求的标准做法是使用 @RequestParam 或 @PathVariable。因为很多反向代理、网关、CDN 收到 GET 请求时,直接丢弃请求体,根本不会转发到后端 Spring 服务。

有聪明的小伙伴肯定会说:嘿嘿,登录方法用的是 Post 请求而不是 Get 请求是不是这个原因呢。因为登录方法的参数就使用@RequestBody了。

java 复制代码
@PostMapping("/user/login")
public Result userLogin(@RequestBody User user){
    log.info("登录:{}",user);
    UserLoginInfo userLoginInfo = userService.login(user);
    if (userLoginInfo!=null) {
        return Result.success(userLoginInfo);
    }
    return Result.error("用户名或密码错误");
}

答曰:这只能算是个次要原因,主要原因是安全问题,有空出篇博客讲讲。

注意:同一个接口方法,最多只能写 1 个 @RequestBody 默认 @RequestBody(required = true),请求体为空时抛出 HttpMessageNotReadableException,报错400。

java 复制代码
// 写两个@RequestBody,错误!
public void test(@RequestBody User user, @RequestBody Order order){}

但可以和其他注解共存

允许:1 个 @RequestBody + 多个 @RequestParam / @PathVariable

java 复制代码
public Result save(
        @RequestBody UserDTO userDTO,
        @RequestParam String token,
        @PathVariable Long id
){}

知识补充第二趴

当提交 application/x-www-form-urlencoded(不是 JSON,不是 form-data)或者查询参数时,用 POJO 实体类作为方法参数,且不写任何注解,Spring 会自动映射同名字段。此时不是默认用 @RequestParam,而是隐式使用 @ModelAttribute,底层使用 ModelAttributeMethodProcessor 解析器(之后可能会出相关的博客讲解,这里不多涉及)。

适用于表单字段很多的情况,不用一个个写 @RequestParam,直接用 POJO 接收。

如果是嵌套对象需要使用点分隔符来绑定:

java 复制代码
@Data
public class User {
    private String name;
    private Address address; // 嵌套对象
}
@Data
public class Address {
    private String city;
}

前端传参要写成:name=张三&address.city=北京

什么时候会用到以上情况:就是我们最常见的条件分页查询

GET 请求不能用 @RequestBody,多个参数一个一个写 @RequestParam 又很麻烦,也不方便维护管理,所以要用分页实体类

  • 一个一个写参数
java 复制代码
@GetMapping
public Result page(@RequestParam(defaultValue = "1") Integer page,
                   @RequestParam(defaultValue = "10") Integer pageSize,
                   String name, Integer gender,
                   @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
                   @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
    log.info("查询请求参数: {}, {}, {}, {}, {}, {}", page, pageSize, name, gender, begin, end);
    PageResult pageResult = empService.page(page, pageSize);
    return Result.success(pageResult);
}
  • 定义实体类
java 复制代码
@Data
public class EmpQueryParam {
    private Integer page = 1; //页码
    private Integer pageSize = 10; //每页展示记录数
    private String name; //姓名
    private Integer gender; //性别
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate begin; //入职开始时间
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate end; //入职结束时间
    
}

这里 EmpQueryParam 参数用的是隐式 @ModelAttribute,而不是 @RequestParam

java 复制代码
@GetMapping
public Result page(EmpQueryParam empQueryParam) {
    log.info("查询请求参数: {}", empQueryParam);
    PageResult pageResult = empService.page(empQueryParam);
    return Result.success(pageResult);
}

等价于:

java 复制代码
public Result page(@ModelAttribute EmpQueryParam empQueryParam)

另外一提:这里的 @DateTimeFormat 注解怎么生效?

解析 URL 查询参数 /x-www-form-urlencoded 表单参数都可以,它可以单独标记在简单类型参数上(@RequestParam),也可以标记在 POJO 的字段上(@ModelAttribute)。但它不能作用于 @RequestBody JSON 解析场景 (要用 @JsonFormat)。

小总结:

若绑定的是简单参数,就用@RequestParam

若绑定的是复杂参数,就用@ModelAttribute

  • 总结

@RequestParam:用于绑定HTTP请求中的查询参数或表单参数到方法参数上。

@RequestBody:用于将HTTP请求体中的内容绑定到方法参数上。

@PathVariable:用于将URL路径中的参数绑定到方法参数上。

本来只是想写查询参数与路径参数有什么区别,结果写了这么多,给大家做了许多扩展,还给自己挖了这么多坑 ̄へ ̄

果然要讲清一个知识点的前提,需要许多知识的补充

希望本篇博客对你有所帮助

相关推荐
Charlie_Byte1 小时前
Hugo 相对路径图片显示问题的分析与解决
后端
Escalating_xu1 小时前
【mmap 进程间通信】从共享内存到跨进程唤醒:用互斥锁、条件变量控制多个进程
java·linux·开发语言·jvm
Charlie_Byte1 小时前
用 conda 固定默认环境 normal
后端
泡海椒1 小时前
脚本包导入实战:JQuick-Java自定义包引入与别名配置教程
java·开发语言·python
隐退山林1 小时前
JavaEE进阶:SpringBoot统一功能处理
java·spring boot·后端
Charlie_Byte1 小时前
Py 邮件集成与密码重置,并用 Mailpit 测试
后端
Charlie_Byte1 小时前
Webhook 与飞书机器人集成
后端
木井巳1 小时前
【记忆化搜索】斐波那契数
java·算法·leetcode·深度优先·剪枝·推荐算法
源代码•宸2 小时前
前置准备:架构设计方法论
经验分享·后端