ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot接收参数的19种方式:从@RequestParam到@MatrixVariable

Spring Boot接收参数的19种方式:从@RequestParam到@MatrixVariable Spring Boot 里接收参数这件事说大不大说小不小。我见过不少同学写接口的时候一个RequestParam走天下遇到要接 JSON 对象、接文件、接请求头、接 Cookie 的时候就开始在搜索引擎里翻来翻去。也有些老手写了两三年代码突然被问到“带MatrixVariable的参数怎么接”当场愣住。这篇文章就把 Spring Boot 里接收参数的主流姿势全部撸一遍整理成 19 种方式从最常用的到最冷门的通通配代码示例、适用场景和踩坑提醒。你如果是刚入门照着抄就能用如果是想查漏补缺重点看后几节。1. 为什么“接收参数”值得单独写一篇先务个虚说清楚这事的底层逻辑。前端把数据交给后端本质上就是通过 HTTP 协议在请求行、请求头、请求体这三个位置把数据送过来。Spring MVC 在底层要做的只有一件事从这三块地方把原始数据捞出来再根据你声明的参数类型去做类型转换、对象绑定、数据校验最终交给 Controller 方法。所以你会发现 19 种方式看似五花八门拆开看就是一条主线参数来源不同 绑定方式不同组合出了不同的写法。这也是为什么很多人看源码或者文档时觉得混乱。PathVariable是从 URL 路径模板里取RequestParam是从查询字符串或表单里取RequestBody是把请求体里的 JSON/XML 反序列化成对象。这三兄弟定位完全不同却又经常同时出现在同一个接口上配合起来用容易让人搞混。把这条主线理清了你再去看那些冷门注解就很容易理解它到底在干吗。这篇文章适合谁来读如果你是初学者前五节里的内容已经能覆盖 90% 的开发场景照着例子调通一遍日常 CRUD 接口不会再卡壳。如果你已经在写业务代码我建议你重点看第四节和第五节RedirectAttributes、MatrixVariable、泛型参数这些场景平时用得少但面试和特殊业务里是真会遇到的。每一小节我都会标注“这是第几种方式”最终在第六节用一张总表把这些方式全部归位方便你收藏对照。2. 最常用的三兄弟路径变量、查询参数、JSON 主体2.1 方式一PathVariable从 URL 路径里捕获值RESTful 风格接口里最常见的就是把资源 ID 放在路径上比如GET /user/123。服务端要把这个123取出来就得用PathVariable。RestController RequestMapping(/user) public class UserController { GetMapping(/{id}) public User getUser(PathVariable Long id) { return userService.getById(id); } }几个细节注意一下。路径变量名和方法参数名一致时可以直接写PathVariable Long id不一致时用name指定比如PathVariable(uid) Long id。PathVariable还支持一个请求路径里同时出现多级变量GetMapping(/{province}/{city}/{district}) public String getRegion(PathVariable String province, PathVariable String city, PathVariable String district) { return province - city - district; }这算不算另一种接收方式严格说还是PathVariable的用法之一不是新方式。但多级路径在实际业务里很常见比如地区联动的接口地址就是/{province}/{city}/{district}这种写法。需要警惕的是路径变量默认是必填的如果请求没带上对应路径段会直接 404Spring 匹配不到这个 URL 模板。另外路径变量和查询参数在 REST 语义上有分工路径变量定位资源查询参数控制筛选和分页别把它们混在同一个位置上。2.2 方式二RequestParam查询参数和表单参数的标准入口接口地址问号后面的键值对比如GET /user/list?page1size20在 Spring Boot 里靠RequestParam接。GetMapping(/list) public PageResultUser list(RequestParam(page) Integer page, RequestParam(value size, defaultValue 10) Integer size) { return userService.page(page, size); }这里有几个要点。value指定前端传的字段名required默认是true也就是说前端不传这个参数会直接报 400 错误。日常开发里分页参数、搜索关键字这类常常带默认值建议用defaultValue而不是把required设成false再自己在方法体里判空。两者虽然都能容忍参数缺失但defaultValue可以让入参永远不为 null后续少一行空值判断。RequestParam不只是接 GET 查询串POST 提交application/x-www-form-urlencoded表单时也能用它接。它和RequestBody的分工要分清前者接的是请求体里按表单规则编码的键值对后者接的是 JSON 字符串。混着用会把请求体数据抢走导致两边都拿不到完整数据。2.3 方式三RequestBody把 JSON 直接变成 Java 对象前后端分离项目里前端传 JSON 已经成为默认事实。Spring Boot 处理 JSON 反序列化靠的是 Jackson一句RequestBody就能完成从 JSON 字符串到对象的转换。PostMapping(/save) public Result save(RequestBody UserSaveDTO dto) { return userService.save(dto); }这一段代码背后发生了什么Spring 通过HttpMessageConverter机制找到能处理application/json的转换器通常是MappingJackson2HttpMessageConverter把请求体的字节流读出来再按照 DTO 类的字段结构映射为对象。所以你只要把字段名定义好就能自动绑定省去了手动JSON.parseObject的过程。实际开发里常犯的错误有三个。第一前端 Content-Type 设置了text/plain或者表单格式但后端用RequestBody结果是 415 或 400因为找不到合适的转换器或者转不了。第二DTO 里的字段类型和 JSON 里的类型对不上比如 JSON 传字符串、Java 侧是IntegerJackson 默认会抛HttpMessageNotReadableException。第三多了没用的字段不会报错少了的字段也不报错但LocalDateTime这类特殊类型反序列化格式不对会直接抛异常需要在 DTO 字段上加JsonFormat或者在全局配置里指定格式。2.4 方式四三者混用时如何分工一个接口可以同时用PathVariable、RequestParam、RequestBody吗完全没问题。示例PostMapping(/order/{orderId}/pay) public Result pay(PathVariable Long orderId, RequestParam String channel, RequestBody PayRequest request) { // orderId 来自路径channel 来自查询串request 来自请求体 JSON }我遇到过有人把路径参数写在RequestBody的对象里接口地址只留一个空路径结果前端调接口时 URL 里写的 ID 根本传不进来。拆分的标准很简单路径上有模板变量就用PathVariable查询串或表单里有独立字段就用RequestParam主体是一段结构化数据就用RequestBody。三种方式互不冲突因为它们从 HTTP 报文的不同位置取数。3. 对象绑定这条线POJO、Map、List 与嵌套结构3.1 方式五不加注解直接把 POJO 当参数Spring MVC 支持在 Controller 方法里直接写一个不带注解的 POJO 参数它会把这个 POJO 的字段名和查询参数或表单字段做自动匹配。GetMapping(/query) public ListUser query(UserQuery query) { // 请求 /query?namezhangage18 // 会自动把 name 和 age 填进 UserQuery 对象 return userService.query(query); }这种方式的好处是参数多的时候不用一个个列RequestParam代码清爽很多。它的底层逻辑是ServletModelAttributeMethodProcessor在做数据绑定本质和ModelAttribute是一致的只是没写注解。需要注意基本类型和字符串的转换在这里也会自动发生比如前端传age18POJO 里是Integer age能正常绑定。但这种方式有个坑如果查询条件里含有嵌套对象比如前端传sort.orderasc这种带点的字段名需要 POJO 内部有对应的嵌套类结构才能绑定成功方式八里我会专门讲。3.2 方式六用 Map 兜底接收查询参数接口参数不确定或者不想为临时需求建一个 DTO 的时候可以直接用MapString, String来接查询参数。GetMapping(/any) public MapString, String any(RequestParam MapString, String params) { return params; }RequestParam加MapString, String的写法会把所有查询参数收进一个 Map。注意这里 Map 的泛型只能是String, String如果希望 Spring 帮你做类型转换那还是老老实实建 DTO。Map 接收看着方便但会把所有参数当成字符串后续要使用还得自己转型代码里全是Integer.valueOf(...)维护起来很痛苦。我的原则是临时脚本、调试接口可以用 Map正式业务尽量用 POJO。3.3 方式七数组和 List 接收同名字段前端传?ids1ids2ids3这种重复同名参数时后端可以用数组或 List 一把接住。GetMapping(/batch) public Result batch(RequestParam(ids) ListLong ids) { // ids 是 [1, 2, 3] return Result.ok(ids); }数组写法RequestParam(ids) Long[] ids同样支持。这里有个容易误会的细节URL 里ids1,2,3逗号分隔和ids1ids2ids3重复键是两种不同的传法Spring 默认不会把逗号分隔的字符串自动拆成 List除非配置了转换器或者前端老老实实传重复键。JSON 请求体里也能接收数组但那是RequestBody ListLong写在请求体里和查询串的 List 完全是两码事注意区分。3.4 方式八嵌套对象绑定字段名里带“点”的学问参数还是一个对象但对象里套对象时Spring 允许前端用“点号”路径表示嵌套关系。假设查询条件类public class OrderQuery { private String status; private PageQuery page; // 里面有 pageNum、pageSize }前端这样传就能成功绑定/order/list?statusPAIDpage.pageNum1page.pageSize20Controller 里写OrderQuery query即可query.getPage().getPageNum()就能拿到分页参数。这个写法的关键是字段名中间用点号和 JSON 里的嵌套结构表达是两套逻辑。很多人在这踩坑是因为前端把参数拼成了page[pageNum]1这种格式Spring 默认绑定器不认识方括号直接忽略。要解决要么前端改成点号路径要么在 POJO 上写JsonAlias加映射但更推荐直接约定前端统一用点号。3.5 方式九RequestBody 泛型集合请求体里直接传一个数组的 JSON比如[{“id”:1},{“id”:2}]后端可以这样接PostMapping(/batch) public Result saveBatch(RequestBody ListUserSaveDTO list) { return userService.saveBatch(list); }这算是RequestBody的一种特殊形式但它单独拎出来说的原因是很多人第一次看到ListUser作为参数时不确定 Spring 能不能反序列化。答案是完全可以Jackson 会根据泛型类型解析出集合元素的具体类型。如果请求体是{users:[{...},{...}]}这种带外层包装的结构就不能直接接List了需要包一层对象public class BatchRequest { private ListUserSaveDTO users; }两种结构对应两种写法看你接口的设计。我建议接口层尽量用轻量 DTO 暴露给前端别把实体类直接当接收参数使用避免底层表结构和接口契约绑死。4. 从 HTTP 上下文里拿数据Header、Cookie、Session 与重定向4.1 方式十RequestHeader 读取请求头请求头里放的一样是数据比如 Token、客户端版本号、语言偏好。Spring 提供RequestHeader来读。GetMapping(/info) public Result info(RequestHeader(Authorization) String token, RequestHeader(value X-Client-Version, required false) String version) { return Result.ok(); }和RequestParam一样的套路支持required、支持defaultValue、支持Map兜底。实际开发里常见的场景是网关鉴权后把用户信息放进请求头下游服务用RequestHeader取。需要注意的是请求头名称是大小写不敏感的Authorization和authorization都能取到但写代码时还是保持规范大小写比较好。4.2 方式十一CookieValue 直接拿 Cookie 值登录态常常以 Cookie 形式存在浏览器里CookieValue就是为这个场景准备的。GetMapping(/me) public Result me(CookieValue(value SESSION, required false) String sessionId) { return Result.ok(sessionId); }这个注解同样支持defaultValue。需要提醒的是Cookie 本身只是浏览器存储的一小段数据不要在里面放敏感明文作用域和过期时间也要留意。在 Spring Boot 里如果你想拿到整个 Cookie 数组而不是某一个值可以通过HttpServletRequest去拿那就是方式十四的内容了。4.3 方式十二ModelAttribute 显式绑定表单/查询参数到对象前面讲到无注解 POJO 会自动绑定那ModelAttribute就是把这件事显式写出来。PostMapping(/register) public Result register(ModelAttribute RegisterDTO dto) { return userService.register(dto); }显式和隐式的绑定规则完全一样区别在于ModelAttribute在 Spring MVC 的生命周期里还有一个额外作用它会把绑定好的对象放到 Model 中使得后续在视图渲染时可以引用。现在前后端分离时代这个特性用得不多了但如果你在做服务端渲染页面或者接手老项目会看到这种写法的存量代码。一个容易踩的坑是ModelAttribute绑定的是表单/查询参数不是 JSON 请求体前端如果用 JSON POST 且没加RequestBody结果就是对象里全是 null。4.4 方式十三SessionAttribute 和 Session 中的参数和单个 Cookie 不同会话里的数据可以用SessionAttribute直接从当前 Session 中取出。GetMapping(/cart) public Result cart(SessionAttribute(value cart, required false) Cart cart) { // 前提是之前往 session 里塞过 cart return Result.ok(cart); }和ModelAttribute容易混淆的另一个注解是SessionAttributes它用在 Controller 类上配合ModelAttribute把对象暂存到 Session。这俩是 Spring MVC 早期做表单回显和跨请求暂存的设计现在新项目中很少主动去用。如果你只是想拿当前登录用户现在主流做法还是从 Token 里解析用户信息而不是依赖 Session。安全性地讲Session 数据存在服务端内存里集群环境下要做 Session 共享复杂度不低这也是它在新架构里逐渐失宠的原因。4.5 方式十四RedirectAttributes重定向时带参数的体面姿势重定向场景POST-Redirect-GETPRG 模式下普通模型数据拿不到重定向后的新请求是全新的 HTTP 请求。这时要用RedirectAttributesPostMapping(/create) public String create(ProductDTO dto, RedirectAttributes redirectAttributes) { Long id productService.create(dto); redirectAttributes.addAttribute(id, id); redirectAttributes.addFlashAttribute(message, 创建成功); return redirect:/product/view; }这里有两个概念。addAttribute会把参数拼到重定向 URL 的查询串上新请求可以用RequestParam接addFlashAttribute则把数据存在 Flash Map 里重定向后立即失效新请求里用ModelAttribute或直接从Model里取。Flash 数据是一次性的正好适合提示消息这种用完即弃的场景。如果你不是返回视图字符串而是用RedirectView或者ResponseEntity做重定向RedirectAttributes的注入方式会略有差异。实际业务中 PRG 模式能很好防止表单重复提交但要注意addAttribute的参数会出现在 URL 上敏感信息不要用这种方式传递。5. 更“非常规”的姿势文件、原生 API、矩阵变量与泛型5.1 方式十五文件上传MultipartFile 和 RequestPart文件上传和前面所有方式都不同请求体是multipart/form-data格式数据被分成多个 part每个 part 可以是一个普通字段也可以是一个文件。PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { String originalFilename file.getOriginalFilename(); long size file.getSize(); // 保存文件... return Result.ok(originalFilename); }更规范的写法是用RequestPart显式声明PostMapping(/upload) public Result upload(RequestPart(file) MultipartFile file, RequestParam(description) String description) { return Result.ok(file.getOriginalFilename() : description); }RequestPart和RequestParam的区别在于RequestPart会把 part 的内容交给HttpMessageConverter去转换对于MultipartFile来说是获取文件流而RequestParam专门处理multipart/form-data里的表单字段和文件项。多文件上传时用MultipartFile[]或多文件同名 part 都能接住。注意文件大小限制Spring Boot 默认限制单文件 1MB、单次请求 10MB超出会抛MaxUploadSizeExceededException需要在配置里调整spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB5.2 方式十六直接用 Servlet 原生 APISpring MVC 没有阻断你和 Servlet 容器打交道Controller 方法里可以直接声明HttpServletRequest、HttpServletResponse、HttpSession这些原生对象框架会自动注入。GetMapping(/native) public Result nativeParam(HttpServletRequest request, HttpServletResponse response, HttpSession session) { String param request.getParameter(name); String header request.getHeader(User-Agent); String[] ids request.getParameterValues(ids); return Result.ok(param : header); }这种方式在你的代码里会拿到最原始的数据结构灵活性最强。但灵活性也是代价代码必须自己处理 null 判断、类型转换测试时也不容易 mock。我的习惯是原生 API 只用来处理框架不好覆盖的边界场景比如自定义参数解析、拦截器里的公共逻辑正常情况下还是用 Spring 的注解式写法和类型安全。5.3 方式十七WebRequest 和 NativeWebRequest如果你不想直接依赖 Servlet API但又需要比注解更灵活的访问方式可以用WebRequest。GetMapping(/web) public Result webParam(WebRequest webRequest) { String param webRequest.getParameter(name); Object attr webRequest.getAttribute(key, RequestAttributes.SCOPE_REQUEST); return Result.ok(param : attr); }WebRequest是 Spring 对请求访问的抽象层接口方法不多但够用比HttpServletRequest更轻量在单元测试里也更好模拟。NativeWebRequest则是在WebRequest基础上额外提供getNativeRequest()方法可以随时把底层原生对象掏出来。实际开发中用得少但如果你的代码想保持对 Servlet 容器的弱依赖用WebRequest是个合理选择。5.4 方式十八MatrixVariable路径分号后的参数这个冷门注解值得单独说一说。URL 路径中分号后面的内容可以携带矩阵参数比如GET /user/42;age18;citybeijing。Spring 支持用MatrixVariable解析GetMapping(/user/{id}) public Result user(PathVariable String id, MatrixVariable(required false) Integer age, MatrixVariable(required false) String city) { return Result.ok(id : age : city); }Spring Boot 默认没有开启矩阵参数支持要在配置里设置Configuration public class WebConfig implements WebMvcConfigurer { Override public void configurePathMatch(PathMatchConfigurer configurer) { configurer.setRemoveSemicolonContent(false); } }为什么默认关闭因为分号常被用来做 URL 路径参数或追踪信息removeSemicolonContent默认是 true即 Spring 会把分号及其后面的内容移除。这个功能确实冷门但拦截器删参数、A/B 测试、REST API 某些特殊场景下偶尔能用上。面试官问起矩阵参数的时候大多数候选人答不上来你知道它的存在和用法就已经赢了一半。5.5 方式十九RequestParam 单独配合数组、集合、Map 已经够用但别漏了多路径组合严格列到这里19 种方式已经全部覆盖。我再补一个容易被忽略的实用细节多个路径变量 数组参数 必要查询参数的组合写法。GetMapping(/store/{storeId}/products/{categoryId}) public Result products(PathVariable Long storeId, PathVariable Long categoryId, RequestParam(value tags, required false) ListString tags) { return Result.ok(); }这种组合不是新注解但很多新手看到控制器方法里三四个注解同时出现会犯晕。记住一个原则每个注解管一段数据源各司其职不会互相干扰。真正会互相干扰的情况是同一个请求体中RequestBody和ModelAttribute同时出现去抢请求体的解析权实际这种写法本来就不该出现在一个方法里。6. 十九种方式对照表一口气写了这么多担心你记混这里做一张汇总表。建议直接收藏写接口前翻一眼。序号接收方式核心注解/类数据来源典型场景1路径变量PathVariableURL 路径模板RESTful 资源定位2查询参数/表单字段RequestParam查询串、表单键值对分页、搜索、简单参数3JSON 请求体RequestBodyapplication/json 请求体新增、更新、复杂对象4查询参数/表单字段 MapRequestParam Map查询串、表单键值对动态参数、调试接口5查询参数/表单字段 List/数组RequestParam List查询串重复同名键批量 ID 传入6POJO 隐式绑定无注解 POJO查询串、表单字段多字段查询条件7嵌套对象绑定POJO 内嵌类点号路径字段名查询条件内含分页/排序8表单绑定显式声明ModelAttribute查询串、表单字段老项目、表单提交9请求体直接接泛型集合RequestBody ListTJSON 数组请求体批量保存10请求头RequestHeaderHTTP HeaderToken、版本号等11CookieCookieValueCookie会话标识、简单标记12Session 属性SessionAttributeHttpSession会话级数据读取13重定向参数RedirectAttributesFlash Map URL 查询串PRG 模式防重复提交14文件上传RequestPart/MultipartFilemultipart/form-data单文件、多文件上传15原生 Servlet APIHttpServletRequest 等整个 HTTP 报文边界场景、Filter 风格逻辑16Spring 请求抽象WebRequest查询串、Attribute弱依赖 Servlet 的轻量取值17矩阵参数MatrixVariable路径分号后的参数特殊 REST 设计18请求体 包装对象RequestBody DTOJSON 对象请求体对象里含 List 或嵌套结构19多注解组合上述任意组合多数据源复杂业务接口7. 实操中最容易翻车的几个场景代码写完不等于接口就能正常用下列几种异常我几乎每天都能在日志里看到。逐个排查一遍能省下你不少调试时间。7.1 400 Bad Request参数类型对不上前端传?ageabc后端方法参数是Integer ageSpring 做类型转换失败直接返回 400日志里能看见Failed to convert value of type java.lang.String to required type java.lang.Integer。排查思路很简单顺着错误信息找到字段名再对照前端实际入参十有八九是类型不匹配或日期格式不对。如果前端实在无法改你可以在 Controller 方法里改用 String 接收再手动转换。7.2 415 Unsupported Media TypeContent-Type 用错前端明明传的是 JSON 字符串但 Content-Type 是text/plain后端又是RequestBodySpring 找不到能处理这种 Content-Type 的消息转换器回 415。这类问题本质是前后端约定不一致我一般建议前端统一用axios这类库默认已经是application/json。如果是微信小程序或工具类请求特别容易漏掉这个头。7.3 嵌套对象绑定失败字段名里点号被吞我遇到过前端传page.pageNum1但后端 POJO 里没有 Page 内部类结果整个 pageNum 静默丢失接口不报错但查不出数据。这种诡异问题最坑人因为 200 状态码让你以为一切正常。排查时在 Controller 里临时输出整个 POJO一眼就能看出某个嵌套字段是 null。记住嵌套绑定要求 POJO 内部有对应类型的字段并且前端字段名用点号连接缺一个条件都绑定不成功。7.4 文件大小超出限制默认 1MB 文件上限前端传个 2MB 的图片就抛异常。这不是代码问题是配置问题在application.yml里调大max-file-size和max-request-size即可。还有个隐性坑Nginx 等反向代理默认client_max_body_size是 1MBSpring Boot 这边调大了前端还是报 413得两头一起调。7.5 requiredfalse 还报错看看是不是全局异常拦截器的锅RequestParam(required false) Integer page明明写了可空前端没传时还是 400。这种情况常见于方法里同时存在其他必填参数报错但异常信息被全局异常处理包装后你只看到一个笼统的 400分不清是哪个参数的问题。建议项目里自定义全局异常处理器时把MethodArgumentTypeMismatchException和MissingServletRequestParameterException的信息原样透出带上字段名排查效率会高很多。回头看我列出的 19 种方式其实没有一种是“银弹”。日常开发中80% 的接口用PathVariable、RequestParam、RequestBody这三个就能搞定剩下的方式属于锦上添花和应对特殊需求。但正因为它们冷门面试的时候问一句“你有没有用过矩阵参数”就能筛掉一批人。我个人的体会是不要为了炫技在正常接口里使用花哨写法团队协作最重要的是可读性和稳定性。但作为个体开发者把这张清单吃透至少写接口时不会再有“这个参数我该用什么接”的犹豫。真遇到了回来看一眼对照表马上就有答案。
返回列表