java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot接口参数注解 @RequestBody和@RequestParam区别详解

SpringBoot接口参数注解怎么选?@RequestBody和@RequestParam区别详解

作者:tokex

搞不懂SpringBoot接口参数注解怎么选?这篇文章用最直白的方式讲透@PathVariable、@RequestParam、@RequestBody和@RequestHeader的区别和用法,帮你理清HTTP请求结构,从此告别参数取不到、报400或415的烦恼,适合入门和进阶开发者

Spring Boot 里写接口,绕不开 @RequestBody 、 @PathVariable 、 @RequestParam 和 @RequestHeader 这四个注解。我当年刚接触 Spring Boot 的时候,总觉得它们长得差不多,用起来也是凭感觉,代码里今天加一个注解,明天换一个注解,直到被一个"参数明明传了但接口死活收不到"的 Bug 逼着把源码翻了一遍,才算彻底弄明白这几兄弟的分工。

这篇文章就把我摸爬滚打总结出来的心得整理出来。

核心思路只有一句话:

先把 HTTP 请求本身的结构搞清楚,再看这四个注解各自对应哪一块数据,基本就不会再选错。文章适合刚入门 Spring Boot 的开发者,也适合写了两年接口但一直没系统梳理过参数绑定的同学。

看完你至少能解决三个问题:

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 四个注解与数据位置的映射关系

我做了个项目里一直在用的对照关系,建议你直接存下来:

注解数据来源典型请求示例主要用途
@PathVariableURL 路径中/api/users/123RESTful 资源定位
@RequestParamURL 查询字符串或表单/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 请求中取数据,这又让边界变得模糊。所以我的建议是: 不要在边界上靠猜,先把每个注解的职责边界划清楚 。

落一下地:

想明白这四句话,后面写接口会顺很多。

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 的取舍边界

这两个注解是"误用重灾区",我总结了一套简单有效的判断逻辑。

我见过一个反面例子:有人写了一个注册接口,参数有用户名、密码、邮箱、手机号、昵称、头像地址,后端方法签名直接写了 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 查询字符串/表单请求体请求头
典型方法GETGET/POSTPOST/PUT任意
必填属性默认必填默认必填默认必填默认必填
默认值支持不支持支持 defaultValue不支持支持 defaultValue
类型转换自动转,报错 400自动转,报错 400JSON 序列化框架自动转,报错 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 请求的结构刻在脑子里,再写代码,思路会清晰很多。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文