写代码时突发奇想,为什么要分开 @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呢?
- 当请求参数名与形参变量名不一致时
如下,请求参数名为 userId,形参变量名为 id,两者不一样
java
@GetMapping("/users")
public Result search(@RequestParam("userId") Integer id){
}
// 也可以写成如下,多写个name属性
@GetMapping("/users")
public Result search(@RequestParam(name = "userId") Integer id){
}
- 当变量不一定会传入时
-
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){
}
- 如果没传参数,就给默认值
java
@GetMapping("/users")
public Result search(@RequestParam(defaultValue = "1") Integer id){
}
- 请求参数的类型是一个数组(前端没有集合,只有数组),当要用一个集合来接收数据的时候,因为类型不一致所以这个时候需要用@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" 路由
解决方法:
- 写两个不同的接口(推荐)
如果有传 id,就找第一个接口;
如果没有传 id,就找第二个接口。
java
@GetMapping("/users/{id}")
public Result search(@PathVariable(required = false) Long id){}
@GetMapping("/users")
public Result searchList(){}
但这个解决方法写不写 required = false 都没有区别,因为 required = false 压根没有起作用。
- 同一个 @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
适用场景:用于传递额外的信息或过滤条件,用来进一步细化请求,如筛选、排序或分页等,定位的资源可能并不唯一。
作用:
-
多用于Get请求 参数,url 地址传参。
位置:查询参数是附加在URL末尾的,位于URL路径之后
例如:/orders?page=1&pageSize=5&status=1,查询分页大小为5,status 为 1的第一页订单列表
-
或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路径中的参数绑定到方法参数上。
本来只是想写查询参数与路径参数有什么区别,结果写了这么多,给大家做了许多扩展,还给自己挖了这么多坑 ̄へ ̄
果然要讲清一个知识点的前提,需要许多知识的补充
希望本篇博客对你有所帮助
