ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Boot接收前端参数的11种方式:从HTTP位置到注解实战

Spring Boot接收前端参数的11种方式:从HTTP位置到注解实战 1. 先搞清楚参数藏在HTTP的哪个位置在做项目review的时候我经常被问到同一个问题Spring Boot接收前端参数到底有几种写法很多新手只会在Controller方法上挂一个RequestParam遇到复杂一点的请求就不知道怎么处理了也有一些人习惯把所有参数都塞进RequestBody结果前端明明只在URL里拼了参数后端却一直报解析异常。想真正掌握参数接收这件事第一件事不是背注解而是先理解HTTP请求里参数到底放在哪。因为Spring Boot的每一种接收方式本质上都是针对参数所在位置做的一次映射。位置搞错了后端怎么改代码都接不到值。这一节先把概念打通后面十几种写法会变得非常自然。1.1 用一句话看懂三个位置的区别HTTP请求里参数基本只有三个存放位置Query String查询字符串也就是URL里?后面的部分比如?page1size10。GET请求最常见POST请求也能这么传但不太推荐。Path路径本身比如/api/user/123中的123就是路径参数。RESTful风格的接口大量使用这种方式。Request Body请求体POST、PUT、PATCH请求中表单数据、JSON数据、文件数据都在请求体里。我们可以用一个生活化的比喻来理解一次HTTP请求就像寄一个快递。Query参数是写在快递单正面备注栏上的字谁都能一眼看到适合放一些简单、非敏感的信息路径参数是快递单号本身它决定了你找的是哪个包裹是资源的唯一标识Body参数是箱子里的物品清单得打开箱子才能看到适合放复杂、体量大、不适合暴露在URL上的信息。看一个真实的请求就明白了GET /api/user/list?page1size10 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拼接?keyvalueQuery StringRequestParam、POJO对象绑定路径占位符如/user/123PathPathVariableAjax发送JSON字符串BodyJSON格式RequestBody表单提交form-dataBody表单格式RequestParam、POJO对象绑定文件上传Bodymultipart格式RequestPartMultipartFile请求头里的认证信息HeaderRequestHeader浏览器自动携带的CookieCookieCookieValue这张表之所以重要是因为后端接不到参数的故障90%都能归类到参数实际位置与接收方式不匹配这个根因上。比如前端用axios发送JSON请求时忘记设置Content-Type: application/json后端用RequestBody去接就必然失败。这种问题不是代码写错了而是双方在协议层面没对齐。把这层基础打好下面正式进入11种方式的逐一拆解。2. 最常用的三个注解RequestParam、PathVariable、RequestBody这三个注解是Spring MVC参数接收的地基日常开发中八成以上的接口都在用它们。我先讲用法再讲原理最后讲坑。2.1 RequestParamQuery参数和表单参数的默认入口先看一个最基础的例子RestController RequestMapping(/api/user) public class UserController { // 前端请求GET /api/user/list?page1size10 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 pagerequired是否必填默认是true。参数缺失会直接抛MissingServletRequestParameterException接口返回400defaultValue默认值一旦设置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 PathVariableRESTful风格路径参数路径参数和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); }这里有一个隐蔽的坑如果方法参数名和路径占位符不一致又没在注解里指定valueSpring是找不到对应占位符的。比如占位符叫{userId}方法参数名写成了id运行时会直接报错。我建议在路径参数存在多个、且命名较长的时候一律显式写PathVariable(xxx)用一点冗余换确定性很划算。类型转换的问题同样值得注意路径参数从URL来本质都是字符串Spring会尝试把它转成方法声明的类型。前端传了abc你声明Long就会抛MethodArgumentTypeMismatchException接口返回400。这种错误码本身就暗示了参数类型不对排查时不用绕弯子。还有编码问题。如果路径参数是中文前端必须做一次encodeURIComponent才能拼进URL。后端拿到的通常是解码后的内容正常无需特殊处理。但如果修改过Tomcat的URIEncoding配置或者用了比较老的容器就会出现中文乱码这时候优先检查应用容器的URI编码是否统一为UTF-8。2.3 RequestBodyJSON 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前端完全没传bodySpring会抛异常。所以对象内部的必填校验必须靠Valid配合JSR-303注解比如NotNull、NotBlank来做Spring不会好心帮你兜底。RequestBody和RequestParam其实可以同时出现在一个方法里这在真实项目中很常见// 前端请求POST /api/user?opcreate // body: {name:张三,age:18} PostMapping(/user) public Result createUser(RequestParam(op) String op, RequestBody UserDTO user) { // op 来自Query Stringuser 来自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张三age18 GetMapping(/search) public Result search(UserQueryDTO query) { return Result.success(name query.getName()); }注意看search()方法的参数UserQueryDTO query前面没有任何注解。Spring拿到请求后发现这个参数是一个复合对象就会用请求参数里的键值对去匹配对象的属性字段。?name张三会调用setName(张三)?age18会调用setAge(18)并完成String到Integer的转换。这种方式的适用场景非常明确表单提交尤其注册、筛选这类十几个字段的页面GET请求的复杂查询条件比如后台管理系统的列表筛选字段多但大部分可选不必填的场景。但这里有几个我真实踩过的坑第一个是类型转换失败。前端传ageabc后端是IntegerSpring会抛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?ids1ids2ids3 GetMapping(/batch) public Result batch(String[] ids) { return Result.success(ids Arrays.toString(ids)); }Spring支持直接用数组接收多个同名参数。甚至可以简写成ids1,2,3逗号分隔也会被拆开。这种写法在老项目中比较常见写起来最省事。List方式// 前端请求GET /api/user/batch?ids1ids2ids3 GetMapping(/batch) public Result batch(RequestParam ListString ids) { return Result.success(ids ids); }List方式有一个必须记住的约束前面必须加上RequestParam注解。如果你写ListString ids而不加注解Spring会把List当成一个模型属性尝试从请求里找同名的一个List对象找不到就直接抛ServletRequestBindingException。这个异常的字面意思很不直观很多新手在这里卡很久其实原因就是少写了一个注解。Map方式// 前端请求GET /api/user/filter?categorybooklevelhigh GetMapping(/filter) public Result filter(RequestParam MapString, 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读取浏览器CookieGetMapping(/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 falseSpring会直接报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; boundaryxxx中每个part的name属性去做匹配。Spring Boot默认的上传文件大小限制是1MB2.x版本超过会抛MaxUploadSizeExceededException。如果业务需要更大的文件可以在配置里调整spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MBmax-file-size是单个文件的上限max-request-size是整个请求的上限包括所有文件和表单项的总大小。这两个值怎么配取决于业务场景个人头像上传5MB足够视频上传可能要按GB来算但这种情况一般不建议走应用服务器直传而是后端生成预签名URL让前端直传对象存储。接收多个文件时参数类型可以声明为MultipartFile[]或ListMultipartFile。文件上传最常见的坑在前端而不是后端。很多前端开发在手动构造FormData时自己写了一个Content-Type: multipart/form-data但忘记带boundary----WebKitFormBoundaryxxx。这样后端会解析失败抛MultipartException报错信息看起来云里雾里。排查这类问题最快的办法是抓包确认请求头看看boundary是否存在且与body一致。6. 11种方式一表总结与日常选型建议到这里11种接收方式全部讲完了。先汇总成一张速查表方便你平时翻序号方式写法适用场景典型前端请求1单个Query参数RequestParam分页、简单筛选、操作类型控制GET /list?page1size102路径参数PathVariableRESTful资源定位GET /user/1233JSON Body对象RequestBody新增、修改接口的复杂业务数据POST /user JSON4POJO对象绑定无注解直接传DTO多字段表单、GET复杂查询GET /search?namexage185数组接收String[]批量勾选、多ID通用处理GET /batch?ids1ids26List接收RequestParam List有序批量集合、集合运算GET /batch?ids1ids27Map接收RequestParam Map动态参数、无法预定义的筛选条件GET /filter?keyval8请求头参数RequestHeader认证信息、客户端信息Header里的Authorization9Cookie参数CookieValue登录态、用户偏好设置Cookie里的JSESSIONID10Servlet原生对象HttpServletRequest复杂混合读取、流读取、旧代码适配任意请求11文件与表单混合RequestPartMultipartFile文件上传、多文件上传POST /upload FormData6.1 我在项目里常用的选型规则基于多年的实际开发经验我一般按下面这套规则来做参数接收的选型GET请求的简单筛选1-3个参数用RequestParamGET请求的复杂筛选字段超过3个直接升级为POJO对象绑定RESTful资源操作路径参数定位资源查询条件放Query StringPOST/PUT接口业务数据一律RequestBody DTO Valid参数校验批量化操作ID列表用RequestParam ListLong最规范参数名不固定的扩展字段用Map兜底登录凭证、令牌放Header不放URL参数更不要放body文件场景普通字段和文件分开接收普通字段走RequestParam文件走RequestPart。还有两个细节想单独提一下。第一个关于DTO设计。我建议统一使用Java Bean规范注意属性和类型要对齐。boolean类型字段在Jackson和Spring自带的属性绑定器里有一些微妙的差异尤其是当你用了isXxx()这种命名风格时容易出现字段映射不到的诡异问题。稳妥的做法是boolean字段统一用Boolean包装类型并保持统一的getter/setter命名习惯。第二个关于接口语义。一个接口的参数位置要固定不能今天用Query传ID明天同一接口又改成body传这对前后端是对齐成本的一次次叠加。接口设计阶段就把参数位置确定下来后面会省掉大量沟通和改bug的时间。6.2 参数接不到值时的标准排查顺序最后分享一个排查思路。如果你在开发中发现Controller参数怎么都接不到值先别急着改代码按下面这个顺序来看前端真实请求打开浏览器F12或抓包确认URL、Content-Type、请求体原文长什么样对照参数位置参数在Query里就检查RequestParam或POJO绑定在路径里就检查PathVariable在JSON body里就检查RequestBody在multipart里就检查RequestPart检查参数名注解的value和前端传的key是否完全一致检查类型类型不匹配的典型表现是400或MethodArgumentTypeMismatchException检查请求方式方法标了GetMapping前端发的是POST参数也可能接不到尤其是RequestBody。我参与过的前后端参数接不上的纠纷几乎每次最后都能归因到上面这几条。把这份排查顺序记住能帮你省下大量无谓的联调时间。
返回列表