【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路径中的参数绑定到方法参数上。

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

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

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

相关推荐
Elastic 中国社区官方博客1 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1881 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn2 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su2 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
码事漫谈3 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780513 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123453 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒3 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记4 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598784 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端