资讯动态

Spring Boot接收前端参数的11种方式,联调时不再为null发愁

发布时间:2026/10/14 12:56:41 来源:尧图企业网站定制
前端同事跑过来和我说接口地址没错、参数也传了Postman里都能通可后端一接就是 null。这种现场我见得太多了十次里有八次其实不是业务逻辑的问题而是“前端把参数放的位置”和“后端接收的方式”没对上。Spring Boot 项目接收前端参数这个话题看着基础真到了联调的时候才发现水很深有人在 RequestBody 里等表单字段有人在无注解的 POJO 里接 JSON还有人直接用 HttpServletRequest 手动 getParameter各有各的委屈。这篇文章我打算把 Spring Boot 接收前端参数的常见姿势完整盘一遍一共 11 种。每一种我都会给出典型的前端传参方式、Controller 写法、背后的处理逻辑以及实际操作中最容易踩的坑。适合刚接触 Spring Boot 的初学者快速建立认知框架也适合写了两三年接口但没系统梳理过的同学查漏补缺。阅读建议不用一口气背下来先记住一个核心口诀——参数在 URL 上就考虑 RequestParam 和 PathVariable在请求头里就用 RequestHeader 和 CookieValue在请求体 JSON 里就用 RequestBody在表单里优先表单绑定。剩下的 RequestPart、HttpServletRequest、MatrixVariable 属于特殊场景碰到再翻。1. 先拆清前端的参数落在HTTP的哪个位置1.1 三种位置决定了三种接收思路HTTP 请求本身是“一行请求行、一堆请求头、一个可选的请求体”前端传参无非是往这三个位置里塞数据。第一种是 URL 上的包括查询字符串和路径参数。查询字符串就是常见的?page1size20路径参数是/user/1001这种大括号里的值。这一类参数在服务端看到的是字符串需要由框架帮你转换类型转换失败时通常报 400。第二种是请求头比如Authorization、X-Request-Id、User-Agent主要用来传递身份凭证和元信息很少用来传业务数据但业务上偶尔确实要用。第三种是请求体里面最常见的格式是 JSON 和表单还可能是文件上传的 multipart。请求体的接收逻辑和前面两类差别很大JSON 要经过反序列化表单要走数据绑定文件得单独处理。理解了这个框架再看 Spring Boot 各种参数注解就不会乱。每个注解本质上都是在告诉 Spring“我要从 HTTP 的哪个位置、以什么形式拿数据”。很多联调问题的根因就是前端把参数放到了 A 位置后端却用 B 位置的注解去收两边各自有理数据却对不上。1.2 一张表看懂11种方式在展开讲之前先放一张总览表。后面每一节都会对应这张表里的某项遇到问题时你可以先翻回这里定位方向。序号接收方式参数实际位置典型场景1RequestParam查询字符串 / 表单字段分页参数、搜索条件、表单基础字段2PathVariableURL 路径RESTful 接口里的资源 ID3RequestHeader请求头获取 Token、版本号、客户端信息4CookieValueCookie登录态、用户标识5RequestBodyJSON 请求体复杂对象、嵌套结构、批量数据6无注解 POJO 绑定查询字符串 / 表单字段表单较多字段时自动映射7ModelAttribute查询字符串 / 表单字段显式表单绑定、多对象绑定8RequestBody Map/JsonNodeJSON 请求体动态 JSON 结构、不确定字段名9RequestPartmultipart 请求体文件上传且表单中同时有普通字段10HttpServletRequest任意原始位置老旧代码迁移、特殊格式解析11MatrixVariableURL 分号参数RESTful 路径内嵌元数据这张表能帮你建立一个基本判断遇到参数问题先问“这个数据到底在 HTTP 的哪个位置”再决定用哪个注解。接下来我按位置分三部分讲把每种的细节和坑位都说清楚。2. 请求行和请求头里的四种拿参方式2.1 RequestParam最常用的查询参数与表单字段接收RequestParam 是大多数人接触的第一个参数注解它默认从查询字符串取数据比如前端这样请求axios.get(/api/user/list, { params: { page: 1, size: 20, keyword: 张三 } })服务端对应写法是GetMapping(/api/user/list) public R list(RequestParam Integer page, RequestParam Integer size, RequestParam String keyword) { return R.ok(...); }Spring 拿到page1size20keyword张三后会自动把字符串1转成 Integer 类型的1。这就是框架帮你做的类型转换前端不用关心后端要什么类型传字符串即可。这里有几个容易忽视的细节。第一个是required属性默认是true也就是说前端没传这个参数时接口直接 400。但分页参数在很多系统里是可选的所以建议这样写public R list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 20) Integer size) {这样前端不传页面相关参数时接口也能正常工作而且 defaultValue 同样会做类型转换字符串1会被转成 Integer。第二个细节是参数名不一致的问题。前端传userName后端字段叫username可以在注解里显式声明public R list(RequestParam(userName) String username) {这个映射功能在联调时特别好用尤其是前后端命名规范不一致的团队。第三个细节是接收数组和 List。前端如果这样传?id1id2id3后端可以直接写RequestParam ListLong idsSpring 会按同名参数聚合如果前端传的是?ids1,2,3Spring 默认也能按逗号拆分后转成 List。这一点比很多人想的灵活。2.2 PathVariableRESTful路径参数RESTful 风格接口普遍把资源 ID 放在路径上比如/api/user/1001而不是/api/user?id1001。Spring Boot 对应的接收方式就是 PathVariableGetMapping(/api/user/{id}) public R detail(PathVariable(id) Long id) { return R.ok(...); }前端 axios 写法相应变成axios.get(/api/user/${userId})PathVariable 的规则很简单路径占位符名字和注解里的名字一致Spring 就会把 URL 中对应位置的字符串提取出来并转成方法参数类型。开发中更常用的是直接在方法签名上声明的写法public R detail(PathVariable Long id) {这个能成立靠的是 Spring 用参数名推断占位符名。但编译后的 class 文件默认不保留参数名所以很多时候会报“Name for argument of type java.lang.Long not specified”之类的错误。保险起见显式写注解值更稳。路径参数有一个天然的限制不能包含/。比如想要传一个日期范围2024-01-01没问题但想传路径本身或带斜杠的内容就会出问题。遇到这种场景要么前端做 URL 编码要么换 RequestParam 传查询字符串。另外路径参数和查询参数经常同时出现比如GET /api/user/1001/orders?page1这就是 PathVariable 和 RequestParam 混用两个注解各管各的位置互不干扰。2.3 RequestHeader从请求头里取身份信息与客户端信息业务数据一般不放在请求头里但身份认证和元信息经常放。比如登录后的 Token或者客户端上报的版本号很多团队会选择放在自定义 Header 中。前端这样设置axios.get(/api/order/detail, { headers: { X-Client-Version: 2.4.0, Authorization: Bearer xxxxx } })后端接收GetMapping(/api/order/detail) public R detail(RequestHeader(Authorization) String token, RequestHeader(X-Client-Version) String version) { return R.ok(...); }RequestHeader 和 RequestParam 的行为非常相似也支持 required、defaultValue用法上可以迁移理解。实际项目中 Token 经常被多个接口读取直接在每个接口方法里加参数会显得啰嗦更优雅的做法是用拦截器或 HandlerInterceptor 统一从 header 里取。参数接收层面知道有这个能力就够应付大多数场景了。这里有一个前端容易忽略的点自定义 Header 会触发浏览器 CORS 预检请求。也就是说如果前后端分开部署前端发起一个带自定义 header 的请求时浏览器会先发一个 OPTIONS 请求确认服务端允许再来发真正的业务请求。很多初学者发现自己请求“发不出去了”其实是后端没有处理 OPTIONS 请求和参数本身没关系。后端在 CorsFilter 或 WebMvcConfigurer 里正确配置跨域后这个问题自然消失。2.4 CookieValue读取登录态与用户标识Cookie 在前后端分离的项目里用得比以前少了但依然有它的位置。比如单点登录后浏览器自动带上会话 Cookie或者某些系统把用户语言偏好放在 Cookie 里。后端读取非常直接GetMapping(/api/user/preference) public R preference(CookieValue(value lang, defaultValue zh-CN) String lang) { return R.ok(...); }前端如果用原生 fetch 或 axios必须开启凭证才能收到并发送 Cookieaxios.get(/api/user/preference, { withCredentials: true })不开启 withCredentials 时浏览器通常不会在跨域请求中携带 Cookie后端的 CookieValue 自然取不到值这是前后端协作中一个高频坑。另外如果 Cookie 设置了 HttpOnly 属性前端 JavaScript 是读不到这个 Cookie 的但浏览器会在请求时自动带上后端依然可以用 CookieValue 读取。所以 HttpOnly 并不影响后端拿值它只是防止 XSS 脚本窃取 Cookie。3. 请求体绑定的四种写法从JSON到表单3.1 RequestBodyJSON结构的主力写法现在的项目绝大多数都是前后端分离JSON 是请求体的绝对主流。后端用 RequestBody 直接接收一个对象是工作中最常用的数据接收方式。前端这样传axios.post(/api/user/save, { name: 张三, age: 28, address: { city: 杭州, street: 某路1号 } })后端对象接收PostMapping(/api/user/save) public R save(RequestBody UserDTO user) { return R.ok(...); }RequestBody 背后的机制是 Spring 根据请求头里的 Content-Type 找到对应的 HttpMessageConverter然后把请求体内容反序列化成方法参数类型。对于 JSON 来说负责干活的是 Jackson。前端只要保证Content-Type: application/json后端就能把 JSON 里每个字段按名字绑定到 UserDTO 的属性上。这里可以放心的是嵌套对象和泛型。上面的 UserDTO 里如果包含 Address 对象Jackson 会自动递归反序列化。如果接口需要批量接收直接这样写PostMapping(/api/user/batch) public R batch(RequestBody ListUserDTO users) {Spring Boot 对ListUserDTO这种泛型做了支持不需要额外配置。需要注意的一点是前端如果传了整个 null也就是请求体内容就是 null 两个字RequestBody 这边会收到一个 null方法里直接调用 users.size() 会空指针建议加参数校验或判空。3.2 无注解POJO绑定表单提交的“隐形”姿势前端的表单提交如果后端根本没有写任何注解Spring 也能把参数绑定到一个对象上。比如前端这样提交POST /api/user/register Content-Type: application/x-www-form-urlencoded name张三age28city杭州后端可以这样写PostMapping(/api/user/register) public R register(UserDTO user) { return R.ok(...); }UserDTO 里只要有 name、age、city 这三个字段Spring 就会自动填充。这种写法叫数据绑定它和 RequestParam 取单个参数本质上是一套体系只不过 Spring 帮你按字段名映射到了对象上。很多人第一次看到这种写法都会疑惑不加注解也能接参数确实可以这是 Spring MVC 从早期版本就有的能力底层是 WebDataBinder 在做属性绑定。它的规则也很“宽容”字段名嵌套时可以用点号比如前端传address.city杭州对应的 DTO 里有一个 address 对象address 里面有一个 city 字段Spring 也能绑定上。那什么时候用这种无注解绑定什么时候用 RequestBody核心看请求体的格式。如果是application/x-www-form-urlencoded或查询字符串用无注解 POJO 绑定如果是 JSON必须用 RequestBody。这里有个容易犯的错前端用 axios 直接传一个 JS 对象默认会序列化成 JSON后端却用无注解 POJO 去接结果绑定不上所有属性都是 null。这不是后端代码的问题是请求格式和接收方式不匹配。无注解 POJO 绑定还有一个局限它不太适合处理复杂嵌套的 JSON 结构JSON 的层级嵌套和动态数组绑定在这种模式下会很别扭这种情况还是得用 RequestBody。3.3 ModelAttribute显式绑定多个表单对象ModelAttribute 在 JSP 时代是非常主流的写法Spring Boot 项目里用得少了但它仍是一个值得掌握的显式方案。它和无注解 POJO 绑定背后的原理一样差别在于它把“我要绑定这个对象”这件事写在了明面上PostMapping(/api/user/register) public R register(ModelAttribute(user) UserDTO user) { return R.ok(...); }显式声明的好处是能同时绑定多个对象而不产生歧义。比如注册接口既要收用户基本信息又要收地址信息表单字段设计成user.name张三address.city杭州后端就可以这样PostMapping(/api/user/register) public R register(ModelAttribute(user) UserDTO user, ModelAttribute(address) AddressDTO address) {没有注解时如果把 UserDTO 和 AddressDTO 同时放参数列表里Spring 按类型判断有时会搞不清该把字段绑给谁显式命名就解决了这个问题。需要注意的是方法上的 ModelAttribute 和参数上的 ModelAttribute 是两个概念。方法级 ModelAttribute 是往 Model 里放数据参数级才是接收前端参数两者不要混淆。实际项目里我的判断是单对象表单绑定用无注解 POJO 更简洁多对象、多前缀绑定就用 ModelAttribute 显式指定避免后续维护的人看不懂参数归属。3.4 RequestBody Map/JsonNode动态JSON结构的兜底方案接口有时候面对的是“字段不固定”的 JSON比如前端要传一个动态配置项集合业务上无法预先定义 DTO。这时候最灵活的方式是用 Map 或 JsonNode 接收。PostMapping(/api/config/save) public R save(RequestBody MapString, Object config) { Object value config.get(retryCount); return R.ok(...); }或者用 JsonNodePostMapping(/api/config/save) public R save(RequestBody JsonNode node) { String name node.path(name).asText(); int retryCount node.path(retryCount).asInt(0); return R.ok(...); }两者都能接任意结构的 JSON但实践中我更推荐 JsonNode。原因很现实JSON 数值在 Jackson 反序列化时可能是 Integer也可能是 Long、BigDecimal取决于数值大小和是否有小数位。用 Map 时你得自己判断 value 的类型再强转很容易出现Integer cannot be cast to Long这种诡异异常而 JsonNode 提供了 asInt、asLong、asText 等明确的方法取值时还能给默认值代码清晰且不容易踩类型坑。当然使用 Map 或 JsonNode 也意味着放弃了编译期类型检查接口的字段规范性降低。我的建议是只把它用来兜底比如配置类接口、低代码表单、外部回调数据。凡是能定义出稳定 DTO 的业务接口还是优先用具体类型生产环境里明确定义的参数比“灵活”更重要。4. 三个藏在角落里的拿参入口4.1 RequestPartmultipart表单里既有字段又有文件文件上传接口是每个项目都会遇到的场景。前端用 FormData 提交时请求体是 multipart/form-data里面既有普通表单字段也有文件。一个常见的写法是const formData new FormData(); formData.append(name, 张三); formData.append(file, fileObj); axios.post(/api/file/upload, formData, { headers: { Content-Type: multipart/form-data } })后端设计接口时比较容易犯的一个错误是试图用 RequestPart 同时接收字符串字段和文件PostMapping(/api/file/upload) public R upload(RequestPart(name) String name, RequestPart(file) MultipartFile file) {实测下来很容易出问题。因为 RequestPart 内部会按 part 的 Content-Type 去做消息转换普通文本字段的 Content-Type 通常是 text/plain和 String 类型本身对不上接口偶尔会报UnsupportedMediaTypeException或直接收不到 name。更稳妥的做法是普通字段用 RequestParam文件用 RequestPart 或 MultipartFilePostMapping(/api/file/upload) public R upload(RequestParam(name) String name, RequestPart(file) MultipartFile file) {这种“字段归字段、文件归文件”的组合在项目里是最稳的。另外如果一次上传多个文件MultipartFile 也可以写成数组public R upload(RequestParam(name) String name, RequestPart(files) MultipartFile[] files) {前端 FormData 里多次 append 同一个files字段即可。4.2 HttpServletRequest回到最原始的方式Spring 并没有把开发者“关死”在注解里你完全可以用最原始的 Servlet API 拿参数。方法参数里直接声明 HttpServletRequest然后在方法体里自己取PostMapping(/api/legacy/save) public R save(HttpServletRequest request) { String name request.getParameter(name); String city request.getHeader(X-City); return R.ok(...); }这种写法在什么情况下有用最常见的是老旧系统迁移有些底层封装已经围绕getParameter写了大量逻辑你没必要把每个方法都改成新注解直接用原生 request 就能保持兼容。其次是特殊格式解析比如请求体是某种自定义文本格式框架默认的转换器处理不了你可以在方法里通过request.getInputStream()读取原始字符串再自己解析。但这里要澄清一个常见误区request.getParameter()不是万能的。它只能拿到查询字符串里的参数和application/x-www-form-urlencoded格式的请求体参数。如果前端用Content-Type: application/json提交getParameter 拿到的永远是 null必须用request.getInputStream()读取原始 body 再手动反序列化。所以“用 HttpServletRequest 就能通吃所有请求”是个错误认知它只是把所有控制权交给了你具体怎么解析还得你来定。4.3 MatrixVariable带分号参数的冷门玩法MatrixVariable 是很多人没听说过的方案它接收的是 URL 里分号后面的参数。比如这样一个地址GET /books/978-3-16-148410-0;authortom;langzh路径里的978-3-16-148410-0是资源 ID分号后面还可以跟一组键值对作为路径内嵌的元数据。后端的接收方式GetMapping(/books/{isbn}) public R book(PathVariable(isbn) String isbn, MatrixVariable(value author, required false) String author, MatrixVariable(value lang, defaultValue en) String lang) {不过这个功能默认是不生效的。Spring 在解析路径时会默认移除分号之后的内容也就是说 PathVariable 拿到的是978-3-16-148410-0分号后面的 author 和 lang 会被丢掉。你需要先告诉 Spring 不要移除分号内容在配置类里加一个 BeanBean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper new UrlPathHelper(); urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } }; }配置完之后 MatrixVariable 就能正常工作了。说实话这个用法在日常业务里非常少见RESTful 接口一般不会把元数据放在分号后面。但有时候我们看到别人的项目里有这种写法至少得知道它不是“魔法”而是 Spring 提供的标准能力。了解即可真正用不用取决于团队约定。5. 参数接收里最容易翻车的几个细节5.1 同名、大小写与参数名拼写查错常常先看这里联调现场最常见的假警报是“我明明传了参数后端怎么收到 null”第一步先查参数名是否完全一致。HTTP 参数名是区分大小写的前端传userName后端注解写username拿到的就是 null。如果不想让前端改后端可以用注解显式映射比如 RequestParam(userName)但大多数团队还是建议统一命名规范从源头减少这种摩擦。第二个容易被忽略的是同名参数。前端传?typeatypeb后端如果写RequestParam String type会拿到其中某一个值吗不一定。Spring 对这种场景有字符串数组转换逻辑很多时候直接报错或取不到预期值。如果想明确接收多个同名前端参数就声明成 List 或数组。这与业务上“一个参数最多传一个值”的常规约定要匹配建议前后端约定清楚重复传参除非是特意设计否则视为非法请求。第三个常见问题是字段名用下划线还是驼峰。前端如果传 JSON里面有user_name后端 DTO 属性是userName接口会绑定不上。Jackson 默认要求字段名精确匹配Java 属性名userName对应 JSON 里的userName。想兼容下划线风格可以给字段加JsonProperty(user_name)也可以全局配置spring.jackson.property-naming-strategy: SNAKE_CASE。但要注意全局配置只影响 JSON 反序列化不影响表单绑定。表单绑定看的是字段名不会因为 SNAKE_CASE 配置自动映射。这套差异很容易导致“同一个对象JSON 传没问题表单传 null”排查的时候要有这个意识。5.2 类型转换与日期格式前端传“字符串”后端要“数字”怎么办HTTP 传参本质都是字符串Spring 帮你做类型转换但转换是有边界的。前端传ageabc后端要 Integer接口会抛出类型转换异常最终返回 400。这里要区分两种情况RequestParam、PathVariable 这类请求行参数转换失败抛的是MethodArgumentTypeMismatchExceptionRequestBody 反序列化失败抛的是HttpMessageNotReadableException。如果你要做统一异常处理需要把这两个异常分别挂在 RestControllerAdvice 里否则默认响应信息对前端很不友好。日期格式是另一个高频翻车点。前端传2024-03-18 10:30:00这种“带空格”的格式后端用 LocalDateTime 直接接收会失败因为 Jackson 默认处理的是 ISO 格式。如果前端传的是标准 ISO 格式2024-03-18T10:30:00那是没问题的。想让后端接收自定义格式可以在 LocalDateTime 字段上配置统一的反序列化器或者在参数上用 DateTimeFormatpublic R query(RequestParam DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss) LocalDateTime startTime) {这里要特别留意DateTimeFormat 对表单绑定的参数有效但对 RequestBody 的 JSON 反序列化不一定生效。JSON 解析走的是 Jackson 的规则你需要在全局配置里设置日期格式或写自定义 deserializer。很多团队在联调时栽在这上面表单接口能收yyyy-MM-ddJSON 接口却收不了原因就是两种绑定路径处理日期的机制不一样。5.3 Content-Type不一致参数“消失”的幕后黑手参数接不到很多时候不是后端代码写错了而是请求头里的 Content-Type 和接收方式不匹配。客户端用 axios 提交一个对象时axios 默认会把它序列化成 JSON但如果你手动设置了Content-Type: application/x-www-form-urlencoded后端 RequestBody 就会拿不到数据或直接报“Content type not supported”。反过来用原生表单提交application/x-www-form-urlencoded时后端如果用无注解 POJO 绑定没问题但用 RequestBody 就不行。有一个项目里的典型报错是Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type application/x-www-form-urlencoded not supported]这个异常的用户体验很差前端看不到具体原因只看到 415。现在的前端框架很少会主动设置错误的 Content-Type更多时候是移动端原生代码或第三方回调在发请求时不够规范。排查这类问题的标准动作就是用浏览器的开发者工具或抓包工具确认请求头里的 Content-Type再看后端用了哪个注解两者对不上就自然知道问题在哪了。POST 请求的中文乱码也和 Content-Type 有一定关系。Spring Boot 默认字符集是 UTF-8大多数情况下不会乱码如果真出现乱码先检查前端请求头里有没有带上charsetUTF-8以及数据库连接串有没有配置characterEncodingutf8不要一上来就怀疑后端编码配置。实际经验里80% 的乱码问题出现在数据库连接或页面展示层真正需要改后端过滤器的场景很少。5.4 和团队约定的参数规范比写对注解更重要把所有注解用法都掌握之后真正决定开发效率的其实是团队内部的一套参数约定。我在项目里会推动前后端至少确认这几件事查询参数和路径参数的分工哪些资源用 RESTful 路径哪些场景用查询字符串不要同一个接口今天用/api/user?id1明天改成/api/user/1。请求体统一使用 JSON新接口一律用 RequestBody 接收 DTO表单绑定只保留给少量登录、上传等网关场景。日期格式统一推荐使用yyyy-MM-dd HH:mm:ss或 ISO 格式前后端保持一致并在接口文档里写清楚。命名规范统一全部驼峰或者全部下划线尽量别混用。有了这些约定参数接收的问题会少一大半。剩下那部分就是查文档和抓包能解决的事。技术方案从来不是越花哨越好在团队里“大家都看得懂、联调不吵架”才是真正的有效方案。6. 最后补一句我的选型习惯如果你问我现在新写一个 Spring Boot 接口最常用的组合是什么我的回答很简单普通查询用 RequestParam路径资源用 PathVariableJSON 对象一律 RequestBody DTO表单上传用 RequestParam 加 MultipartFile动态配置接口用 JsonNode。其余写法我会控制使用频率特别是无注解 POJO 绑定和 RequestPart 接字符串这种看起来方便但有隐性门槛的写法我一般只在团队约定明确、接口格式稳定的前提下才会用。还有一点个人心得拿到一个参数接收问题永远先确认请求实际长什么样再看后端注解。抓一次包往往比在代码里梭哈半小时更高效。这 11 种方式本身没有优劣之分只有是否匹配场景的区别你把这些边界都想清楚了接口开发里很大一部分联调痛苦自然就会消失。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑