☰
Spring Boot接收前端参数的11种方式:从HTTP位置到注解实战
2026/10/3 1:14:16 网站建设 项目流程

1. 先搞清楚参数藏在HTTP的哪个位置

在做项目review的时候,我经常被问到同一个问题:Spring Boot接收前端参数,到底有几种写法?很多新手只会在Controller方法上挂一个@RequestParam,遇到复杂一点的请求就不知道怎么处理了;也有一些人习惯把所有参数都塞进@RequestBody,结果前端明明只在URL里拼了参数,后端却一直报解析异常。

想真正掌握参数接收这件事,第一件事不是背注解,而是先理解HTTP请求里参数到底放在哪。因为Spring Boot的每一种接收方式,本质上都是针对参数所在位置做的一次映射。位置搞错了,后端怎么改代码都接不到值。

这一节先把概念打通,后面十几种写法会变得非常自然。

1.1 用一句话看懂三个位置的区别

HTTP请求里,参数基本只有三个存放位置:

  1. Query String(查询字符串),也就是URL里?后面的部分,比如?page=1&size=10。GET请求最常见,POST请求也能这么传,但不太推荐。
  2. Path(路径本身),比如/api/user/123中的123就是路径参数。RESTful风格的接口大量使用这种方式。
  3. Request Body(请求体),POST、PUT、PATCH请求中,表单数据、JSON数据、文件数据都在请求体里。

我们可以用一个生活化的比喻来理解:一次HTTP请求就像寄一个快递。

  • Query参数是写在快递单正面备注栏上的字,谁都能一眼看到,适合放一些简单、非敏感的信息;
  • 路径参数是快递单号本身,它决定了你找的是哪个包裹,是资源的唯一标识;
  • Body参数是箱子里的物品清单,得打开箱子才能看到,适合放复杂、体量大、不适合暴露在URL上的信息。

看一个真实的请求就明白了:

GET /api/user/list?page=1&size=10 GET /api/user/123 POST /api/user Content-Type: application/json {"name": "张三", "age": 18}

第一个请求,参数在URL的Query String里;第二个请求,123在路径里;第三个请求,name和age组成的JSON对象在Body里。三种位置,对应Spring Boot里完全不同的接收策略。

1.2 前端传参方式与后端接收方式对照表

我在带团队新人的时候,会先让他们记住下面这张对照表。每次参数接不上,第一反应就是对着这个表检查参数位置和注解是否匹配。

前端传参方式HTTP位置后端接收方式
URL拼接?key=valueQuery String@RequestParam、POJO对象绑定
路径占位符,如/user/123Path@PathVariable
Ajax发送JSON字符串Body(JSON格式)@RequestBody
表单提交(form-data)Body(表单格式)@RequestParam、POJO对象绑定
文件上传Body(multipart格式)@RequestPart+MultipartFile
请求头里的认证信息Header@RequestHeader
浏览器自动携带的CookieCookie@CookieValue

这张表之所以重要,是因为后端接不到参数的故障,90%都能归类到"参数实际位置与接收方式不匹配"这个根因上。比如前端用axios发送JSON请求时忘记设置Content-Type: application/json,后端用@RequestBody去接,就必然失败。这种问题不是代码写错了,而是双方在协议层面没对齐。

把这层基础打好,下面正式进入11种方式的逐一拆解。

2. 最常用的三个注解:@RequestParam、@PathVariable、@RequestBody

这三个注解是Spring MVC参数接收的"地基",日常开发中八成以上的接口都在用它们。我先讲用法,再讲原理,最后讲坑。

2.1 @RequestParam:Query参数和表单参数的默认入口

先看一个最基础的例子:

@RestController @RequestMapping("/api/user") public class UserController { // 前端请求:GET /api/user/list?page=1&size=10 @GetMapping("/list") public Result list(@RequestParam int page, @RequestParam int size) { return Result.success("page=" + page + ", size=" + size); } }

@RequestParam做的事情,就是把Query String里的page、size取出来,并做一次类型转换,绑定到方法参数上。

它有三个属性需要重点掌握:

  • value:指定前端参数名。如果后端变量名和前端key不一致,必须用这个属性显式映射,比如@RequestParam("pageNo") int page;
  • required:是否必填,默认是true。参数缺失会直接抛MissingServletRequestParameterException,接口返回400;
  • defaultValue:默认值,一旦设置,required会自动变成false。
@GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { return Result.success("page=" + page + ", size=" + size); }

这里想多说一句:很多人以为@RequestParam只能用在GET请求上,其实POST请求通过form-data提交的字段,同样可以用它接收。Spring在做参数解析时,会同时从Query String和表单数据里找同名参数。

我在实际项目中遇到最多的报错就是MissingServletRequestParameterException。排查步骤非常固定:先抓包看真实请求的URL长什么样,再对照Controller的注解声明,问题通常出在前端key与后端value不一致,或者前端漏传了必填参数。

2.2 @PathVariable:RESTful风格路径参数

路径参数和Query参数最大的区别是:它是URL路径的一部分,而不是?后面的键值对。

// 前端请求:GET /api/user/123 @GetMapping("/{id}") public Result getUser(@PathVariable Long id) { return Result.success("id=" + id); }

当一个系统采用RESTful设计时,资源的标识天然适合放在路径上。比如GET /api/user/123表达的是"获取id为123的用户",语义清晰、URL简洁。

多路径参数的写法:

// 前端请求:GET /api/order/2024/001 @GetMapping("/order/{year}/{no}") public Result getOrder(@PathVariable("year") String year, @PathVariable("no") String no) { return Result.success("year=" + year + ", no=" + no); }

这里有一个隐蔽的坑:如果方法参数名和路径占位符不一致,又没在注解里指定value,Spring是找不到对应占位符的。比如占位符叫{userId},方法参数名写成了id,运行时会直接报错。我建议在路径参数存在多个、且命名较长的时候,一律显式写@PathVariable("xxx"),用一点冗余换确定性,很划算。

类型转换的问题同样值得注意:路径参数从URL来,本质都是字符串,Spring会尝试把它转成方法声明的类型。前端传了abc,你声明Long,就会抛MethodArgumentTypeMismatchException,接口返回400。这种错误码本身就暗示了"参数类型不对",排查时不用绕弯子。

还有编码问题。如果路径参数是中文,前端必须做一次encodeURIComponent才能拼进URL。后端拿到的通常是解码后的内容,正常无需特殊处理。但如果修改过Tomcat的URIEncoding配置,或者用了比较老的容器,就会出现中文乱码,这时候优先检查应用容器的URI编码是否统一为UTF-8。

2.3 @RequestBody:JSON Body参数的接收

前后端分离的项目里,@RequestBody是出镜率最高的一个注解。

// 前端请求:POST /api/user // Content-Type: application/json // body: {"name":"张三","age":18} public class UserDTO { private String name; private Integer age; // getter/setter 省略 } @PostMapping("/user") public Result createUser(@RequestBody UserDTO user) { return Result.success("name=" + user.getName() + ", age=" + user.getAge()); }

@RequestBody的底层逻辑是:Spring根据请求头的Content-Type找到对应的HttpMessageConverter,把请求体的原始字节流反序列化成目标对象。application/json对应的就是Jackson的MappingJackson2HttpMessageConverter。

这里有几个必须记住的约束:

  • 前端必须设置Content-Type: application/json。如果不设置,比如axios默认没指定时,body会被当作普通文本或表单格式,@RequestBody解析到的字符串无法变成对象,轻则字段全为null,重则直接抛HttpMessageNotReadableException;
  • 一个方法只能有一个@RequestBody。如果需要传多个JSON对象,请设计一个聚合对象把它们包起来,不要试图写两个@RequestBody参数;
  • 空body的处理很微妙。前端传了{},Jackson会实例化一个空对象,属性全是null;前端完全没传body,Spring会抛异常。所以对象内部的必填校验,必须靠@Valid配合JSR-303注解(比如@NotNull、@NotBlank)来做,Spring不会好心帮你兜底。

@RequestBody和@RequestParam其实可以同时出现在一个方法里,这在真实项目中很常见:

// 前端请求:POST /api/user?op=create // body: {"name":"张三","age":18} @PostMapping("/user") public Result createUser(@RequestParam("op") String op, @RequestBody UserDTO user) { // op 来自Query String,user 来自JSON body return Result.success("op=" + op + ", name=" + user.getName()); }

这种混合写法的适用场景是:接口的"操作类型"这类控制参数放URL上,业务数据放JSON body里,职责清晰。但要注意一个原则——同一个接口的参数语义要单一,不要今天用Query传ID,明天又改成body传ID,前后端对齐的成本会成倍上升。

3. POJO对象绑定与集合类型参数:告别一个参数一个注解

如果接口有十几个字段,你还在一个接一个地写@RequestParam,你的代码会迅速变成一坨难以维护的东西。Spring还提供了对象绑定和集合类型接收两种更高效的姿势。

3.1 直接传POJO对象:不写注解也能绑定

Spring MVC有一个非常实用的特性:当方法参数是一个普通Java对象时,即使不加任何注解,它也会自动从Query String和表单参数里寻找匹配字段,执行setter绑定。这是"数据绑定"机制,而不是JSON反序列化。

@Data public class UserQueryDTO { private String name; private Integer age; private String email; } // 前端请求:GET /api/user/search?name=张三&age=18 @GetMapping("/search") public Result search(UserQueryDTO query) { return Result.success("name=" + query.getName()); }

注意看,search()方法的参数UserQueryDTO query前面没有任何注解。Spring拿到请求后,发现这个参数是一个复合对象,就会用请求参数里的键值对去匹配对象的属性字段。?name=张三会调用setName("张三"),?age=18会调用setAge(18)并完成String到Integer的转换。

这种方式的适用场景非常明确:

  • 表单提交,尤其注册、筛选这类十几个字段的页面;
  • GET请求的复杂查询条件,比如后台管理系统的列表筛选;
  • 字段多但大部分可选不必填的场景。

但这里有几个我真实踩过的坑:

第一个是类型转换失败。前端传age=abc,后端是Integer,Spring会抛MethodArgumentTypeMismatchException。如果你的接口允许用户输入任意内容,建议要么把这类字段先声明成String再自己在service层转换,要么在全局异常处理里统一兜住。

第二个是字段名映射。前端传userName,后端字段是user_name,两者对不上,字段静默为null,不报错但结果不对。我建议在项目里统一约定参数命名规范,前端和DTO都使用驼峰,省掉一层翻译成本。

第三个是未知字段的问题。前端多传了一个DTO里不存在的字段,默认情况下Spring会忽略。但如果你的项目把Jackson的FAIL_ON_UNKNOWN_PROPERTIES配成了true,请求会直接400。这在老系统迁移时很容易踩到,迁移前先自查配置。

3.2 数组、List、Map:一套接收多个值

批量操作、多选筛选、动态表单,这些场景需要一个参数名对应多个值。Spring提供了三种集合接收方式。

数组方式:

// 前端请求:GET /api/user/batch?ids=1&ids=2&ids=3 @GetMapping("/batch") public Result batch(String[] ids) { return Result.success("ids=" + Arrays.toString(ids)); }

Spring支持直接用数组接收多个同名参数。甚至可以简写成ids=1,2,3,逗号分隔也会被拆开。这种写法在老项目中比较常见,写起来最省事。

List方式:

// 前端请求:GET /api/user/batch?ids=1&ids=2&ids=3 @GetMapping("/batch") public Result batch(@RequestParam List<String> ids) { return Result.success("ids=" + ids); }

List方式有一个必须记住的约束:前面必须加上@RequestParam注解。如果你写List<String> ids而不加注解,Spring会把List当成一个"模型属性",尝试从请求里找同名的一个List对象,找不到就直接抛ServletRequestBindingException。这个异常的字面意思很不直观,很多新手在这里卡很久,其实原因就是少写了一个注解。

Map方式:

// 前端请求:GET /api/user/filter?category=book&level=high @GetMapping("/filter") public Result filter(@RequestParam Map<String, String> params) { return Result.success("params=" + params); }

Map方式适合参数名不确定、没法预定义DTO的动态查询场景。比如做报表系统时,筛选条件常常是用户自定义的字段集合。注意Map里的value全是String,需要数值时必须在service层手动转换。

把三种方式放一起看:

  • 数组:写起来最简单,适合勾选ID这种纯批量场景;
  • List:需要保持参数顺序、或者要直接做集合运算时用,比数组更灵活;
  • Map:适合动态、扩展性强的参数结构,但可读性和类型安全最弱,不宜滥用。

4. 请求头与Cookie参数:从"元信息"里拿数据

有些参数既不在URL里也不在body里,而是藏在HTTP请求的"元信息"中,比如登录凭证、客户端类型、会话标识。这种场景下,你还需要掌握另外两个注解。

4.1 @RequestHeader:从请求头读取数据

// 前端请求:GET /api/user/profile // Header: // Authorization: Bearer xxxxxx // User-Agent: Mozilla/5.0 @GetMapping("/profile") public Result profile(@RequestHeader("Authorization") String token, @RequestHeader(value = "User-Agent", required = false, defaultValue = "unknown") String userAgent) { return Result.success("token=" + token); }

@RequestHeader的属性和@RequestParam几乎完全一样,也有value、required、defaultValue。HTTP头本身的字段名不区分大小写,Spring在匹配时会做标准化处理,所以你写authorization和Authorization都能拿到值。

实际使用中,最常读取的请求头有这么几个:

  • Authorization:携带登录凭证,JWT token一般放在这里;
  • X-Requested-With:用来区分是不是Ajax请求;
  • Accept、Content-Type:内容协商时用。

一个我的个人建议:Authorization这类通用头部参数的提取,尽量不要在每个Controller方法里写一遍@RequestHeader,而是放到拦截器或过滤器统一处理。Controller只关心业务参数,鉴权逻辑由框架层完成,代码会干净很多。我见过一个老项目,几乎每个方法都有三行重复的token解析代码,后来重构抽到拦截器里,整体代码量少了将近五分之一。

4.2 @CookieValue:读取浏览器Cookie

@GetMapping("/cart") public Result getCart(@CookieValue(value = "JSESSIONID", required = false) String sessionId) { return Result.success("sessionId=" + sessionId); }

Cookie参数最常见的用途是:

  • 读取会话标识,比如老的JSESSIONID模式下的登录态;
  • 读取前端埋点写入的用户偏好,比如theme、language;
  • 读取第三方登录流程中种在浏览器里的临时Cookie。

这里有一个容易误解的点:Cookie有HttpOnly属性时,JavaScript读不到,但后端依然可以正常读取。HttpOnly只是限制了浏览器的脚本访问权限,并不影响服务端的@CookieValue读取。所以不要看到Cookie带HttpOnly就以为后端拿不到。

还有一个坑:如果@CookieValue指定的Cookie不存在,且没设required = false,Spring会直接报MissingRequestCookieException。Cookie这种高度依赖客户端环境的东西,不要默认它一定存在,建议都加上required = false或defaultValue再使用。

5. 原始Servlet API与文件上传参数:兜底与特殊场景

前面讲的所有注解,本质上都是Spring对Servlet API的一层封装和解耦。但有两类场景必须越过这层封装:一是需要操作原始请求对象,二是处理文件上传这种multipart混合内容。

5.1 直接注入HttpServletRequest拿原始参数

Spring MVC允许在Controller方法参数里直接声明HttpServletRequest、HttpServletResponse等Servlet原生对象,容器会自动注入。

@GetMapping("/old-school") public Result oldSchool(HttpServletRequest request) { String page = request.getParameter("page"); String size = request.getParameter("size"); // 也可以一次性遍历所有参数 request.getParameterMap().forEach((k, v) -> System.out.println(k + "=" + String.join(",", v)) ); return Result.success("page=" + page + ", size=" + size); }

这种写法在什么场景下才有必要?

第一,你需要在同一个方法里同时访问URL参数、表单参数、请求头、InputStream等多个维度的信息,但不想在方法签名里列出一长串注解参数。

第二,你需要读取请求body的原始字节流做签名验证、日志记录。这里要特别注意:请求流只能读一次,一旦调用getInputStream(),body就"没了"。如果你还要用@RequestBody接JSON,两者就会冲突。正确的做法是用OncePerRequestFilter配合ContentCachingRequestWrapper包装请求,先把流缓存下来再读取。

第三,历史代码迁移过渡期,需要临时兼容一些基于Servlet API写法的老接口。

我不推荐全项目都用这种方式,原因很现实:代码里全是request.getParameter("xxx"),魔法字符串满天飞,没有类型安全,没有参数校验,可读性极差。它更适合当"兜底工具",而不是主力方案。

5.2 @RequestPart与MultipartFile:文件上传中的参数接收

文件上传是让新人最容易困惑的场景。因为文件file和普通参数name混在同一个multipart/form-data请求体里,只用前面的注解搞不定。

@PostMapping("/upload") public Result upload(@RequestParam("name") String name, @RequestPart("file") MultipartFile file) { String originalFilename = file.getOriginalFilename(); long size = file.getSize(); return Result.success("name=" + name + ", size=" + size + ", file=" + originalFilename); }

对应的前端axios写法:

const formData = new FormData(); formData.append('name', '张三'); formData.append('file', fileInput.files[0]); axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } });

核心规则是这样:普通字段用@RequestParam接收,文件字段用@RequestPart加MultipartFile接收。Spring会根据Content-Type: multipart/form-data; boundary=xxx中每个part的name属性去做匹配。

Spring Boot默认的上传文件大小限制是1MB(2.x版本),超过会抛MaxUploadSizeExceededException。如果业务需要更大的文件,可以在配置里调整:

spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB

max-file-size是单个文件的上限,max-request-size是整个请求的上限,包括所有文件和表单项的总大小。这两个值怎么配,取决于业务场景:个人头像上传,5MB足够;视频上传,可能要按GB来算,但这种情况一般不建议走应用服务器直传,而是后端生成预签名URL,让前端直传对象存储。

接收多个文件时,参数类型可以声明为MultipartFile[]或List<MultipartFile>。

文件上传最常见的坑在前端而不是后端。很多前端开发在手动构造FormData时,自己写了一个Content-Type: multipart/form-data,但忘记带boundary=----WebKitFormBoundaryxxx。这样后端会解析失败,抛MultipartException,报错信息看起来云里雾里。排查这类问题最快的办法是抓包确认请求头,看看boundary是否存在且与body一致。

6. 11种方式一表总结与日常选型建议

到这里,11种接收方式全部讲完了。先汇总成一张速查表,方便你平时翻:

序号方式写法适用场景典型前端请求
1单个Query参数@RequestParam分页、简单筛选、操作类型控制GET /list?page=1&size=10
2路径参数@PathVariableRESTful资源定位GET /user/123
3JSON Body对象@RequestBody新增、修改接口的复杂业务数据POST /user+ JSON
4POJO对象绑定无注解直接传DTO多字段表单、GET复杂查询GET /search?name=x&age=18
5数组接收String[]批量勾选、多ID通用处理GET /batch?ids=1&ids=2
6List接收@RequestParam List有序批量集合、集合运算GET /batch?ids=1&ids=2
7Map接收@RequestParam Map动态参数、无法预定义的筛选条件GET /filter?key=val
8请求头参数@RequestHeader认证信息、客户端信息Header里的Authorization
9Cookie参数@CookieValue登录态、用户偏好设置Cookie里的JSESSIONID
10Servlet原生对象HttpServletRequest复杂混合读取、流读取、旧代码适配任意请求
11文件与表单混合@RequestPart+MultipartFile文件上传、多文件上传POST /upload+ FormData

6.1 我在项目里常用的选型规则

基于多年的实际开发经验,我一般按下面这套规则来做参数接收的选型:

  1. GET请求的简单筛选,1-3个参数,用@RequestParam;
  2. GET请求的复杂筛选,字段超过3个,直接升级为POJO对象绑定;
  3. RESTful资源操作,路径参数定位资源,查询条件放Query String;
  4. POST/PUT接口,业务数据一律@RequestBody+ DTO +@Valid参数校验;
  5. 批量化操作,ID列表用@RequestParam List<Long>最规范;
  6. 参数名不固定的扩展字段,用Map兜底;
  7. 登录凭证、令牌,放Header不放URL参数,更不要放body;
  8. 文件场景,普通字段和文件分开接收,普通字段走@RequestParam,文件走@RequestPart。

还有两个细节想单独提一下。

第一个关于DTO设计。我建议统一使用Java Bean规范,注意属性和类型要对齐。boolean类型字段在Jackson和Spring自带的属性绑定器里有一些微妙的差异,尤其是当你用了isXxx()这种命名风格时,容易出现字段映射不到的诡异问题。稳妥的做法是boolean字段统一用Boolean包装类型,并保持统一的getter/setter命名习惯。

第二个关于接口语义。一个接口的参数位置要固定,不能今天用Query传ID,明天同一接口又改成body传,这对前后端是对齐成本的一次次叠加。接口设计阶段就把参数位置确定下来,后面会省掉大量沟通和改bug的时间。

6.2 参数接不到值时的标准排查顺序

最后分享一个排查思路。如果你在开发中发现Controller参数怎么都接不到值,先别急着改代码,按下面这个顺序来:

  1. 看前端真实请求:打开浏览器F12或抓包,确认URL、Content-Type、请求体原文长什么样;
  2. 对照参数位置:参数在Query里就检查@RequestParam或POJO绑定;在路径里就检查@PathVariable;在JSON body里就检查@RequestBody;在multipart里就检查@RequestPart;
  3. 检查参数名:注解的value和前端传的key是否完全一致;
  4. 检查类型:类型不匹配的典型表现是400或MethodArgumentTypeMismatchException;
  5. 检查请求方式:方法标了@GetMapping,前端发的是POST,参数也可能接不到,尤其是@RequestBody。

我参与过的"前后端参数接不上"的纠纷,几乎每次最后都能归因到上面这几条。把这份排查顺序记住,能帮你省下大量无谓的联调时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询