ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SpringBoot接口报错排查实战:404、400、500全解析

SpringBoot接口报错排查实战:404、400、500全解析 前后端联调十个报错里有八个是接口问题而接口问题里404、400、500这三兄弟又占了绝大多数。我这些年帮同事、帮网友排查过太多SpringBoot接口疑难杂症发现大量问题其实翻来覆去就是那几个根源——路径没对上、参数没绑上、服务端没兜住。这篇文章干脆把SpringBoot接口开发里最常见的报错场景做一次系统性梳理从404的路径映射、400的参数绑定到500的服务端异常每类报错我都给出排查思路、常见误区和可直接落地的解决方案。不管你是刚入门写Controller的新手还是在为前后端联调憔悴的老手这套方法应该都能帮你省下不少排查时间。1. 404 报错路径映射问题先分清是谁甩的锅1.1 服务没启动、路径前缀和注解用错最常见的404根因出现404时很多人的第一反应是改代码但我建议先分清楚这个404到底是谁抛出来的。SpringBoot项目自身的404通常是一个白标签错误页页面上写着Whitelabel Error Page, status404如果是nginx返回的页面风格明显不同如果项目前面还有网关响应体里往往是一个JSON里面带着status404。先确认是哪一层抛的比直接埋头查Controller快得多。最常见的404场景是服务压根没起来。前端一调接口直接网络错误console里提示net::ERR_CONNECTION_REFUSED这种情况下后端应用要么进程挂了要么启动失败了。实际开发中有个坑IDE里看起来应用还在运行但你改完代码触发了重启启动过程中端口还没监听前端这时候请求就会失败更离谱的是你改了application.yml里的端口旧进程没杀掉新进程启动报Port already in useIDE还显示在运行中。遇到这种直接看启动日志里有没有Started Application in x seconds或者用命令行curl一下本地的健康检查地址立刻见分晓。第二种是路径前缀问题。SpringBoot里有两个地方容易出404server.servlet.context-path配置和Controller类上的RequestMapping前缀。配置了context-path/api前端请求却漏掉了/api那必然404。还有一种情况是类上写着RequestMapping(/user)方法上写PostMapping(/create)前端请求POST /user/create没问题但如果前端把方法名一起拼进去比如POST /user/createUser那后端没有这个映射404就是板上钉钉的事。第三种是注解用错了。RestController和Controller的区别很多人背得滚瓜烂熟写代码时还是会混。直接用Controller而方法上没有加ResponseBody方法返回字符串时会走视图解析器Spring尝试去找一个同名的HTML模板文件找不到就返回404。我帮人排查过一个接口后端Controller明明有对应方法断点都进不去原因就是方法返回的是String但类上用了Controller导致Spring去解析视图而不是直接返JSON。很多老项目里某些方法忘了加ResponseBody就会出现这种接口明明存在却404的诡异现象。还有一类要特别拎出来请求方法不对并不会返回404而是返回405 Method Not Allowed。前端用了GET去请求一个PostMapping的接口看到的是405而不是404。但很多前端框架对非2xx状态码的处理比较粗暴统一弹一个错误导致前端同学以为又是404。这种信息差浪费了不少联调时间。1.2 五步定位法从完整URL到Handler映射逐段排查遇到404别急着改代码按这个顺序走一遍第一步打开浏览器F12看Network里请求的完整URL。重点看三样HTTP方法、完整路径、请求的域名端口。用Postman或Apifox单独再发一次同样的请求如果工具里能通、浏览器里不通那大概率是跨域、Cookie或代理问题和SpringBoot本身没关系。第二步把URL跟Controller的映射逐段比对。尤其是类级别RequestMapping加上方法级别的组合映射两段拼接时容易出问题。一个容易忽略的点是Spring Boot 2.5之后特别是Spring Boot 3.x里路径匹配策略从AntPathMatcher换成了PathPatternParser如果你项目里用了带正则或通配符的路径行为可能和以前不完全一样。升级版本后接口404的优先查这个。第三步看后端日志。如果请求根本没进Controller日志里通常连一条Mapping信息都没有。Spring Boot默认不会打印所有请求日志需要额外配置AccessLog嫌麻烦的话可以直接在DispatcherServlet上打断点看有没有匹配到HandlerExecutionChain。这个断点能看到请求进来后Spring框架有没有找到对应的handler是定位404的利器。第四步用actuator的mappings端点核对所有已注册的URL。只要引入了spring-boot-starter-actuator再配上management.endpoints.web.exposure.includemappings访问/actuator/mappings就能看到项目里所有Handler映射的完整路径。这个列表是Spring自己维护的直接看它比自己翻代码猜路径准确得多。第五步检查拦截器和过滤器有没有劫持路径。比如全局拦截器里做登录校验路径没放行直接返回了个404响应还有后端做了URL重写或者nginx层规则把前端请求转发到了不存在的服务上。这种404严格来说不是SpringBoot的锅但前后端联调时经常遇到需要两边一起对nginx配置和网关路由。1.3 404排查中的三个真实踩坑案例我踩过几个坑值得单独说说。第一个是https和http混用导致的404。前端页面是https接口请求用的是http浏览器默认会拦截mixed content但有些浏览器console里的报错不显眼接口看起来就像404。这种排查起来特别容易绕弯子后来我养成了习惯一看接口不通先看一眼页面协议和接口协议是否一致。第二个是端口被系统占用实际监听端口和配置文件对不上。用IDEA启动项目时如果8080被别的进程占了SpringBoot启动会失败但某些IDE配置了自动切换端口应用启动后监听了8081前端代码里还写死了8080那所有请求都是404。这个场景尤其在多人共用一台开发机时高发建议前端把接口域名端口收敛到一个配置文件里别写死在代码各处。第三个是纯粹的前端路径写错。前端说接口404我查了半天Controller没问题、路径没冲突最后发现前端请求的是/user/info而后端接口写的是/user/getInfo纯属前端代码笔误。所以排查404一定要先拿完整的请求URL说话而不是拿一句接口404就不停地翻后端代码。2. 400 报错参数绑定失败九成是类型和字段对不上2.1 类型不匹配、字段名不一致、缺参漏参逐类拆解400 Bad Request在SpringBoot里绝大多数是参数绑定阶段出了问题。第一种最常见的是类型不匹配接口声明Integer、Long、BigDecimal前端传过来一个abc字符串Spring做类型转换时抛TypeMismatchException框架直接回400。这属于前端数据格式不符合后端预期但Spring默认的错误页很难看前端只看到400 Bad Request具体哪个参数错了完全靠猜。第二种是RequestBody绑定的JSON和实体类字段对不上。比如前端传{username:张三,age:18}但后端实体类里字段叫name不叫username。这种情况有两个走向如果Jackson配置了FAIL_ON_UNKNOWN_PROPERTIES会直接抛UnrecognizedPropertyException没配置的话就是浅层绑定username被忽略age正常绑上。看起来没报错但业务层拿到的username是null这比400还坑因为表面上是200实际数据不对。第三种是该传的参数没传。RequestParam(value page, required true) Integer page前端没带page参数就会抛MissingServletRequestParameterException直接400。这个错误提示相对明确但很多前端同学对HTTP状态码不敏感以为传个空值就行结果后端解析时又出幺蛾子。第四种是body里的字段类型对不上。比如后端是LocalDateTime前端传的是2024-01-15按默认的Jackson配置转换不了抛HttpMessageNotReadableException也是400。日期时间格式是联调里高频踩坑点后面专门说。2.2 让400错误开口说话全局异常处理统一响应SpringBoot默认的400响应几乎不包含任何调试信息这就是前后端联调最痛苦的地方。前端只看到400 Bad Request后端日志里其实有具体异常但很多同学不知道去哪里看或者日志级别没开关键的绑定异常被淹没了。我的习惯分三步第一步开发环境下把Spring Boot日志里的debug开起来能看到RequestResponseBodyMethodProcessor和HandlerMethodArgumentResolver这些组件打印的参数绑定过程。第二步在全局异常处理器里专门捕获MethodArgumentNotValidException、MethodArgumentTypeMismatchException、HttpMessageNotReadableException、MissingServletRequestParameterException把错误信息包装成统一的JSON返回给前端。前端能立刻看到参数xxx类型不正确期望Integer实际传入abc联调效率直接提升一个档次。第三步在Controller方法参数上把注解和required属性写清楚不要依赖默认值尤其是分页参数、状态参数这些前端容易漏传的。全局异常处理这块我见过不少项目上来就复制网上的代码捕获了一堆异常但没区分业务异常和系统异常导致前端看到的错误信息含糊不清。我建议至少区分三层参数绑定异常400、业务校验异常可自定义BizException、系统异常500。参数绑定异常要把字段名和错误原因一起返回业务异常要带业务语义的错误码系统异常只给一个系统繁忙的统一提示具体堆栈打进日志。2.3 Valid校验失败与枚举、日期格式的隐藏坑还有一个容易被忽略的Valid或Validated参数校验失败同样返回400。比如NotBlank(message 用户名不能为空)前端没传用户名Spring在参数校验阶段抛MethodArgumentNotValidException返回400。这个场景联调中出现频率极高但前端看到的还是那句400 Bad Request如果不做全局异常处理后端只能一条条翻日志。两个隐藏坑值得单独说。第一个是枚举转换接口参数是枚举类型前端传1或ASpring默认不支持字符串直接转枚举对象。需要自定义ConverterFactory或者用JsonCreator配合JsonValue注解处理。如果没做转换配置前端传过来的值没法转换一样是400。我在实际项目里给前端定的规矩是枚举统一用code值传输后端写一个通用的枚举转换工厂一劳永逸。这踏实的规则能避免双方在枚举字段上反复扯皮。第二个坑是全局日期格式配置。SpringBoot默认的Jackson时间格式是ISO格式比如2022-11-20T15:03:22前端如果习惯传yyyy-MM-dd HH:mm:ss几乎必报错。可以这样配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8但要注意这个date-format只对java.util.Date生效对Java 8的时间APILocalDateTime、LocalDate是不起作用的。LocalDateTime需要另外注册Jackson的JavaTimeModule自定义LocalDateTimeSerializer和LocalDateTimeDeserializer。这条如果不处理前后端每次对日期字段都要争论一次属于那种不改就一直疼改了一次以后再也不疼的配置。3. 500 报错服务端异常看堆栈才能救命3.1 空指针、SQL1064、依赖注入与事务五大高频根因500系列报错本质是服务端代码运行期抛了异常。我从接触过的项目里统计高频根因大概有五个空指针、SQL异常、依赖注入问题、事务问题、外部调用异常。空指针是当之无愧的第一名。尤其是从数据库查出实体后没判空直接访问对象的字段或方法。这种问题在异常堆栈里最典型的表现是NullPointerException at com.xxx.service.UserServiceImpl.checkUser。排查方式很直接看堆栈到第几行点进去看哪个对象为null再往前追这个对象是从哪来的。很多同学一看到空指针就心慌其实按照谁调用了这个方法、参数有没有可能是null、代码里哪里取了这个值的思路追一遍大多数五分钟能定位。SQL类异常里MySQL的1064语法错误上榜率极高。这个报错通常带SQL片段和错误位置看错误信息里near xxx基本能定位是表名、字段名写错还是把保留字当成了字段名。还有一个容易被忽略的场景MyBatis的动态SQL拼接出了问题。这里必须再三强调#{}和${}的区别前者是预编译参数占位符后者是字符串直接拼接。很多人把参数写到${}里一旦字段值是字符串单引号拼少了就语法报错这是1064的经典来源。实际遇到报错时把日志里MyBatis打印的Preparing语句复制到数据库客户端里执行一遍语法对不对一眼就能看出来。依赖注入问题也很常见。典型错误是字段报空指针但本质是Bean没注入进来。比如Service注解忘了加、Autowired的类被new出来了、或者一个接口有多个实现类却没用Qualifier指定。Spring启动时如果配置了懒加载这些问题可能不会立刻暴露直到某个请求进来才在运行期炸出来。所以项目里发现一个接口第一次调用就500优先检查这个接口依赖的Bean是否真的被Spring管理了。事务问题也值得单独说。Transactional默认只回滚RuntimeException和Error检查异常比如IOException不会触发回滚。很多人在方法里try-catch之后发现事务没生效数据写到一半后续逻辑失败但没有回滚。两种解法一是把检查异常包一层RuntimeException抛出二是在Transactional上声明rollbackFor Exception.class。我个人更推荐后者因为团队里不是每个人都清楚回滚机制主动声明更保险。3.2 堆栈定位实操流程与Caused by的正确读法500出现的瞬间别急着刷新页面或者反复请求。先把后端控制台或日志文件里的堆栈捞出来这是最直接的路径。第一看异常类型。根据异常包名能快速判断层次org.springframework.dao开头的和数据库相关org.apache.ibatis开头的是MyBatis解析问题java.lang.NullPointerException是业务代码问题java.net.ConnectException是网络调用问题。比如看到MySQLSyntaxErrorException直接往SQL方向走看到ClassCastException往类型转换方向走。第二找Caused by。很多异常是有因果链的最外层的异常往往不具代表性最里面的Caused by才是真正的根因。比如接口报了500最外层是org.springframework.transaction.UnexpectedRollbackException这个看起来和事务有关但往下一层很可能是一个业务Service里的算术异常导致的。只看最外层就写解决方案很容易被误导。第三定位到业务代码层时把出错的那行代码和入参结合起来看。重点看参数传的是什么、从哪来的如果入参本身就是null那问题可能更早是调用方的锅。IDEA里可以直接在异常堆栈行点进去看到具体代码行这时候用Evaluate Expression看看局部变量的值往往立刻就能明白哪里出了岔子。3.3 从500到全局异常处理输出可读的错误响应500对应的是代码抛异常从联调体验来考虑我建议项目初期就做好两件事。第一定义一个全局异常处理类。用RestControllerAdvice加ExceptionHandler把常见异常都捕获转换成统一响应体返回。一个基础版本长这样RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(MethodArgumentNotValidException.class) public RVoid handleValidException(MethodArgumentNotValidException e) { String msg e.getBindingResult().getFieldErrors().stream() .map(f - f.getField() f.getDefaultMessage()) .collect(Collectors.joining(; )); return R.fail(400, msg); } ExceptionHandler(Exception.class) public RVoid handleException(Exception e, HttpServletRequest request) { log.error(request {} error, request.getRequestURI(), e); return R.fail(500, 系统繁忙请稍后重试); } }切记不要把完整的堆栈信息直接返回给前端内部异常细节只进日志响应体里放人类可读的错误提示即可。第二在Service层和Controller层合理打日志。错误级别的日志统一用log.error(操作描述{}, 关键参数, e)这种形式保证堆栈完整打印。日志是排500的第一工具多打一行日志排查时间能省十分钟。我见过太多项目一个Service方法几十行毛都没有一个log一报错就只能靠猜这种代码应该被抓去面壁。4. 前后端联调高频问题跨域、接口契约与序列化4.1 CORS跨域配置的几种正确姿势与常见冲突前后端分离开发时前端跑在8080后端跑在9090前端页面发起请求时如果没做任何跨域配置浏览器会拦截响应控制台报CORS errorNetwork里可能显示blocked by CORS policy。但注意后端其实已经收到请求了只是响应被浏览器拦了所以前端看起来像接口挂了后端日志里却能看到这个请求的访问记录。这个认知差经常导致前后端互相甩锅。SpringBoot里解决跨域的方式很多。最简单的是写一个CorsFilter或者实现WebMvcConfigurer的addCorsMappings方法。但要注意Spring Boot 2.4前后写法有差异allowedOrigins()和allowCredentials(true)不能同时使用否则Spring会直接拒绝配置。因为allowCredentials本身就是允许携带Cookie而代表所有来源两者同时开是有安全矛盾的。要允许所有来源又需要携带凭证得用allowedOriginPatterns(*)。这个坑我见过好多次配置了半天发现还是跨域其实就是这里冲突了。另外如果项目前后端之间还有网关或nginx跨域配置的位置也要想清楚。可以在网关层统一处理也可以通过nginx配置Access-Control-Allow-Origin响应头解决。遵循就近原则哪里离浏览器最近就在哪里处理最合适。别在SpringBoot、网关、nginx三层各配一套配乱了你都不知道是谁在拦截。4.2 RESTful接口规范与统一响应体设计联调报错里很多不是代码问题是接口约定问题。前端和后端对接口应该返回什么的理解不一致。有的后端喜欢返回{code: 0, data: xxx}有的返回{success: true, result: xxx}前端没有统一封装之前每个请求的解析逻辑都不一样联调效率低到令人发指。我强烈建议项目初期把接口契约定死。响应体统一一个顶层结构比如{code, message, data, traceId}成功时code0失败时code非0。HTTP状态码只用来表示传输层语义404表示资源不存在400表示请求参数有问题500表示服务端错误。具体业务错误码通过响应体的code字段下发前端只解析code不根据HTTP状态码猜业务结果。这样设计的意义在于HTTP状态码的种类有限根本表达不了复杂的业务异常而业务错误码可以无限扩展。还有请求路径的规范。RESTful风格下资源用名词复数动作用HTTP方法表达。获取用户列表是GET /users创建用户是POST /users删除用户是DELETE /users/{id}。不要出现GET /getUserList、POST /deleteUser这种动词满天飞的写法。新项目一定要坚持这套规范虽然老项目很难推倒重来但每个团队都应该有意识地往这个方向收敛不然接口路径一多就彻底失控前端光记路径就能记疯。4.3 Long精度丢失、日期格式与Swagger/Knife4j联调提效前后端联调中JSON序列化问题也是高频故障点。第一个是Long类型ID精度丢失。前端JavaScript的Number类型超过2^53后精度会丢失如果后端直接返回Long类型主键前端拿到后可能变成另一个数字。我见过真实案例订单ID的尾部两位直接被抹掉变成0前端拿着截断后的ID去查询详情查出来的东西牛头不对马嘴。解决方案是在Jackson配置里把Long类型序列化为String或者用ToStringSerializer处理ID字段。第二个是BigDecimal精度问题尤其是金额字段。默认Jackson序列化BigDecimal会输出原始精度但如果前后端传参时用了Double接收很容易丢精度。建议后端用BigDecimal接收前端传字符串。这个知识点做支付、订单系统的一定要记牢。第三个是日期格式前面说过LocalDateTime的坑这里不重复。在接口文档这块我建议每个SpringBoot项目接入springdoc-openapiSpring Boot 3或springfoxSpring Boot 2老项目配合Knife4j增强UI。每个接口的路径、入参、响应体结构、参数示例都可视化前端直接照着文档调能大幅减少路径写错字段名写错这类低级404/400。不过文档工具也不是零成本。第一次接入时要注意Knife4j和Spring MVC版本兼容性否则静态资源路径被Servlet容器拦截文档页面可能打不开文档页打开后如果注解里没写清楚字段说明前端照样迷茫所以Controller里该写的Schema(description 用户ID)这些元数据一定别省在网关模式下Knife4j的basePath设置也容易出错需要手动指定服务路径前缀。5. 一套实用的接口报错排查工具链5.1 日志分级与出入口打点让报错可追踪日常接口报错我强烈建议把日志分级用好。开发和测试环境用DEBUG级别生产环境至少INFO错误日志统一用log.error输出。每个接口的入口和出口都打日志入口打印接收到的参数出口打印返回结果和耗时。一旦前端反馈接口报错先看日志里有没有这个URL的入口记录。有说明请求进到了后端问题出在业务逻辑没有说明请求被前一层拦截了或者压根没到后端直接去查网关、nginx和网络。这个入口出口打点的习惯能做到任何一个接口出问题五分钟内能定位到是哪一层。对老项目一时半会儿没法给所有接口补日志的话优先补涉及金额、状态变更的核心接口别等出了事故再后悔。5.2 IDEA断点调试与Apifox/Postman配合技巧日志能解决八成问题剩下两成需要断点。SpringBoot接口调试时很多人喜欢直接在Controller方法上打断点但这里有个盲区参数在进入Controller之前可能已经被Spring转换过了要看原始参数还得往前找。我习惯在Service层和Mapper层都打断点这样能看到参数是怎么一层层传下来的。用IDEA的Evaluate Expression可以临时计算表达式快速判断某个对象是否为null或者实时算一下集合的size。接口测试工具我推荐Apifox或Postman配合环境变量管理能模拟各种复杂场景。但有个残酷的事实工具里测试通过不代表前端浏览器能通过因为存在CORS、Cookie、HTTP缓存这些差异。所以联调时两端都要具备用工具再验证一次的能力这能避免很多我这边明明通了的吵吵。5.3 高频报错速查表整理了一份我日常用得最多的排查表贴出来供参考报错特征大概率原因第一步操作Whitelabel Error Page 404路径不匹配/Controller未映射查看actuator/mappings核对映射404 但后端日志无请求记录请求没到后端/被网关或nginx拦截检查nginx配置和网关路由400 MissingServletRequestParameterException必填参数缺失对照接口文档补参数400 类型转换失败参数类型不一致核对前端传参类型调整后端接收类型400 HttpMessageNotReadableExceptionJSON格式错误/字段类型不对检查请求体JSON结构是否符合实体类500 NullPointerException空指针看堆栈定位null来源500 MySQLSyntaxErrorException 1064SQL语法错误复制日志SQL到数据库客户端执行验错500 NoSuchBeanDefinitionExceptionBean未注入/未扫描检查包扫描路径和AutowiredCORS error跨域配置缺失/allowCredentials冲突配置CorsFilter或网关统一处理Long ID精度丢失序列化Long为Number配置Long转String序列化这张速查表是我自己整理贴在工位上的版本实测下来解决了我日常八成的接口报错排查需求。接口报错这件事本质上是信息差问题请求方不知道服务端的契约服务端不知道请求方的格式。把404、400、500这三类问题的排查思路理顺再配合统一的异常处理、完善的日志打点和一份能对号的速查表联调真的可以少掉很多头发。后面你要是再遇到新的奇葩报错也可以往这张表里继续补自己的排查思路攒成属于你自己的排错手册。
返回列表