资讯详情

接口报错全返回 500?Spring Boot 全局异常处理与统一响应体实战(面试常问)

📅 2026/10/9 7:56:42 | 华诺云谱 👁 阅读
接口报错全返回 500?Spring Boot 全局异常处理与统一响应体实战(面试常问)
一、起因前端同学的一句话我的物联网设备接入平台Spring Boot 3.5 EMQX有一个看板前端数据全靠 REST 接口喂。有天自己扮演前端联调故意乱发请求结果发现一个尴尬的现象用 GET 去访问一个只支持 POST 的接口 → 返回500 服务器内部错误POST 的 body 写成非法 JSON → 也是500 服务器内部错误。这两种明明都是调用方的错却报成了服务器内部错误。调用方看到 500 会以为我的服务挂了实际上是他的请求姿势不对。更糟的是服务端日志还会记一整条 ERROR 堆栈——客户端的错把真正的服务端故障日志淹掉了。这篇文章就讲我这套接口的错误处理是怎么搭的统一响应体 全局异常处理器以及这次实测揪出来的两个兜底坑。所有代码都来自项目真实源码所有响应都是实测结果。二、先立规矩统一响应体第一件事是让所有接口返回同一种结构。项目里的ApiResponse// 来源src/main/java/com/iothub/common/ApiResponse.java/** * 统一响应体。所有接口都返回这个结构前端只需要判断 code。 * * 面试考点为什么要统一响应体 * 答散装 JSON 每个接口结构不同前端没法统一处理约定 code/message/data * 三段式后拦截器、错误处理、前端封装都能按同一套逻辑走。 */publicrecordApiResponseT(intcode,Stringmessage,Tdata){publicstaticTApiResponseTok(Tdata){returnnewApiResponse(0,success,data);}publicstaticTApiResponseTerror(intcode,Stringmessage){returnnewApiResponse(code,message,null);}}三段式code为 0 表示成功非 0 就是错误码data只装业务数据。浏览器里直接访问概览接口看到的真实返回{code:0,message:success,data:[{id:1,deviceName:temp-sensor-01,productKey:productA,status:offline,lastReceivedAt:null,metrics:{}}]}一个 record 三行就写完了——Java 17 之后这种纯数据载体用 record 比 class 省事得多这也是个面试小加分点。三、业务代码只管抛BusinessException有了统一结构业务层的错误怎么变成这个结构答案是自定义异常// 来源src/main/java/com/iothub/common/BusinessException.java/** * 业务异常service 层抛出携带 HTTP 状态码由全局异常处理器统一转成响应体。 * 好处业务代码只管抛controller 不用到处 try-catch。 */GetterpublicclassBusinessExceptionextendsRuntimeException{privatefinalintstatus;publicBusinessException(intstatus,Stringmessage){super(message);this.statusstatus;}}service 层用起来很干净比如设备注册时的重名检查真实代码// 来源src/main/java/com/iothub/device/DeviceService.java节选publicRegisterResultregister(RegisterDeviceRequestrequest){if(deviceRepository.existsByProductKeyAndDeviceName(request.getProductKey(),request.getDeviceName())){thrownewBusinessException(409,同名设备已存在);}// ... 正常注册流程}注意两点RuntimeException而不是受检异常业务代码不用被 try-catch 包住异常自带 HTTP 状态码409 冲突谁抛谁定语义。四、RestControllerAdvice把所有异常收进一张网最后一块拼图是全局异常处理器负责把 controller 抛出来的所有异常统一转成ApiResponse// 来源src/main/java/com/iothub/common/GlobalExceptionHandler.javaSlf4jRestControllerAdvicepublicclassGlobalExceptionHandler{ExceptionHandler(MethodArgumentNotValidException.class)publicResponseEntityApiResponseVoidhandleValidation(MethodArgumentNotValidExceptione){Stringmsge.getBindingResult().getFieldErrors().stream().map(f-f.getField(): f.getDefaultMessage()).findFirst().orElse(参数错误);returnResponseEntity.badRequest().body(ApiResponse.error(400,msg));}ExceptionHandler(BusinessException.class)publicResponseEntityApiResponseVoidhandleBusiness(BusinessExceptione){returnResponseEntity.status(e.getStatus()).body(ApiResponse.error(e.getStatus(),e.getMessage()));}ExceptionHandler(NoResourceFoundException.class)publicResponseEntityApiResponseVoidhandleNoResource(NoResourceFoundExceptione){log.debug(静态资源不存在: {},e.getResourcePath());returnResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse.error(404,资源不存在));}ExceptionHandler(Exception.class)publicResponseEntityApiResponseVoidhandleUnknown(Exceptione){log.error(未处理异常,e);returnResponseEntity.internalServerError().body(ApiResponse.error(500,服务器内部错误));}}源码里还有一个IllegalArgumentException的处理器篇幅原因略。几个设计点参数校验失败400返回具体哪个字段错了。配合Valid和实体上的NotBlank(message deviceName 不能为空)调用方能直接看到改哪里。未知异常返回 500 但不泄露堆栈。message是固定的服务器内部错误堆栈只进服务端日志——这是安全底线面试常问。404 单独处理而不是落进兜底。源码注释里写了原因浏览器每次来要favicon.ico都会触发一次NoResourceFoundException如果落进Exception兜底正常请求会打一整条 ERROR 堆栈把真故障淹掉。五、实测正确的错误各回各家服务起在 8080用 curl 把三条错误路径都打了一遍全部真实结果# 1) 参数校验失败 → 400 POST /api/devices {productKey:productA} {code:400,message:deviceName: deviceName 不能为空,data:null} # 2) 同名设备冲突 → 409BusinessException POST /api/devices {deviceName:temp-sensor-01,productKey:productA} {code:409,message:同名设备已存在,data:null} # 3) 未知路径 → 404NoResourceFoundException GET /api/not-exist {code:404,message:资源不存在,data:null}结构统一、状态码准确、message 可读——到这里一切都对。六、翻车现场两种调用方错误被兜成了 500然后就是开头说的两连翻车# 4) 用 GET 访问只支持 POST 的接口 → 预期 405实际 500 GET /api/devices/1/reset-secret {code:500,message:服务器内部错误,data:null} # 5) POST body 写成非法 JSON → 预期 400实际 500 POST /api/devices {bad json {code:500,message:服务器内部错误,data:null}根因是同一个ExceptionHandler(Exception.class)是无条件兜底。Spring 对请求方法不支持HttpRequestMethodNotSupportedException和JSON 解析失败HttpMessageNotReadableException本来有各自的标准映射405/400但这两类异常没有专属处理器时会直接落进最宽的Exception兜底——被当成未知异常记 ERROR 堆栈、返回 500。这类问题的危害不只是状态码难看客户端的重试策略会跟着错500 让人以为重试有用405 重试毫无意义服务端日志被无效堆栈刷屏真正的故障反而看不见。这其实是第一章404 单独处理的教训换了个马甲再犯一次——调用方的错永远不该按服务端故障记账。七、修复兜底之前先铺两张网给处理器补上两个专属方法状态码和语义就归位了// 修复代码GlobalExceptionHandler 新增方法不支持 → 405ExceptionHandler(HttpRequestMethodNotSupportedException.class)publicResponseEntityApiResponseVoidhandleMethodNotSupported(HttpRequestMethodNotSupportedExceptione){returnResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(ApiResponse.error(405,请求方法不支持: e.getMethod()));}// 修复代码GlobalExceptionHandler 新增请求体解析失败 → 400ExceptionHandler(HttpMessageNotReadableException.class)publicResponseEntityApiResponseVoidhandleNotReadable(HttpMessageNotReadableExceptione){returnResponseEntity.badRequest().body(ApiResponse.error(400,请求体不是合法 JSON));}Spring 的异常匹配规则是先精确后兜底抛出的异常能命中更具体的ExceptionHandler就绝不落到Exception.class。所以只要补上网405/400 自动归位兜底继续只接真正的意外。面试聊到这里的加分句兜底处理器解决的是别把堆栈泄露给客户端而不是什么错都往里塞每发现一类正常错误被 500 接走就给它单独加一层。八、小结与面试考点这套组合拳一句话总结前端永远收到同一种结构服务端永远知道哪个错误是谁的。面试可以主动讲的点为什么统一响应体结构一致后前端封装、错误处理、拦截器都按同一套逻辑走代价是放弃了 HTTP 状态码的细粒度表达所以用 HTTP status 业务 code 双轨补齐。RestControllerAdvice 的匹配优先级精确类型 父类Exception.class是最后一张网不要让客户端错误4xx落进它。全局异常处理器的安全意义未知异常不能把堆栈、SQL、内部路径透给客户端message 固定、堆栈进日志。record 当响应体不可变、天然是数据载体Java 17 项目里的惯用法。作者软件工程在读专升本正在从零搭一套物联网设备接入平台Spring Boot EMQX MySQL踩过的坑都写成文章。上一篇写了 MQTT 遗嘱消息这一篇回到后端基本功。欢迎关注下期见。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑