Day 1 | 接口参数收不到?
鱼罐头入职第 1 天
鱼罐头分到了一台台式机 显示器还挺大的 还没来得及看电脑的配置 老王就发了任务下来
一、第一个任务
"鱼罐头,打开本地项目,在里面写个条件查询接口。前端那边等你,今天联调完。"
"保证完成任务!"

鱼罐头接下任务,打开心爱的 IDEA,看着里面繁杂的项目,先从接口看了起来,慢慢熟悉。
熟悉完项目,觉得需求十分简单——不就是一个条件查询嘛。
二、接口写完了,参数全是 null
鱼罐头看着已有的接口照葫芦画瓢,写了个条件查询:
@RestController
public class UserController {
@PostMapping("/user/query")
public Result query(@RequestParam(required = false) String name,
@RequestParam(required = false) Integer age) {
System.out.println("name = " + name);
System.out.println("age = " + age);
return Result.success();
}
}前端小刘把页面调好了,说:"我这边传了,你那边收到没?"
鱼罐头一看后端日志:
name = null
age = null"没收到。" 他说。
"我明明传了啊!" 小刘在对面喊。
三、最可怕的不是报错,是不报错
鱼罐头第一反应是检查代码:路径对不对?注解对不对?变量名对不对?数据库连上没?
全对。
又检查一遍。还是全对。
关键是——接口不报错。状态码 200,参数就是 null。报错还能顺着日志查,这玩意儿安静得像什么都没发生,你连从哪下手都不知道。
鱼罐头盯着日志里的 null,有点懵。前端把页面截图发过来,数据明明在页面上。他甚至开始怀疑是不是前端传的字段名跟参数名不一样——不是,字段名也看了,对的。
四、老王一句话点破
下午三点,老王端着茶杯路过,瞥了鱼罐头一眼:
"卡哪了?"
"参数收不到,全是 null,还不报错。" 鱼罐头快哭了。
老王放下杯子,看了眼代码,又看了眼小刘发来的请求截图,乐了:
"你拿 @RequestParam 去收 JSON?"
"啊?"

老王把截图转过来:"你看,前端发的请求长这样。"
POST /user/query HTTP/1.1
Content-Type: application/json
{ "name": "鱼罐头", "age": 24 }"看到没,name 和 age 躺在最后这个 body 里。@RequestParam 是干嘛的?它是去 URL 问号后面——?name=xxx 那种——还有表单里找参数的。你让它去 body 里翻?俩东西压根不在一个地儿,它找得到个鬼。"
鱼罐头眼睛直了:"那……它为什么不报错?"
老王:"默认情况下它 required=true,找不到参数会直接甩你一个 400。你看项目里那俩老接口,是不是都带着 required = false?你照抄的,正常——结果把这句也抄过来了。得,它不报错了,直接返回给你 null。"
鱼罐头有点垂头丧气。
老王看他那样,语气软了点:"不过第一次写,知道抄项目里现成的接口,说明会看代码,这毛病不算大。改过来就好。"
他喝了口茶,补了句:
"不报错的 bug 最难受——你连从哪下手都不知道。"
五、改一行,通了
老王让鱼罐头把注解换掉:
@PostMapping("/user/query")
public Result query(@RequestBody QueryReq req) {
System.out.println("name = " + req.getName());
System.out.println("age = " + req.getAge());
return Result.success();
}鱼罐头问:"这个 QueryReq 是哪来的?"
老王:"你得先有个类,把前端传的东西接住。" 说着,他又新建了个文件:
public class QueryReq {
private String name;
private Integer age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
}"看到没,字段名跟 JSON 里的一一对应。Spring 拿到 body 里的 JSON,用 Jackson 自动转成这个对象,字段对上了就帮你填进去——所以 req.getName() 能拿到值。"
鱼罐头盯着那几行,恍然大悟:"哦——原来收 JSON,还得先建个类把它接住。"
重启,再试。
name = 鱼罐头
age = 24通了。
六、鱼罐头的笔记
接口是通了,可鱼罐头心里不踏实。老王白天把请求摆到他面前,他记住了,却没完全懂——query、body、request 这些词在他脑子里还是散的。回到出租屋,他打开电脑,把 HTTP 请求从头到脚拆了一遍。

先从最眼熟的 URL 下手。URL 分好几段:
http://localhost:8080/user/query?name=xxx
└──┬──┘└─────┬─────┘ └────┬────┘└───┬───┘
协议 域名 路径 query(问号后面的参数)再把整个 HTTP 请求拼起来看,URL 就站在第一行:
POST /user/query HTTP/1.1
Host: localhost:8080
Content-Type: application/json
{ "name": "鱼罐头", "age": 24 }- 第一行叫请求行:方法(POST)+ URL + 协议版本
- 中间是请求头(Header):一堆
key: value - 空行下面才是请求体(Body):前端传的 JSON 就躺在这里
所以参数有三条路可以进来:
| 参数从哪来 | 长什么样 | 用什么注解接 |
|---|---|---|
| query(URL 问号后) | ?name=xxx | @RequestParam |
| 表单(form) | name=xxx&age=24 | @RequestParam |
| 路径里 | /user/{id} | @PathVariable |
| 请求体 body | JSON | @RequestBody |
三条路各自怎么走?
① @RequestParam——收 query 和表单
前端这么发:
GET /user/query?name=鱼罐头&age=24后端这么接:
@GetMapping("/user/query")
public Result query(@RequestParam String name,
@RequestParam Integer age) {
// name = "鱼罐头",age = 24
}参数跟在问号后面,@RequestParam 按名字一个个取,取到的就是字符串和数字。
② @PathVariable——收路径里的
前端这么发:
GET /user/1后端这么接:
@GetMapping("/user/{id}")
public Result getUser(@PathVariable Long id) {
// id = 1
}/user/{id} 里的花括号 {id} 是个占位符,@PathVariable 把路径上这个位置的值取出来。
③ @RequestBody——收 body 里的 JSON
前端这么发:
POST /user/query
Content-Type: application/json
{ "name": "鱼罐头", "age": 24 }后端这么接:
@PostMapping("/user/query")
public Result query(@RequestBody QueryReq req) {
// req.getName() = "鱼罐头",req.getAge() = 24
}JSON 躺在 body 里,@RequestBody 把它整个变成 QueryReq 对象——这跟白天老王给他改的那行一模一样。
鱼罐头这才彻底明白白天那个坑:前端把数据塞进了 body(JSON),他却让 @RequestParam 去 query 和表单里找——找得到才怪。 而 @RequestParam 默认 required=true,找不到本该甩 400 报错;可他是从老接口抄的 required=false,于是连报错都省了,直接安静地给他 null。
他顺手把那张表存进笔记,标题写着:"参数从哪来,就用哪个注解接。"
七、下集预告
第二天上班,老王过来看了一眼:
"写完了?那用 Git 提交一下吧。"
"Git……?"
鱼罐头只知道是版本控制器,也没有用过呀。
鱼罐头默默关掉 IDEA,打开了搜索引擎。
延伸阅读:Spring MVC:请求参数 @RequestParam、请求体 @RequestBody、请求映射与路径变量 @PathVariable。
