首页/新闻资讯/正文详情

Spring Boot 统一返回体 + 全局异常处理:让接口不再吐 500 堆栈

发布时间:2026/9/24 17:55:19 来源:云帆数科 栏目:资讯中心
Spring Boot 统一返回体 + 全局异常处理:让接口不再吐 500 堆栈
Spring Boot 统一返回体 全局异常处理让接口不再吐 500 堆栈环境Spring Boot MyBatis-Plus Lombok JDK 17上一篇把三层架构搭好之后接口能查数据了但返回的东西不太像样直接一个裸数组。而且参数写错的时候浏览器上是一整页 Java 堆栈。这篇做两件事把返回格式统一成ResultT再把异常统一兜住。做完之后不管请求成功还是失败前端拿到的都是同一个结构。一、为什么要有统一返回体改造前 Controller 长这样GetMapping(/list)publicListBooklist(...){returnbookService.listBooks(uid,status,keyword);}成功的时候返回[{...}, {...}]。但问题是失败的时候返回什么前端要写这样的判断逻辑先看 HTTP 状态码是不是 200再看返回体是不是数组再看数组里有没有数据。接口一多每个都要写一遍。统一成ResultT之后前端只需要看code{code:1,message:success,data:[...]}Result 类packagecom.first.book.common;importlombok.Data;// T 是类型参数T 代表这个 Result 里装的到底是什么类型// 装书的列表就是 ResultListBook装一个用户就是 ResultSysUserDatapublicclassResultT{// 1 成功0 失败privateIntegercode;// 给前端看的提示文字privateStringmessage;// 真正的数据。类型是 T具体是什么由调用方决定privateTdata;// 成功时用这个把数据装进来code 固定 1publicstaticTResultTsuccess(Tdata){ResultTresultnewResult();result.setCode(1);result.setMessage(success);result.setData(data);returnresult;}// 失败时用这个只传一句提示data 保持 nullpublicstaticTResultTerror(Stringmessage){ResultTresultnewResult();result.setCode(0);result.setMessage(message);returnresult;}}Controller 改两处就行GetMapping(/list)// 返回类型从 ListBook 变成 ResultListBookpublicResultListBooklist(RequestParam(requiredfalse)Integeruid,RequestParam(requiredfalse)Integerstatus,RequestParam(requiredfalse)Stringkeyword){ListBookbooksbookService.listBooks(uid,status,keyword);// 不再直接 return而是包一层returnResult.success(books);}Service 一行都不用改。包装是 Controller 的活Service 只管业务不需要知道接口要返回成什么格式。二、静态方法里为什么要多写一个T这是我卡最久的地方。看这两行的区别// 类上的 T —— 实例方法可以直接用publicTgetData(){returndata;}// 静态方法 —— 必须在自己签名里重新声明 TpublicstaticTResultTsuccess(Tdata){...}当时盯着看了半天没想通类上不是已经有T了吗为什么还要再写一个关键在这个 T 是谁的。类上的那个T属于实例。你写new ResultString()的时候T 才被定下来是 String。也就是说它得先有个对象才有意义。而静态方法不用 new 就能调用。它压根不属于任何实例所以类上的 T对它来说是不存在的。它只能在自己签名里声明一个自己的 T。这两个 T 连作用域都不是一个层级的一个是实例级一个是方法级。如果漏写会报这个错错误: 无法从静态上下文中引用非静态类型 T编译器这句话就是在说静态方法里没有当前实例你说的那个 T 我找不到。补上之后类型推断才能工作 —— 传ListBook进去返回的就是ResultListBook。后面单元测试里可以直接声明类型接收不用强转。三、为什么用静态工厂不用 new写成new Result()然后挨个 setter要三行而且很容易漏 —— 忘了setCode返回的 code 就是 null前端判断全乱。用静态工厂一行搞定而且成功时 message 一定是 “success”、code 一定是 1把正确的格式焊死在方法里。以后要改提示文案改一处就行不用翻所有 Controller。data声明成T而不是Object也是一个道理。写 Object 也能编译但调用方拿到的类型信息全丢了取值得强转。用泛型之后编译器会帮你检查你装的类型和声明的对不对。四、异常怎么兜上半场只解决了正常路径返回什么。但请求出错的时候呢改造前访问localhost:8080/book/list?uid-1浏览器上是一整页 Java 堆栈。这东西不能说完全没用开发时能看堆栈但绝对不能给用户看—— 里面有你项目名、类名、甚至框架版本。1. 自定义业务异常packagecom.first.book.exception;// 业务异常用来表达这个请求本身就不合法publicclassBizExceptionextendsRuntimeException{// 只留一个构造方法把提示语交给父类存着publicBizException(Stringmessage){super(message);}}为什么继承RuntimeException不继承Exception这个点值得说一下。继承Exception是受检异常编译器会强迫每个调用方要么try-catch要么在自己方法签名上写throws。结果就是listBooks抛了BookService接口要声明throws BizExceptionBookController也得处理 —— 一条调用链全被污染每个中间层都得写一个自己根本不处理的东西。继承RuntimeException是非受检的随时能抛上层不用知道一路冒泡上去最后被全局处理器接住。这才是我想要的效果。super(message)那行是把提示语存进异常对象后面用e.getMessage()取出来。2. 全局处理器packagecom.first.book.exception;importcom.first.book.common.Result;importlombok.extern.slf4j.Slf4j;importorg.springframework.web.bind.annotation.ExceptionHandler;importorg.springframework.web.bind.annotation.RestControllerAdvice;Slf4j// 这个注解让下面的方法对所有 Controller 生效RestControllerAdvicepublicclassGlobalExceptionHandler{// 第一层专门接住 BizException我们自己主动抛的那种ExceptionHandler(BizException.class)publicResultVoidhandleBizException(BizExceptione){// 业务异常是可预期的用 warn 级别log.warn(业务异常{},e.getMessage());// 把抛异常时写的那句提示原样交给前端returnResult.error(e.getMessage());}// 第二层兜住所有没被上面接住的异常ExceptionHandler(Exception.class)publicResultVoidhandleException(Exceptione){// 必须打日志。吞掉异常还不留痕迹线上出问题你什么都查不到log.error(系统异常,e);// 对外只给一句模糊的话别把内部细节暴露出去returnResult.error(系统繁忙请稍后重试);}}返回类型写的ResultVoid是因为失败响应里没有 data。泛型填Void表示这里没有数据不影响 JSON 输出 ——data字段照样是 null。3. Service 里加个校验光有处理器还不够得有人真的抛出来才验证得了OverridepublicListBooklistBooks(Integeruid,Integerstatus,Stringkeyword){// 新增业务校验if(uid!nulluid0){thrownewBizException(uid 不能为负数);}QueryWrapperBookqwnewQueryWrapper();// ... 后面不变}注意这里没有 try-catch。Service 只管发现问题就抛至于怎么变成给前端的 JSON那是全局处理器的活。每个业务方法都能保持干净这是这套机制最舒服的地方。五、两个 handler 谁先谁后不用你操心顺序Spring 自己按就近原则找最匹配的那个。抛BizException→ 精确命中handleBizException抛别的比如?uidabc触发的MethodArgumentTypeMismatchException→ 落到handleException兜底。关键在于兜底那个Exception.class一定要有。少了它没被匹配上的异常照样会变成 500 堆栈等于白做。还有一点容易忽略兜底里必须打日志。捕获了异常却什么都不记这叫吞异常。线上用户报接口报错了你打开日志一片干净 —— 等于自己把眼睛蒙上。log.error(系统异常, e)的第二个参数传e才会把完整堆栈打出来。只传字符串的话堆栈就丢了。分工要清楚完整堆栈写进日志给自己看模糊提示返回前端给用户看两者别搞反。六、验收重启应用依次访问下面三个地址。请求返回的 message走了哪条路localhost:8080/book/listsuccessdata 里有 6 条正常路径localhost:8080/book/list?uid-1uid 不能为负数精确命中handleBizExceptionlocalhost:8080/book/list?uidabc系统繁忙请稍后重试落到handleException兜底第二个是重点message就是你在throw new BizException(...)里写的那句话说明从 Service 抛出来、一路冒泡到被处理器接住、再转成 JSON 的这条链路真的通了。第三个也值得看一眼。abc转不成数字Spring 抛的是MethodArgumentTypeMismatchException它不属于 BizException所以被兜底接住了。提示语系统繁忙其实不准确明明是参数写错了这是我故意留的简化等学到参数校验Validation时再补上精确提示。另外注意这三个响应的 HTTP 状态码都是 200不是 500。因为异常已经被处理成正常返回值了。前端靠code判断成败这是企业项目里的常见做法 —— 否则前端每调一个接口都得同时处理 HTTP 状态码和业务码反而更乱。七、我遇到的报错报错 / 现象原因传uid-1还是吐 500 堆栈GlobalExceptionHandler忘了加RestControllerAdviceSpring 扫不到它返回值不是 JSON 而是页面名用了ControllerAdvice少写了 ResponseBody 那半个要换成RestControllerAdvice找不到符号: 变量 logSlf4j没加或者 IDEA 没识别 lombokBuild → Rebuild找不到符号: 方法 getMessage()BizException忘了写super(message)消息没存进去三个请求全都返回系统繁忙兜底方法写得太宽把BizException也吃掉了检查第一个 handler 的注解参数改了没生效应用没重启Spring Boot 默认不热更新八、下一步到这里一个接口的正常返回和出错返回就都有统一格式了。三层架构负责各层职责分离ResultT负责正常路径RestControllerAdvice负责异常路径几块拼起来才像个正经项目。下一步打算学参数校验Validation到时候?uidabc这种错也能给出准确的中文提示不用再拿系统繁忙糊弄。

相关推荐

opencv概述
opencv概述

了解OpenCV 1、概述2、OpenCV详细介绍 2.1、OpenCV的起源2.2、OpenCV开发语言2.3、OpenCV的应用领域 3、OpenCV模块划分4、OpenCV源码文件结构 4.1、根目录介绍4.2、常用模块介绍4.3、CUDA加速模块 5、OpenCV配置以及Visual Studio使用OpenCV6、关于Lena图片7、OpenCV和OpenGL的… · 2026/9/24 17:55:05

正则化:为什么模型越复杂越容易过拟合?L1/L2/Dropout
正则化:为什么模型越复杂越容易过拟合?L1/L2/Dropout

从 Tikhonov 正则化到 Dropout,理解防过拟合的数学本质开头:模型越「聪明」,越容易「死记硬背」 上一篇我们学习了 SVM。SVM 通过最大间隔和正则化参数 C 来防止过拟合。 今天我们系统地学习正则化——防止过拟合的核心技术。 你有没有遇到过… · 2026/9/24 17:55:05

博客系统接口测试用例设计
博客系统接口测试用例设计

· 2026/9/24 17:54:46

GPT 6 Astra vs Opus 5.5:同一张“鹈鹕骑自行车”,不同思考档位能差多少?
GPT 6 Astra vs Opus 5.5:同一张“鹈鹕骑自行车”,不同思考档位能差多少?

可以。下面我直接按 CSDN/技术博客 的风格给你整理一版,图片位置也一起放进去。为了避免把单次样例说成普遍结论,我会把文章定位成一次 实际体验对比。 GPT 6 Astra vs Opus 5.5:同一张“鹈鹕骑自行车”,不同思考档位能差多少&… · 2026/9/24 19:07:21

手机存储空间不足别只清缓存:从原理到实操的完整清理指南
手机存储空间不足别只清缓存:从原理到实操的完整清理指南

我手机里最常出现的"劝退"信号,从来不是卡顿,而是那条怎么躲都躲不掉的"存储空间不足"。64G的老机型,连哄带骗用了三年,最后连在朋友圈发张照片都得先腾地方。真正开始琢磨这个问题,是我发现系统自… · 2026/9/24 19:07:14

ARIMA+SVM混合模型:股票价格预测的残差建模与Python实战
ARIMA+SVM混合模型:股票价格预测的残差建模与Python实战

简介:这份资源面向具备一定MATLAB基础、希望入门时间序列与机器学习组合建模的金融数据分析学习者,核心是用支持向量机改进ARIMA股票价格预测。包内共3个文件,以2个m脚本和1个xlsx数据表为主,压缩包约13KB,脚本承担ARI… · 2026/9/24 19:07:14

shadcn-vue Navigation Menu 组件实战:基于 reka-ui 构建可访问的网站导航栏
shadcn-vue Navigation Menu 组件实战:基于 reka-ui 构建可访问的网站导航栏

shadcn-vue Navigation Menu 组件实战:基于 reka-ui 构建可访问的网站导航栏 【免费下载链接】shadcn-vue Vue port of shadcn-ui 项目地址: https://gitcode.com/gh_mirrors/sh/shadcn-vue Navigation Menu 是 shadcn-vue 提供的用于网站导航的组件集合&… · 2026/9/24 19:07:14

Node.js服务端开发实战:从事件循环到异步I/O与部署
Node.js服务端开发实战:从事件循环到异步I/O与部署

先说清楚一件事:Node.js 不是一门语言,也不是一个框架,它是一个“服务端运行时环境”。很多人刚接触的时候,下载安装完 Node.js,打开一个黑乎乎的终端敲了两行代码,然后问我:“所以这东西到底解… · 2026/9/24 19:07:08

kpartx命令详解:轻松挂载多分区磁盘镜像
kpartx命令详解:轻松挂载多分区磁盘镜像

1. kpartx 到底解决什么问题:从一次“挂不上镜像”说起 做嵌入式Linux开发、玩树莓派镜像、或者帮朋友恢复一张整盘备份的人,几乎都遇到过同一个尴尬:手里拿到一个 xxx.img 文件,明明里面有好几个分区,用 mount -o … · 2026/9/24 19:07:08

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13

1D-CNN时间序列建模实战:从Conv1d原理到工业落地
1D-CNN时间序列建模实战:从Conv1d原理到工业落地

简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26

柔软的L:汉语语流中被忽视的舌肌张力控制
柔软的L:汉语语流中被忽视的舌肌张力控制

1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码