ARTICLE DETAIL

资讯详情

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

SpringBoot前后端传参全攻略:从URL到RequestBody的完整链路

SpringBoot前后端传参全攻略:从URL到RequestBody的完整链路 1. 从一次线上联调事故说起传参方式不对接口全废先讲个我真实经历过的场景。前些年做一个前后端分离的项目前端用Vue后端SpringBoot联调的时候前端同学一直报接口500。我看了半天后端日志发现Controller方法签名明明写的是RequestParam String name但前端axios发过来的Content-Type是application/jsonbody里是{name:张三}这种JSON。后端拿不到参数直接报MissingServletRequestParameterException。这种问题在SpringBoot开发里太常见了几乎每个刚接触前后端分离的开发者都会踩一遍。本质上就是一句话前端用什么姿势把数据发过来后端就要用什么姿势去接。姿势不匹配轻则参数为null重则直接400、415、500。这篇文章我打算把SpringBoot前后端传参这件事从根上捋一遍涵盖传统MVC页面的表单传参、前后端分离下的JSON交互、文件上传、嵌套对象、日期格式处理以及联调时最容易翻车的几个排查细节。内容会尽量做到可直接抄作业每一步都配上真实可用的代码和请求示例而不是只贴个官方文档片段。适合谁看刚学SpringBoot的初学者、正在做前后端分离项目开发的同学以及被联调传参问题折腾过但一直没系统梳理过的人。看完之后你至少能解决80%以上的传参报错并且知道问题出在哪一层、该往哪里查。2. 先把传参这件事拆到最底层HTTP请求的三个信息位很多同学一上来就背注解RequestParam、PathVariable、RequestBody背得滚瓜烂熟但一遇到实际问题就懵因为不理解这些注解背后对应的是HTTP协议里的什么结构。其实所有传参方式最后都可以拆成三件事请求行里的URL、请求头里的Content-Type、请求体里的数据格式。2.1 URL上的参数Query String和路径占位符先说URL。HTTP请求的URL可以携带两种参数一种叫Query String查询字符串另一种是路径占位符RESTful风格。Query String就是URL里?后面的部分比如GET /api/user?name张三age25这种参数的特点是通过GET请求传递明文写在URL上适合传简单的查询条件。也不是说POST就不能带Query String完全可以但一般没人这么干因为POST的body才是主角。路径占位符则是把参数直接嵌在URL路径里比如GET /api/user/1001这里的1001是用户的ID。这种风格就是RESTful API常用的语义更清晰一个URL就能表达访问ID为1001的用户资源。对于后端来说对应关系是这样的Query String用RequestParam接收路径占位符用PathVariable接收这两者我都建议在后端代码里显式指定参数名不要省略。比如GetMapping(/api/user) public Result getUser(RequestParam(name) String name) { // ... }为什么显式指定因为如果你不写(name)SpringBoot是依赖编译期的参数名元数据来推断的。在有些构建配置下编译后参数名会变成arg0、arg1这种导致运行时直接报找不到参数。这是我在实际项目中踩过的坑项目用的Java版本和构建工具的组合不同表现还不一样玄学得很。所以显式声明参数名是最稳的做法。2.2 Content-Type告诉后端我这包数据是什么格式Content-Type这个请求头很多人忽略它但恰恰是它决定了SpringBoot用哪个参数解析器来处理你的请求体。最常见的几种Content-TypeContent-Type含义SpringBoot对应的接收注解application/x-www-form-urlencoded表单键值对格式是name张三age25RequestParam / 普通对象绑定application/jsonJSON字符串RequestBodymultipart/form-data文件上传或者文件字段混合MultipartFile / 对象绑定text/plain纯文本少见RequestBody String看到没有Content-Type不同后端接参数的注解就不一样。你前端用axios发JSONContent-Type自动就是application/json后端如果用RequestParam去接必然拿不到数据。反过来前端用表单格式提交后端用RequestBody接也会报HttpMediaTypeNotSupportedException。再说深一层。SpringBoot的DispatcherServlet在处理请求时会根据请求头里的Content-Type从HandlerMethodArgumentResolver列表里选出一个合适的解析器。这是一套策略模式每种注解都有对应的解析器RequestParam对应RequestParamMethodArgumentResolverRequestBody对应RequestResponseBodyMethodProcessor普通对象没有注解对应ServletModelAttributeMethodProcessor我刚开始学SpringBoot的时候以为这些注解只是拿来用后来才明白搞清楚它们的底层解析器遇到问题才能一眼定位。比如说你发现某个参数解析器没生效那就去看是不是Content-Type不对或者是不是缺少了某个依赖。2.3 Body里的数据格式键值对、JSON、还是多部分混合最后是请求体本身。同样是一个body格式完全不同表单格式是URL编码的键值对字符串JSON是一个结构化的文本multipart则是二进制的分段数据。我这几年联调经验里最深的体会是前后端分离项目里90%的业务接口都应该用JSON交互不要去想着用表单格式传复杂对象。为什么因为JSON天然支持嵌套结构可以表达复杂的数据关系而且和前端JavaScript对象的序列化天然契合——你在前端写一个对象JSON.stringify一下就发出去了后端RequestBody接收再反序列化成Java对象整个过程非常自然。但如果你的接口涉及文件上传那情况就特殊了文件本身是二进制没法直接塞进JSON里这时候才需要multipart/form-data。关于文件上传我后面单独讲。到这里我想先把基础认知补全。一个HTTP请求到SpringBoot后端要经过网络传输 - Servlet容器 - DispatcherServlet - HandlerMethodArgumentResolver - Controller方法参数这么一条链路。传参的每一个环节出错都可能表现为参数拿不到或参数类型转换失败但根因可能藏在任何一个位置。3. 常用注解逐个拆解各自的适用场景和命门接下来是正菜。我把SpringBoot里和传参相关的常用注解全部过一遍每个注解说清楚三件事它是什么、什么时候用、最容易踩什么坑。3.1 RequestParamQuery String和表单字段的接盘侠RequestParam是使用频率最高的一个注解用于接收Query String参数和application/x-www-form-urlencoded表单字段。基本用法PostMapping(/api/login) public Result login(RequestParam(username) String username, RequestParam(value remember, required false, defaultValue false) boolean remember) { // ... }这里有几个属性值得说明一下value参数名前端传的参数必须叫这个名required是否必传默认是true意思就是前端不传这个参数直接报400defaultValue默认值如果前端没传就用这个值填充这三个属性是联调时最常出问题的。前端少传一个参数后端报MissingServletRequestParameterException前端传了但类型对不上报MethodArgumentTypeMismatchException。这类错误一看日志就明白没什么技术含量但累积起来就很烦人。命门RequestParam接不了JSON对象。前端发{username:张三}后端的RequestParam String username是拿不到的因为JSON不在这条解析链路上。3.2 PathVariableRESTful路径里的参数提取器PathVariable和RequestParam长得像但语义完全不同。它接收的是URL路径里的占位符是最RESTful的一种参数传递方式。GetMapping(/api/user/{id}) public Result getUser(PathVariable(id) Long id) { // ... }对应请求GET /api/user/1001这种方式的优点是URL语义清晰、对搜索引擎友好、参数不会出现在Query String里。缺点是不适合传太多参数一般只用于定位资源的ID或名称。你不可能把整个查询条件都塞进路径里。命门路径占位符的名字必须和{}里的名字一致否则SpringBoot在启动时甚至直接报错。另外路径变量默认不能包含/如果你想传带斜杠的值比如文件路径得用{id:.}这种正则变体但绝大多数场景用不上知道有这回事就行。3.3 RequestBodyJSON交互的核心入口如果你的项目是前后端分离RequestBody应该是你用得最多的注解。它负责把请求体里的JSON字符串反序列化成Java对象底层用的是Jackson这个库SpringBoot默认自带。PostMapping(/api/user) public Result createUser(RequestBody UserDTO user) { // user里已经有数据库落库需要的数据了 }这个注解强在哪它可以直接接一个复杂的嵌套对象。比如前端发来{ username: 张三, age: 25, address: { province: 浙江省, city: 杭州市 } }后端只要定义好对应的DTO结构Jackson会自动完成映射public class UserDTO { private String username; private Integer age; private AddressDTO address; // getters and setters 或 Lombok Data } public class AddressDTO { private String province; private String city; }命门前端字段名和Java属性名不一致时反序列化会得到null。比如前端传userName后端属性写username两边对不上。解决办法有两种一种是在Java里加JsonProperty(userName)注解显式映射另一种是约定前后端都用驼峰命名。我个人强烈推荐第二种——靠约定不要靠注解一个个去映射否则每个字段都要写注解代码没法看。另外一个巨坑是日期格式。前端传2025-06-01 10:30:00后端用LocalDateTime接默认情况下Jackson是不认这个格式的它只认ISO格式的2025-06-01T10:30:00。不处理的话反序列化直接报错。解决方案是在application.yml里做全局配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8注意这个配置对java.util.Date生效对Java 8的LocalDateTime不一定生效后者需要在字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)。这里有个时间兼容性的坑后面实战部分我会细说。3.4 无注解参数绑定适合表单提交场景还有一种写法Controller方法参数不加任何注解直接写一个对象PostMapping(/api/login) public Result login(UserLoginForm form) { // form.getUsername() ... }这种写法依赖SpringMVC的ServletModelAttributeMethodProcessor它会自动从Query String和表单字段里把参数匹配到对象的属性上。字段名对上就赋值对不上就保持null。适用场景传统MVC模式下的表单提交。比如你用Thymeleaf渲染页面或者用普通的HTML表单form action/api/login methodpost浏览器提交的Content-Type是application/x-www-form-urlencoded用这种方式接收代码最简洁。命门它接不了JSON。前端如果发JSON这个方式一样拿不到数据。而且当方法里同时存在RequestBody和无注解对象时SpringBoot会分别处理不会互相干扰但如果前端混着Content-Type发结果可能很随机建议一个Controller方法里统一用一种接收方式。3.5 RequestHeader和CookieValue别忽略请求头和Cookie里的参数这两个注解属于小众但偶尔保命的类型。场景是这样的你的系统需要从Header里拿token做鉴权或者从Cookie里拿一个用户标识。GetMapping(/api/profile) public Result getProfile(RequestHeader(X-Token) String token) { // 解析token返回用户信息 }GetMapping(/api/theme) public Result getTheme(CookieValue(value theme, defaultValue light) String theme) { // ... }这两个注解的道理和RequestParam一样也是从请求的不同位置取参数。唯一需要注意的是Header里的参数名大小写不敏感HTTP协议规定Header名不区分大小写SpringBoot底层也是忽略大小写匹配的。4. 前后端分离场景下的JSON交互实战从DTO设计到联调这段是重头戏。前后端分离已经成了当前Web开发的标配我围绕这个场景讲一遍完整的JSON交互流程包括DTO怎么设计、前端axios怎么发出请求、后端怎么接收校验、常见的报错怎么排查。4.1 为什么DTO是必选项而不是可选项很多刚入门的同学喜欢直接把Entity丢在Controller方法参数里比如PostMapping(/api/user) public Result save(RequestBody User user) { // ... }看起来省事但这个写法在真实项目里是灾难。为什么第一Entity对应的是数据库表结构它可能包含createTime、updateTime、deleted这些字段这些字段根本不该由前端传。如果前端不小心传了deleted: true你就在不知情的情况下把逻辑删除了你慌不慌第二Entity的结构通常比前端需要的数据多。前端只需要username和password你让前端传整个User对象多传的字段会不会被恶意利用比如有个role字段前端传个role: admin你用什么策略去阻止所以正确的做法是接收用DTO落库前转换为Entity。DTO的精髓是切面——前端需要什么字段、允许传什么字段DTO就定义什么字段一个多余的字段都别有。Data public class UserCreateDTO { NotBlank(message 用户名不能为空) private String username; NotBlank(message 密码不能为空) Size(min 6, message 密码长度不能少于6位) private String password; }4.2 前端发出正确的JSON请求axios示例前端那边用axios发请求的正确姿势是这样的import axios from axios axios.post(/api/user, { username: 张三, password: 123456 }).then(response { console.log(response.data) })这里有一个容易被忽略的细节如果你把对象作为data直接传进去axios会自动把对象序列化成JSON并把Content-Type设置为application/json这是标准行为。所以你不用手动做任何设置正常的JSON对象传参就是走RequestBody。但有些人习惯这么写axios.post(/api/user, { data: { username: 张三, } })这是错的axios会在请求体里多包一层data后端RequestBody UserCreateDTO user收到的user对象整个是null——不是某个字段为null是整个对象为null。这个坑我见过不下三次每次都是前端同学和后端同学对着日志看半天。为什么因为axios的常见配置是axios.post(url, data)的第二参数才是请求体不存在什么config.data的写法除非你自己封装了请求函数。正确的封装可以简化成function post(url, data) { return axios.post(url, data) }后端对应的ControllerPostMapping(/api/user) public Result createUser(Validated RequestBody UserCreateDTO dto) { User user new User(); BeanUtils.copyProperties(dto, user); // userService.save(user); return Result.success(); }4.3 参数校验Validated不是摆设前面代码里出现了Validated这个注解配合DTO里的NotBlank、Size等校验注解可以在进入业务逻辑之前就把不合法的参数拦截下来。PostMapping(/api/user) public Result createUser(Validated RequestBody UserCreateDTO dto, BindingResult result) { if (result.hasErrors()) { return Result.fail(result.getFieldError().getDefaultMessage()); } // ... }这里有个细节要提醒BindingResult必须紧跟在被校验的参数后面中间不能有其他参数否则校验结果不会自动注入你会拿不到错误信息。如果你不想在方法里写这一堆判断可以用全局异常处理器RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(MethodArgumentNotValidException.class) public Result handleValidationException(MethodArgumentNotValidException ex) { String message ex.getBindingResult().getFieldError().getDefaultMessage(); return Result.fail(message); } }个人经验是优先用全局异常处理器Controller方法保持干净只写业务逻辑。否则每个方法都要复制一遍校验代码维护起来想哭。4.4 联调常见的四种报错与排查路径联调阶段最怕的就是前端说接口报错了然后丢一个Network截图过来。我总结了四种高频报错每种对应不同的排查方向。400 Bad Request Required request body is missing后端用了RequestBody但前端没发请求体或者请求体是空的。排查方向看Network面板里Request Payload是不是空的如果是GET请求就别用RequestBodyGET根本不该有body。400 Bad Request JSON parse error请求体有内容但JSON格式不对或者字段类型对不上。比如前端传了age: 25岁后端Integer age接不了。排查方向先在浏览器里手动执行一次接口用Postman或Apifox看是前端序列化问题还是后端类型定义问题。415 Unsupported Media TypeContent-Type不对。最常见的是前端自己手动设置了Content-Type比如设成application/x-www-form-urlencoded但body里发的又是JSON字符串。排查方向打开Network面板看Request Headers里的Content-Type到底写了什么。500 Internal Server Error排除了前面的格式问题那就是后端业务逻辑报错了。看后端日志重点看异常堆栈的前几行一般就是空指针或者数据库报错。有时候是因为RequestBody反序列化成功了但DTO里的字段为null进业务层就炸了。这些报错看着多其实核心就一句话先确认三个信息位是否匹配——URL上的参数有没有传到、Content-Type和body格式是否匹配、后端注解是否和前端发送姿势对得上。只要你养成这个排查习惯联调效率至少翻一倍。5. 文件上传与多字段混合提交一个容易卡壳的特殊场景聊完JSON再说说文件上传。这个场景容易卡壳主要是因为文件是二进制不能简单地塞进JSON里需要走multipart/form-data。5.1 multipart/form-data的典型姿态前端用FormData对象const formData new FormData() formData.append(file, fileInput.files[0]) formData.append(description, 这是一段描述文字) formData.append(type, avatar) axios.post(/api/upload, formData) // axios检测到传的是FormData会自动设置Content-Type为multipart/form-data这里有个小知识如果你手动设置Content-Type: multipart/form-data但忘记设置boundary浏览器会自动给你加上不过手动设置后有些环境会丢失boundary导致后端解析失败。最佳做法是不手动设让axios帮你处理。后端对应接收PostMapping(/api/upload) public Result upload(RequestParam(file) MultipartFile file, RequestParam(description) String description, RequestParam(type) String type) { // 保存文件逻辑 return Result.success(); }没错文件上传时其他普通字段用的是RequestParam接收而不是RequestBody。这个和其他场景不一样别搞混了。5.2 MultipartFile的三个高频操作校验、转存、回显URLMultipartFile接口提供了一些常用方法PostMapping(/api/upload) public Result upload(RequestParam(file) MultipartFile file) throws IOException { // 1. 校验是否为空 if (file.isEmpty()) { return Result.fail(文件不能为空); } // 2. 校验文件大小 if (file.getSize() 2 * 1024 * 1024) { return Result.fail(文件大小不能超过2MB); } // 3. 校验文件类型 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); if (!Arrays.asList(.jpg, .png, .gif).contains(ext)) { return Result.fail(不支持的文件类型); } // 4. 保存到本地 String fileName UUID.randomUUID() ext; File dest new File(/data/upload/ fileName); file.transferTo(dest); return Result.success(/files/ fileName); }实操中要注意的几个点文件名不能直接用用户传的。原因是安全问题——用户可以传../etc/passwd这种路径如果你直接用file.getOriginalFilename()去拼接路径就可能造成目录穿越漏洞。虽然现在多数系统都有防护但养成良好的习惯总没错。我一般是UUID生成新文件名扩展名从原文件里提取。文件大小限制要在配置里显式设置。SpringBoot默认的单个文件大小上限是1MB你没注意的话传大一点的文件直接报FileSizeLimitExceededException。配置如下spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MBmax-file-size控制单个文件上限max-request-size控制单次请求的总大小上限包括所有文件和其他字段。文件保存路径要提前规划。建议用绝对路径不要用相对路径。相对路径的基准目录取决于你启动服务时的工作目录换了启动方式路径就变了很坑。我当时就是没注意本地IDE启动正常部署到服务器跑起来文件路径全错了排查了半天才发现是工作目录不一致导致的。5.3 文件JSON混合尽量别这么干有些场景比如你需要上传文件的同时传一个JSON对象有同学会尝试把JSON对象转成字符串塞进表单的某个字段里formData.append(fileInfo, JSON.stringify({ name: 张三, age: 25 }))后端RequestParam(fileInfo) String fileInfoStr然后手动把JSON字符串转成对象ObjectMapper mapper new ObjectMapper(); UserInfo info mapper.readValue(fileInfoStr, UserInfo.class);这种做法能用但不推荐。因为一旦JSON里包含复杂嵌套或集合手动序列化反序列化容易出错而且可读性差。更好的方案是把文件上传和业务数据分开先上传文件拿URL再把URL和其他字段放进业务DTO提交。或者说如果你确实需要一次提交可以在DTO里定义一个MultipartFile字段和一个JSON字符串字段然后用前面说的无注解对象绑定方式接收。但那个对象绑定方式对multipart的支持又会有一些注意点不建议新手一上来就这么干。我的建议是文件走文件上传接口业务数据走JSON接口分两步。虽然请求变多了但边界清晰排查问题也容易。6. 传参过程中的几个高频隐性杀手编码、日期和大小写前面讲的都是看得见的传参问题下面这几种坑属于隐性杀手——你不知道它的存在但一旦触发排查起来极其痛苦。6.1 中文乱码源头在容器编码中文乱码这个问题的表现是后端拿到参数后打印出来是???或者乱码。很多人第一反应是数据库编码问题其实在前后端传参阶段就可能已经乱了。排查顺序是这样的第一步确认浏览器发出的请求里URL或body是UTF-8编码。前端一般是默认UTF-8但如果你在页面上手动拼接URL浏览器会按页面编码去编码URL如果页面是GBKURL里的中文就是GBK编码后端按UTF-8解码必然乱码。第二步确认SpringBoot的字符编码过滤器生效。SpringBoot已经默认配置了CharacterEncodingFilter强制UTF-8这层一般问题不大。第三步确认Tomcat对URL的编码。这层容易被忽略。Tomcat的URIEncoding默认在SpringBoot内嵌模式下就是UTF-8但如果你改过配置或者部署到外部Tomcat环境可能就不是了。外部Tomcat需要在server.xml里设置Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /命门POST请求body里的中文乱码和URL里的中文乱码是两个不同的编码环节。body的编码走CharacterEncodingFilterURL的编码走容器的URIEncoding别搞混。如果前端表单提交中文正常但URL参数乱码大概率是容器URIEncoding的问题。6.2 日期参数三种注解三种脾气日期格式是一个特别容易出错的点因为它在不同注解下的处理逻辑完全不同。场景一Query String里传日期GetMapping(/api/order) public Result getOrders(RequestParam(startDate) LocalDate startDate) { // ... }请求URLGET /api/order?startDate2025-06-01这时候SpringBoot会尝试把字符串2025-06-01转成LocalDate默认支持的格式是ISO格式yyyy-MM-dd。如果你传2025/06/01或20250601就会报MethodArgumentTypeMismatchException。如果你想自定义支持的格式在DateTimeFormat注解里指定GetMapping(/api/order) public Result getOrders(RequestParam(startDate) DateTimeFormat(pattern yyyy/MM/dd) LocalDate startDate) { // ... }场景二JSON body里传日期前面说过JSON里传日期走的是Jackson的序列化反序列化逻辑。RequestBody里接收LocalDateTime需要字段上加JsonFormatpublic class OrderQueryDTO { JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime startTime; }这里有个细节timezone GMT8很重要。如果不指定Jackson会使用服务器默认时区。如果你部署的服务器时区是UTC那时间会少8个小时数据看起来就像穿越了。我实际遇过一次开发环境正常测试环境部署到海外的服务器上写入数据库的时间全部比实际少了8小时排查半天才发现是时区问题。场景三表单格式传日期表单格式和Query String的逻辑类似也是走DateTimeFormat。但如果是无注解对象绑定同样也是DateTimeFormat生效。所以你可以理解为Query String和表单格式归DateTimeFormat管JSON归JsonFormat管两条线互不干扰。6.3 大小写和命名风格一个前端字段引发的血案这是个纯实践问题没有技术含量但破坏力很大。场景是这样的前端代码里变量名习惯用驼峰比如userName。后端的Java属性也是驼峰userName。两边看着一致对上了。但数据库字段是下划线风格比如user_name。如果你的MyBatis配置里开启了map-underscore-to-camel-case: true那从数据库查出来再映射到Java对象字段也是对的。问题出在哪出现在有的前端同学不注意字段名大小写比如username和userName一个是全小写一个是驼峰。后端DTO属性是userNameusername传进来就是null。这属于纯粹的约定问题只能通过接口文档来约束。我建议的前后端字段约定是接口文档里格式统一用小驼峰比如userName、phoneNumber后端DTO属性用同样的命名数据库字段用下划线通过MyBatis配置自动映射这样一套约定下来从前端到后端再到数据库中间没有任何手工映射出错概率最低。7. 实战排查链路一次真实的传参定位全过程这节我完整走一遍排查链路。场景是前端点击注册按钮后端接口报500日志显示Required request body is missing。7.1 复现现象与日志初判前端同学描述点了注册按钮页面报服务器内部错误看我这边日志。我打开日志看到这样的错误org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public com.example.Result com.example.controller.UserController.register(org.springframework.web.bind.annotation.RequestBody com.example.dto.UserRegisterDTO)这个异常信息其实已经说得非常明白了RequestBody注解标注的方法参数找不到请求体。7.2 从Network面板反向推导前端代码我先不急着看后端代码而是打开浏览器的Network面板复现一次请求看三个信息位Request MethodPOST正确Content-Typeapplication/x-www-form-urlencodedRequest Payloadusername张三password123456看到Content-Type就明白了前端把请求发成了表单格式但后端的RequestBody只认JSON格式。两个注解不对应。为什么会这样大概率是前端同学用了类似jQuery的$.ajax或者原生XMLHttpRequest并且没有显式设置Content-Type。XMLHttpRequest的默认Content-Type就是application/x-www-form-urlencoded;charsetUTF-8而axios会自动根据data类型选择Content-Type。7.3 修复与验证让前端把代码改成axios或者fetch确保请求体是JSON字符串、Content-Type是application/json。如果是axios最常见的问题代码是axios.post(/api/register, username张三password123456)这样第二个参数传了字符串axios就只能当字符串发Content-Type是text/plain。改成传对象axios.post(/api/register, { username: 张三, password: 123456 })后端Controller不用动因为RequestBody等着就是JSON。改完后重新请求观察NetworkContent-Type变成application/jsonRequest Payload变成{username:张三,password:123456}后端日志不再报错接口正常返回。这个问题从头到尾大概花了20分钟排查其中15分钟在确认前端发的是什么。7.4 这套排查思路的通用版本从这个案例我提炼出一个通用排查模板遇到任何传参问题都可以按这三步走第一步看请求。浏览器Network面板里看Request Method、Request URL、Query String Parameters、Request Headers、Request Payload这五项。这一步能确定前端实际上发了什么。第二步看注解。Controller方法上的注解是什么是RequestParam还是RequestBody参数类型是什么和第一步看到的内容是否匹配第三步看日志。异常栈顶层的异常类和异常消息。SpringBoot的异常消息基本都能直接告诉你缺什么比如MissingServletRequestParameterException说明缺参数HttpMessageNotReadableException说明body缺失或格式不对。排错的核心是不要凭猜先看事实。前端说什么、后端日志说什么都可能有偏差但Network面板和异常堆栈是客观的。8. 进阶场景集合参数、数组参数和复杂嵌套对象基础用法讲完了再说几个进阶场景。这些场景在业务里不常见但遇到的时候如果没有储备会浪费很多时间。8.1 前端传多个ID数组和List的接收场景批量删除用户前端传一组ID。方式一Query String RequestParam Listaxios.get(/api/user/batch, { params: { ids: [1001, 1002, 1003] } })后端GetMapping(/api/user/batch) public Result batchGet(RequestParam(ids) ListLong ids) { // ... }注意axios会将数组序列化为ids[]1001ids[]1002ids[]1003这一段还是有点细节。有的后端框架能接有的接不到带[]的参数名。为了避免这个坑可以在axios里加一个参数序列化配置或者干脆用POST JSON的方式传数组。方式二POST JSON传数组axios.post(/api/user/batch/delete, [1001, 1002, 1003])后端PostMapping(/api/user/batch/delete) public Result batchDelete(RequestBody ListLong ids) { // ... }这种方式最干净推荐。但如果后期需要附带其他参数比如操作人ID就需要包一层对象axios.post(/api/user/batch/delete, { operatorId: 888, ids: [1001, 1002, 1003] })后端PostMapping(/api/user/batch/delete) public Result batchDelete(RequestBody BatchDeleteDTO dto) { // ... }8.2 前端传Map动态字段的兜底方案有些接口的字段是动态的比如表单设计器里用户的扩展属性这时候用固定的DTO就不好使了。兜底方案是用Map直接接收PostMapping(/api/dynamic) public Result dynamicFields(RequestBody MapString, Object params) { String name (String) params.get(name); Integer age (Integer) params.get(age); // ... }前端传什么键值对后端都能收。但Map方案是有代价的你失去了类型安全和编译期检查字段名拼错了也不会在编译期暴露只是运行时拿到null。所以我的建议是优先级永远是DTO Map只有在一个接口需要支持高度动态字段时才用Map。8.3 复杂嵌套对象集合里有对象对象里有集合现在大多数业务接口的数据结构都不再是扁平的嵌套对象几乎成了家常便饭。比如一个订单接口{ orderNo: NO20250601001, customer: { name: 张三, phone: 13800138000 }, items: [ { skuId: S001, quantity: 2, price: 199.99 }, { skuId: S002, quantity: 1, price: 99.00 } ] }后端DTOData public class OrderCreateDTO { private String orderNo; private CustomerDTO customer; private ListOrderItemDTO items; } Data public class CustomerDTO { private String name; private String phone; } Data public class OrderItemDTO { private String skuId; private Integer quantity; private BigDecimal price; }Jackson处理这种嵌套结构和Java泛型是完全透明的只要你把DTO结构定义对反序列化会自动完成。命门嵌套对象里如果出现空缺字段——比如customer传了nullitems传了空数组后端逻辑能不能撑住建议在服务层对所有嵌套对象做null判断或者用Optional、断言等方式提前校验别等到取值的时候才报空指针。8.4 多个RequestBody抱歉一个方法只能有一个有的同学会试图一个方法里放两个RequestBody参数比如PostMapping(/api/test) public Result test(RequestBody UserDTO user, RequestBody AddressDTO address) { // ... }这是不行的。一个HTTP请求只可能有一个bodySpringBoot也只允许方法里存在一个RequestBody参数多个会直接报错。如果你确实需要传两组数据就合并成一个DTOpublic class UserWithAddressDTO { private UserDTO user; private AddressDTO address; }这样前端传的JSON就是{ user: { ... }, address: { ... } }接的时候各取所需就行。9. 传参之外的最后一道防线全局配置与统一API规范聊到最后我想把视野拉高一点。传参本身已经讲得差不多了但如果项目里传参习惯混乱今天这个人用Query String传用户信息明天那个人用JSON传表单数据接口文档写得稀烂联调效率会很低。所以最后讲一下全局配置和统一规范的事。9.1 在配置层提前规避问题回到最开头说的Jackson配置这是我在所有SpringBoot项目里的标配spring: jackson: # 日期格式化主要影响java.util.Date date-format: yyyy-MM-dd HH:mm:ss # 时区 time-zone: GMT8 # 忽略无法识别的字段避免前端多传字段导致报错 deserialization: fail-on-unknown-properties: false最后这个fail-on-unknown-properties: false很关键。它允许前端多传字段而不报错。默认Jackson遇到未知字段会抛UnrecognizedPropertyException这在开发阶段能帮你发现DTO定义遗漏但上线后却可能因为前端遗留字段导致整个接口500。我建议开发阶段保持默认报错上线前关掉这样两边兼容。9.2 接口文档是传参协作的地基说实话前后端联调里大量传参问题根源不是技术而是没有接口文档。前后端对这个字段该叫啥、该传啥格式的理解不一致就会出现各种诡异的传参null、报错。我的经验是项目一开始就引入OpenAPISwagger文档或者在代码里用javadoc式的注释把每个接口的请求示例写清楚。SpringBoot集成Swagger不算复杂关键是坚持。接口多了以后没有文档的维护成本比写文档的成本高得多。不过要注意一点Swagger生成出来的文档你最好自己也看看它不会替你安排命名是否合理。真正有用的接口文档应当是前端照着写请求就能通的程度。如果它照着写请求还是报错那这个问题就值得记录下来回头复盘。9.3 传参规范清单根据我这些年踩过的坑整理一份传参规范清单新项目能直接照着执行普通业务接口统一用POST JSON RequestBody不要混用查询接口统一用GET Query StringRequestParam参数简单直接资源定位用RESTful路径PathVariable但只放ID或唯一标识文件上传统一用multipart/form-data其他字段用RequestParam日期格式统一为yyyy-MM-dd HH:mm:ssJSON里用JsonFormat标注字段命名统一小驼峰前端后端一个标准任何接口接收参数一律DTO禁止直接Entity这几条规范看起来简单但每条背后都是真实项目的教训。遵守了你的前后端联调效率至少提升30%。总结一下我个人的体会传参这件事表面上是一堆注解和格式的排列组合本质上是前端数据和后端数据模型的映射契约。这个契约维护得好项目开发顺风顺水维护得不好每天的工作就是在排查为什么参数是null。我建议每一个开发者都花一个下午把自己负责的系统里的所有接口过一遍看看传参方式是否统一、是否有冗余DTO、是否有不规范的写法——这个过程的价值不亚于学会一个新框架。
返回列表