SpringBoot接口参数注解怎么选?@RequestBody和@RequestParam区别详解
作者:tokex
Spring Boot 里写接口,绕不开 @RequestBody 、 @PathVariable 、 @RequestParam 和 @RequestHeader 这四个注解。我当年刚接触 Spring Boot 的时候,总觉得它们长得差不多,用起来也是凭感觉,代码里今天加一个注解,明天换一个注解,直到被一个"参数明明传了但接口死活收不到"的 Bug 逼着把源码翻了一遍,才算彻底弄明白这几兄弟的分工。
这篇文章就把我摸爬滚打总结出来的心得整理出来。
核心思路只有一句话:
先把 HTTP 请求本身的结构搞清楚,再看这四个注解各自对应哪一块数据,基本就不会再选错。文章适合刚入门 Spring Boot 的开发者,也适合写了两年接口但一直没系统梳理过参数绑定的同学。
看完你至少能解决三个问题:
- 第一,面对不同场景知道该用哪个注解;
- 第二,知道每个注解常用的属性和隐藏坑;
- 第三,遇到参数取不到、报 400、报 415 这类问题,能自己顺着思路排查,而不是瞎猜。
1. 参数从哪里来:先理清 HTTP 请求的结构
很多人搞混注解,不是因为注解本身难,而是对"HTTP 请求到底长什么样"没有一个直观的印象。
这就好比你要去一个小区取快递,你得先知道快递放在 1 号楼还是 2 号楼,是放在丰巢柜还是放门口。
四个注解对应的是不同的"取件位置",位置都搞不清楚,自然容易拿错。
1.1 请求行、请求头和请求体
一个标准的 HTTP 请求,拆开来看可以分成三个部分。
第一部分是请求行,也就是请求的第一行,里面包含请求方法、请求路径和协议版本。
比如:
GET /api/users/123?page=1&size=10 HTTP/1.1
这一行里其实藏了两类数据:一类是路径中的参数,比如 /api/users/123 里的 123 ,这就是路径参数,也就是 @PathVariable 要处理的东西;另一类是 ? 后面的 page=1&size=10 ,这是查询参数,也就是 @RequestParam 的地盘。
第二部分是请求头,也就是从第二行开始一直到空行结束的键值对。比如 Host 、 User-Agent 、 Content-Type 这些,它们描述的是请求的元信息。你要读这些信息,就得用 @RequestHeader 。
第三部分是请求体,也就是空行后面的内容。通常 POST 、 PUT 请求会把业务数据放在这里,JSON、表单、XML 都可能。要解析这部分数据,用的就是 @RequestBody 。
你看,四类数据放的位置清清楚楚。搞懂了请求的结构,注解的选择其实已经完成了一大半。
1.2 四个注解与数据位置的映射关系
我做了个项目里一直在用的对照关系,建议你直接存下来:
| 注解 | 数据来源 | 典型请求示例 | 主要用途 |
|---|---|---|---|
| @PathVariable | URL 路径中 | /api/users/123 | RESTful 资源定位 |
| @RequestParam | URL 查询字符串或表单 | /api/users?page=1 或 POST 表单 | 分页、筛选、简单入参 |
| @RequestBody | 请求体 | POST /api/users 带 JSON | 复杂对象、批量数据 |
| @RequestHeader | 请求头 | Authorization: Bearer xxx | 认证信息、客户端标识 |
这个表格我在团队内部分享过很多次,基本能解决 80% 的"该用哪个注解"的疑问。
1.3 为什么初学者容易把参数位置搞混
除了对 HTTP 结构不熟悉,还有一个原因:Spring Boot 在参数绑定上做了太多"宽容处理",反而让人产生误解。
比如 @RequestParam 既能拿 URL 里的查询参数,又能拿 application/x-www-form-urlencoded 类型的表单字段; @RequestBody 则经常被写成了 @RequestParam ,因为有些同事觉得"反正都是传参数"。等接口写完,用 POST 工具测试,传的是 JSON,后端的 @RequestParam 当然拿不到值,于是怀疑人生。
更隐蔽的是,早期版本的 Spring 还允许 @RequestParam 在某些情况下自动从 multipart 请求中取数据,这又让边界变得模糊。所以我的建议是: 不要在边界上靠猜,先把每个注解的职责边界划清楚 。
落一下地:
- 需要定位资源,用
@PathVariable。 - 需要过滤、分页、排序,用
@RequestParam。 - 需要传递完整的对象结构,用
@RequestBody。 - 需要读调用方附加的元信息,用
@RequestHeader。
想明白这四句话,后面写接口会顺很多。
2. @PathVariable:路径上的参数怎么取
@PathVariable 大概是四个注解里最好理解的一个,因为它处理的参数是"看得见"的——就写在 URL 路径里。
2.1 路径参数的基础用法
假设你要写一个根据用户 ID 查询用户详情的接口:
@GetMapping("/api/users/{id}")
public Result<UserVO> getUser(@PathVariable("id") Long id) {
User user = userService.getById(id);
return Result.ok(convert(user));
}
请求 /api/users/10086 ,Spring 会把路径模板里 {id} 对应的 10086 解析出来,绑定到方法参数 id 上。这里有几个细节值得注意。
路径模板的变量名和方法参数名不一致时,必须显式指定名字:
@GetMapping("/api/users/{userId}")
public Result<UserVO> getUser(@PathVariable("userId") Long id) { ... }
如果不写 ("userId") ,Spring 会按照参数名去匹配路径变量。
如果你的项目开启了 -parameters 编译参数,编译器会把参数名保留在字节码里,那么变量名一致时可以省略;但如果没开启,反射拿到的参数名是 arg0 ,匹配就失败了。
所以我个人的习惯是: 路径变量名和参数名不一致时,永远写清楚映射名 ,不赌编译器配置。
2.2 参数与路径变量名不一致时的坑
这个坑我踩过一次。
当时项目里有个接口,路径写的是:
@GetMapping("/api/order/{orderNo}")
public Result<OrderVO> getOrder(@PathVariable String orderNo) { ... }
测试一切正常。
后来有个同事重构,把路径变量改成了 {orderId} ,方法参数也顺手改成了 orderId ,但映射名没带上:
@GetMapping("/api/order/{orderId}")
public Result<OrderVO> getOrder(@PathVariable String orderNo) { ... }
启动时不报错,运行时接口直接变成 orderNo 为 null ,查了半天才发现参数名对不上。这类问题连编译期都发现不了,只能在联调阶段暴露。所以写完接口,最好第一时间用浏览器或 Postman 验证一下路径变量是否真的能取到。
2.3 RESTful 接口设计中的路径参数
实际项目中,路径参数最常见的就是资源 ID、订单号、商品编码这类"唯一标识"。
设计 RESTful 接口时,我喜欢遵循一个原则:路径表达"是什么资源",查询参数表达"怎么筛选"。
@GetMapping("/api/products/{productId}/reviews")
public Result<List<ReviewVO>> listReviews(@PathVariable Long productId,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size) {
这个接口的语义是"获取某个商品下的评论列表", productId 是资源定位, page 、 size 是资源集合的筛选条件。二者分得很清楚。
另外,路径参数支持正则约束,可以用在需要限定格式的场景:
@GetMapping("/api/orders/{orderNo:\\d{12,20}}")
public Result<OrderVO> getOrder(@PathVariable String orderNo) { ... }
这里只允许 12 到 20 位的数字订单号,其他格式直接 404,不进方法体。
这个特性我用来挡掉一部分伪造参数,实测效果不错,但注意别把正则写得太复杂,否则代码阅读成本会变高。
3. @RequestParam:查询参数与表单参数的正确打开方式
@RequestParam 是使用频率最高的注解之一,因为几乎每个接口都要分页、筛选、附带一些简单字段。
3.1 基础用法与属性配置
看一个最常见的写法:
@GetMapping("/api/users")
public Result<PageResult<UserVO>> listUsers(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String keyword) {
return Result.ok(userService.pageQuery(page, size, keyword));
}
三个属性值得逐一说明。
required 表示参数是否必传。默认是 true ,也就是说不传就直接报 400 Bad Request 。但实际接口里,筛选条件多半是可选的,所以我会显式加 required = false 。
defaultValue 表示默认值。
注意, 一旦设置了 defaultValue , required 属性就隐式变成 false ,因为 Spring 认为"有默认值就不算缺失"。
比如上面代码里的 page 和 size ,不传也能正常跑。
还有个很容易忽略的点:
参数名如果不写,Spring 会按照方法参数名自动匹配。在 GET 请求中,查询参数的 key 必须和方法参数名一致。
如果你用的 IDE 编译配置里没有保留参数名,就有可能出现传 ?name=张三 但方法签名是 @RequestParam String userName 导致拿不到值的情况。所以跨团队协作或者写公共接口时,建议把 name 指定清楚:
@GetMapping("/api/users")
public Result<?> list(@RequestParam(name = "userName", required = false) String userName) { ... }
3.2 用集合接收同名参数
还有一种常见场景:前端筛选多个状态值,比如 ?status=1&status=2&status=3 。用 @RequestParam 接收时,可以这样写:
@GetMapping("/api/orders")
public Result<?> listOrders(@RequestParam(name = "status", required = false) List<Integer> statusList) {
...
}
Spring 会自动把多个同名参数聚合成一个 List 。这个技巧我在处理多选筛选时经常用,代码简洁,前端也省事。
如果没有 List 接收,只是单一参数,那么多个同 value 只会拿到第一个,这也是容易踩的一个隐性坑。
建议对选择性不确定的接口,直接约定用 List 接收更稳妥。
3.3 @RequestParam 与表单提交的边界
很多人以为 @RequestParam 只能用于 GET 请求,这其实是个误解。它也能用于 POST 请求,但有限定条件:Content-Type 必须是 application/x-www-form-urlencoded (URL 编码表单)。也就是 HTML 表单默认的提交格式。
实际开发中,有一种"一半 JSON 一半表单"的接口设计容易出问题。比如后端方法这么写:
@PostMapping("/api/search")
public Result<?> search(@RequestParam String keyword,
@RequestBody SearchQuery query) { ... }
前端用 POST 提交,Content-Type 要是 application/json ,那么 keyword 这个 @RequestParam 是拿不到的,因为 JSON 请求体里没有 URL 编码表单字段。
如果非要用这种混合模式,要么前端把 keyword 也放进 JSON,要么加上一个额外的表单字段,在后端同时用两个参数接收。
但说实话,这种设计非常反直觉,我一般不建议这么做。与其这样,不如把参数都塞进一个 DTO,用 @RequestBody 统一接收,接口契约明确得多。
4. @RequestBody:请求体的 JSON 解析
@RequestBody 是后端接收 JSON 最常用的方式,也是参数绑定体系里最"重量级"的一个。
4.1 JSON 序列化与 Java 对象
@RequestBody 做的事其实分两步:
第一步拿到请求体中的原始字节流,
第二步通过消息转换器(HttpMessageConverter)把 JSON 字符串映射成 Java 对象。
@PostMapping("/api/users")
public Result<UserVO> createUser(@RequestBody @Valid UserCreateDTO dto) {
return Result.ok(userService.create(dto));
}
这里的 UserCreateDTO 是一个用来承载前端提交数据的对象。我见过不少团队直接把实体类 User 当作接收对象,这样写虽然能跑,但隐患很大。
比如数据库里有个 createTime 字段,前端提交的 JSON 里万一含了这个字段,你根本控制不住它会不会被动改掉。更稳妥的做法是 为每次写入创建独立的 DTO ,只包含你希望客户端传入的字段。
配合 @Valid 能做参数校验:
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@Email
private String email;
}
一旦校验失败,Spring 会抛出 MethodArgumentNotValidException ,可以在全局异常处理器里统一转成友好提示。
4.2 与 @RequestParam 的取舍边界
这两个注解是"误用重灾区",我总结了一套简单有效的判断逻辑。
- 如果前端传的是 JSON 对象、嵌套对象、数组,那只能用
@RequestBody。 - 如果传的是单个字段、字符串、数字,用
@RequestParam更轻。 - 如果参数数量超过三四个,且字段结构固定,优先考虑 DTO +
@RequestBody。
我见过一个反面例子:有人写了一个注册接口,参数有用户名、密码、邮箱、手机号、昵称、头像地址,后端方法签名直接写了 6 个 @RequestParam String 。这接口用起来痛苦不说,改动字段还要改方法签名,测试用例也跟着改。后来我重构成了 RegisterDTO ,一个对象搞定,舒服很多。
4.3 多个对象传参的三种实用方案
@RequestBody 有一个限制: 一个方法里只能有一个 。但实际业务中,前端往往要同时提交"用户信息"和"订单信息",怎么办?我常用三种方案。
第一种:定义一个"组合 DTO"。
public class UserOrderCreateDTO {
private UserCreateDTO user;
private OrderCreateDTO order;
}
这种方案最干净,但也最笨重。如果只是偶尔混合传参,能接受。
第二种:用 Map<String, Object> 接收,再手动转换。灵活性高但失去了类型安全,不推荐在正式接口里用,调试时最费劲。
第三种:借助 @RequestPart 配合 @RequestBody 。注意, @RequestPart 更常用于文件上传时附带 JSON 数据,比如:
@PostMapping("/api/import")
public Result<?> importData(@RequestPart("file") MultipartFile file,
@RequestBody @Valid ImportConfigDTO config) { ... }
这个模式适合"一个文件 + 一份配置"的导入场景,真实项目我用过好几次。
总结一下:编码风格不骚,能用一个对象就不要拆多个对象,这是接口设计的第一准则。
5. @RequestHeader:读请求头信息
@RequestHeader 看起来小众,但在认证、埋点、灰度发布、本地化这些场景里,它就是主力。
5.1 基础用法
@GetMapping("/api/user/info")
public Result<?> getUserInfo(@RequestHeader("Authorization") String token) {
String userId = authService.parseToken(token);
return Result.ok(userService.getInfo(userId));
}
从 Authorization 请求头里取 token,解析出用户身份,接口内部就不再依赖参数传 userId 了,安全性也提升不少。
它同样支持 required 和 defaultValue :
@GetMapping("/api/version")
public Result<?> getVersion(@RequestHeader(value = "X-Client-Version", defaultValue = "unknown") String clientVersion) {
...
}
有时候客户端升级了一套新逻辑,但老版本还在线上跑。后端起一个接口,通过请求头里的客户端版本号走不同分支,就能做灰度兼容。这个模式我实际用过不止一次。
5.2 读取全部请求头的实用配置
如果不想在方法签名里一个个写字段,可以直接把请求头整体注入一个 Map :
@GetMapping("/api/debug")
public Result<?> debug(@RequestHeader Map<String, String> headers) {
return Result.ok(headers);
}
Spring 会把所有请求头以键值对形式放入 Map 。这个方法我一般只放在调试接口里,线上不会用,因为请求头里的 Accept-Encoding 、 Cache-Control 等字段多半用不上,全量返回不仅浪费带宽,还可能把 Cookie 暴露出去,有安全风险。
如果需要拿多个特定 Header 并且想统一管理,更推荐用 HttpServletRequest 直接读取:
@GetMapping("/api/debug")
public Result<?> debug(HttpServletRequest request) {
String token = request.getHeader("Authorization");
...
}
HttpServletRequest 这种方式适合在拦截器、过滤器里统一读取,而控制器里我建议还是用 @RequestHeader ,语义更清晰。
5.3 与安全相关的 Header 处理心得
安全相关的 Header 有个细节:不要太信任前端传的 Header。比如很多团队习惯让前端把 userId 放在 X-User-Id 请求头里,后端直接信任并使用。这种做法风险很大,因为请求头是可以被恶意构造的,你拿到的 X-User-Id 不一定代表真实用户。正确的做法是: 只用请求头传不可伪造的凭证(比如签名 token),然后在后端解析凭证得到用户身份 。我之前的项目里,会把 token 解析后得到 userId 放进 ThreadLocal 或者 RequestContextHolder ,后续代码随时可以取,而不是让前端直接传 userId。这才是 Header 传认证信息的正确姿势。
6. 一张表彻底对比四个注解,以及怎么选
前面分头讲了各自的用法,这一节做个系统性对比,也把选型逻辑落地。
6.1 关键属性与数据来源对比
| 维度 | @PathVariable | @RequestParam | @RequestBody | @RequestHeader |
|---|---|---|---|---|
| 数据来源 | URL 路径 | URL 查询字符串/表单 | 请求体 | 请求头 |
| 典型方法 | GET | GET/POST | POST/PUT | 任意 |
| 必填属性 | 默认必填 | 默认必填 | 默认必填 | 默认必填 |
| 默认值支持 | 不支持 | 支持 defaultValue | 不支持 | 支持 defaultValue |
| 类型转换 | 自动转,报错 400 | 自动转,报错 400 | JSON 序列化框架 | 自动转,报错 400 |
| 同一方法数量 | 可多个 | 可多个 | 只能一个 | 可多个 |
| 常见用途 | RESTful 资源 ID | 分页、筛选、简单字段 | 复杂对象、批量数据 | token、来源标识 |
四个注解都支持 required = false 和 defaultValue (除了路径参数没有默认值说法)。Spring 绑定失败时基本都返回 400,但 @RequestBody 反序列化失败时返回的往往是 400 或 415,区别在于 Content-Type 是否正确配置。
6.2 接口设计时的选型思路
实际设计接口的时候,我建议按照下面这个顺序思考。
第一步,看数据在请求里的什么位置。如果在路径中,选 @PathVariable ;如果在查询串中,选 @RequestParam ;如果在请求体里,选 @RequestBody ;如果只是元信息,选 @RequestHeader 。
第二步,看字段多少。1 到 2 个简单字段用 @RequestParam 很清爽;超过 3 个或者字段是嵌套结构,立即考虑 DTO。
第三步,看接口语义。RESTful 风格下,资源操作路径上必然有 ID,用 @PathVariable ;资源列表查询必然是分页+筛选,用 @RequestParam ;创建和更新操作,数据量大且结构固定,用 @RequestBody 。
这一步想下来,几乎不存在"不知道用什么"的情况。如果还是犹豫,就停下来想一想:前端真正想给我传递的,是一个对象,还是一堆零散的参数?对象就 @RequestBody ,零散参数就 @RequestParam 。
7. 常见问题与排查技巧实录
最后这部分,我把我这些年排过的"参数相关"问题和大家唠唠。这几个问题几乎每个项目都见过,排查思路也很有价值。
7.1 参数总为 null:先看位置对不对
现象:前端明明传了参数,后端却收到 null 。
排查看三件事:
第一,看请求实际发出的时候参数放哪。按我上面说的划分, @RequestParam 对应查询串或表单, @RequestBody 对应 JSON 请求体。参数放错位置,后端当然拿不到。
第二,确认 Content-Type。 @RequestParam 想接收 POST 请求的表单字段,Content-Type 必须是 application/x-www-form-urlencoded 。如果你用工具测试时看到 Content-Type 是 application/json ,那 @RequestParam 拿不到就太正常了。
第三,检查参数名是否匹配。方法签名里带 name 或者 value 时,以它们为准;没有指定时,对参数名的要求比较严格。建议在 IDE 里开启 -parameters 编译参数,或者在 Spring Boot 的 Maven 插件里配好 <parameters>true</parameters> ,省掉一堆小毛病。
7.2 请求返回 400 或 415 的排查思路
400 和 415 都是 HTTP 状态码,但原因截然不同。
400 通常是"参数格式不对"或"必填参数缺失"。比如 @RequestParam Integer age 收到了 "abc" ,类型转换失败,返回 400。路径变量类型不对也会 400。 @RequestBody 里 JSON 字段和 DTO 字段类型不匹配,同样是 400。
415 则是"媒体类型不被支持"。服务器端方法用 @RequestBody 接收 JSON,但 HTTP 请求的 Content-Type 是 text/plain ,Spring 找不到能处理 text/plain 的消息转换器,就返回 415。解决办法是在 Controller 方法上或者全局配置里保证 Content-Type 和实际传输格式一致。排查这类问题时,我习惯先看请求头里的 Content-Type ,再看服务端方法的接收类型,两头对不上基本就是 415;两头都是 JSON 还报错,才是去查 DTO 字段格式的问题。
7.3 中文乱码与编码问题
接口返回的中文 json 乱码,一般是服务端输出编码没设置成 UTF-8,或者客户端对响应体解码方式不对。Spring Boot 里我一般直接配置:
server.servlet.encoding.charset=UTF-8 server.servlet.encoding.force=true
如果是参数本身乱码,先确认数据库连接串里有没有 characterEncoding=utf8 ,再确认 HTTP 请求的 Content-Type 里有没有 charset=UTF-8 。大部分乱码问题,追一圈都能追到编码设置缺失上。
7.4 一个真实的排查复盘
之前维护一个订单查询接口,前端说"搜索关键字失效了,传了但后端没反应"。
第一步看请求日志,发现前端把筛选条件全部放进了 JSON 请求体,而后端方法签名却用了 @RequestParam String keyword 。前端和后端各写各的,契约不一致。
第二步跟前端沟通后,统一约定:把 keyword 放进查询串, @RequestParam 接住;其余复杂筛选条件放 JSON 里用 @RequestBody 接收。一个接口可以同时有 @RequestParam 和 @RequestBody ,只要它们分别从不同位置取数据就行。改完之后,接口立刻正常。
这个小复盘想说明一件事:很多参数问题本质不是技术高低,而是前后端对参数位置没有形成统一约定。 越早约定清楚"什么数据走路径,什么数据走查询串,什么数据走请求体",联调阶段的破事就越少。
最后再分享一个我个人的习惯:每写完一个接口,我都会用 Postman 或者 IDE 自带的 HTTP 客户端先自测一遍,每个注解的场景都覆盖一次。路径参数传什么、查询参数传什么、请求体长什么样,测一遍心里就有底了,等到前端联调的时候基本不会出幺蛾子。Spring Boot 参数绑定这块,说难也难,说简单也简单,核心就是你把 HTTP 请求的结构刻在脑子里,再写代码,思路会清晰很多。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
