全局异常处理-把错误拦在业务之外
07-全局异常处理-把错误拦在业务之外大家好我是黒漂技术佬。上篇我们把分页壳子PageResult讲透了这篇聊一个平时看不见、出事要你命的东西——全局异常处理。新手写业务最容易踩的坑在一个方法里try-catch包半屏catch 里e.printStackTrace()然后return null。前端拿到null一脸问号不知道是参数错了、没登录还是服务器炸了。更惨的是数据库抛个SQLException直接把堆栈塞进 HTTP 响应用户看见一坨红色异常页面。“URM Ultra的后端用 Spring Boot 3 的RestControllerAdvice把异常收口得极其干净。今天我们就拆开看它是怎么做到的以及为什么业务代码里不该出现 try-catch”。一、先搞懂异常到底该在哪处理很多教程一上来就贴RestControllerAdvice但没讲清楚为什么。我们先建立一条原则业务方法的职责是把事做成不是把错兜住。什么意思假设你写扣库存// 反面教材在业务里疯狂 try-catchpublicvoiddeductStock(LongdeviceId,StringslotNo,intqty){try{DeviceProductdprepository.findByDeviceIdAndSlotNo(deviceId,slotNo);if(dpnull){thrownewRuntimeException(货道不存在);}if(dp.getStock()qty){thrownewRuntimeException(库存不足);}dp.setStock(dp.getStock()-qty);repository.save(dp);}catch(Exceptione){e.printStackTrace();// 错日志没、异常吞了throwe;// 或者 return; 错上加错}}这种写法的毛病业务代码里夹着一堆异常处理读起来像在跟异常搏斗看不清主干逻辑catch后e.printStackTrace()既不进日志框架、也进不了监控系统抛出的还是RuntimeException前端收到一个 500 堆栈根本不知道库存不足是业务逻辑错还是系统崩了。正确思路业务里只管抛统一在一个地方接。抛异常是廉价的、语义明确的怎么把异常翻译成前端能懂的报文交给全局处理器。二、业务异常 BusinessException给错误一个业务身份项目先定义了一个专属异常把业务错误和普通系统错误区分开// 项目源码com.urm.ultra.common.BusinessExceptionGetterpublicclassBusinessExceptionextendsRuntimeException{privatefinalintcode;publicBusinessException(Stringmessage){this(500,message);}publicBusinessException(intcode,Stringmessage){super(message);this.codecode;}}注意三点它继承RuntimeException是运行时异常不用方法签名上写throws业务里随手throw new BusinessException(...)即可带了一个code字段。这就是业务码——Result里那个code0 表示成功非 0 表示各类错误提供了两个构造只传message时默认code500传(code, message)时自定义业务码比如库存不足可以抛new BusinessException(4001, 库存不足)前端就能按码做特定提示。有了它扣库存就能写得极其干净// 项目源码风格示意业务里只抛不抓publicvoiddeductStock(LongdeviceId,StringslotNo,intqty){DeviceProductdprepository.findByDeviceIdAndSlotNo(deviceId,slotNo);if(dpnull){thrownewBusinessException(货道不存在);}if(dp.getStock()null||dp.getStock()qty){thrownewBusinessException(4001,库存不足);}dp.setStock(dp.getStock()-qty);repository.save(dp);}主干逻辑三行错误用throw一句带过。这就是把错误拦在业务之外的第一层含义业务代码只描述正常流程 在异常点抛出自带语义的异常。三、RestControllerAdvice全局的异常接盘侠关键角色登场。RestControllerAdvice是 Spring 的统一异常拦截器专盯所有RestController里抛出的异常。项目里就这么一个类// 项目源码com.urm.ultra.common.GlobalExceptionHandlerSlf4jRestControllerAdvicepublicclassGlobalExceptionHandler{ExceptionHandler(BusinessException.class)publicResultVoidhandleBusiness(BusinessExceptione){returnResult.error(e.getCode(),e.getMessage());}ExceptionHandler({MethodArgumentNotValidException.class,BindException.class})publicResultVoidhandleValidation(Exceptione){Stringmsg参数校验失败;if(einstanceofMethodArgumentNotValidExceptionexex.getBindingResult().getFieldError()!null){msgex.getBindingResult().getFieldError().getDefaultMessage();}elseif(einstanceofBindExceptionexex.getBindingResult().getFieldError()!null){msgex.getBindingResult().getFieldError().getDefaultMessage();}returnResult.error(400,msg);}ExceptionHandler(Exception.class)publicResultVoidhandleException(Exceptione){log.error(系统异常,e);returnResult.error(500,系统异常: e.getMessage());}}逐行拆RestControllerAdvice告诉 Spring我是全局异常处理中心作用范围覆盖所有 REST 接口和ControllerAdvice的区别是它默认把返回值当ResponseBody处理直接写 JSON不用再标ResponseBody。Slf4jLombok 注解自动生成log对象兜底异常时打日志用。四、三个 ExceptionHandler 的分工ExceptionHandler(Xxx.class)表示这个方法专门处理 Xxx 类型的异常。匹配顺序是最具体的优先所以三个 handler 的层级是这样1. handleBusiness —— 业务异常原样回传ExceptionHandler(BusinessException.class)publicResultVoidhandleBusiness(BusinessExceptione){returnResult.error(e.getCode(),e.getMessage());}业务里抛的BusinessException这里把它翻译成Result.error(code, message)。比如前面库存不足抛的是new BusinessException(4001, 库存不足)前端就收到{code:4001,message:库存不足,data:null}前端按code4001弹该货道库存不足请重试体验直接拉满。2. handleValidation —— 参数校验失败统一 400ExceptionHandler({MethodArgumentNotValidException.class,BindException.class})publicResultVoidhandleValidation(Exceptione){...}这两个异常来自参数校验下一篇细讲DTO 上标了NotBlank但前端没传Spring 在校验阶段就抛MethodArgumentNotValidException。这里统一转成Result.error(400, 字段消息)——把哪个字段错、错在哪的提示直接塞进message前端照着弹就行。BindException是表单/路径变量绑定失败时抛的同类异常一并接住保证 GET 和 POST 两种校验失败都走同一套返回。3. handleException —— 兜底任何漏网的都归 500ExceptionHandler(Exception.class)publicResultVoidhandleException(Exceptione){log.error(系统异常,e);returnResult.error(500,系统异常: e.getMessage());}这是最后一道防线。任何你没预料的异常——NPE、SQL 报错、JSON 解析失败——都会被它接住转成code500的标准报文同时用log.error把完整堆栈打进日志。这一层至关重要对前端永远拿到结构统一的Result不会出现有时返回 Result、有时抛出 Tomcat 默认错误页的分裂对运维异常进日志了出问题能查对用户不会看到一坨英文堆栈而是一句系统异常: xxx。注意顺序——ExceptionHandler(Exception.class)放在最后。因为它最泛能接所有异常前面的BusinessException、MethodArgumentNotValidException更具体Spring 会优先匹配具体的。如果把Exception放第一业务异常反而会被它吃掉那就乱套了。五、为什么这叫与业务解耦回看开头那句原则现在我们能说清解耦在哪业务方法零 try-catch所有异常沿调用栈一路抛到GlobalExceptionHandler业务代码只剩 happy path 语义化 throw可读性和可维护性暴涨异常翻译集中所有异常 → HTTP 响应的映射只有一个类负责。哪天你想把 500 报文从系统异常: xxx改成服务器开小差了只改这一处响应结构恒定无论成功失败前端永远解析同一套Result{code,message,data}不用为不同错误写不同解析分支日志与对外信息分离兜底异常把堆栈记进日志给开发看对外只回系统异常加简短信息给用户看避免把内部细节泄漏出去。这四点合起来就是把错误拦在业务之外——错误在边界全局处理器被统一栏截、翻译、记录业务核心保持纯粹。六、一个常见误区401/404 要不要也管细心的朋友会问登录过期401、资源不存在404呢项目约定上BusinessException的code是可扩展的——比如设备不存在你可以抛new BusinessException(4040, 设备不存在)统一走code业务码前端按码提示HTTP 状态码仍是 200。这种业务码优先风格在前后端分离 小程序里很常见小程序端统一只看Result.code不依赖 HTTP 状态码分支。当然你也可以让 Spring Security 这类安全框架直接返回 HTTP 401那属于另一套鉴权链路。Demo 阶段URM Ultra用业务码表达业务失败足够清晰也避免了 HTTP 状态码和业务的语义打架。七、小结与避坑清单业务里只抛不抓异常沿调用栈抛到全局处理器业务主干才干净定义BusinessException(code, message)给错误一个业务身份前端按code做差异化提示RestControllerAdvice 多个ExceptionHandler按具体优先、兜底最后的顺序分层接异常BusinessException→ 原样Result.error(code,msg)校验异常 → 统一 400其它 → 兜底 500 打日志兜底 handler 务必打log.error否则线上异常就消失了你永远不知道系统炸过好处的本质异常翻译集中、响应结构恒定、业务与错误处理解耦。下一篇我们顺着MethodArgumentNotValidException这根线把参数校验讲透——看看NotBlank是怎么在进业务之前就把脏数据挡在门外的。

相关新闻

考试信息一站式查询:从报名时间到备考资料的全流程指南

考试信息一站式查询:从报名时间到备考资料的全流程指南

前阵子有个备考的朋友跟我抱怨,说准备教资和公考快被信息折腾疯了。明明只是想知道“什么时候报名、去哪个网站、看什么书”,结果搜索引擎翻了几页,公众号存了一堆截图,群里问来问去,最后发现连考试时间都没搞准。其实…

2026/10/15 0:54:45 阅读更多 →
“哈哈哈哈哈”为何能成为国民级网络热词?拆解情绪表达与内容运营密码

“哈哈哈哈哈”为何能成为国民级网络热词?拆解情绪表达与内容运营密码

如果你经常参与社交网络上的互动,一定见过这种场面:一条离谱视频底下的热评前排全是同一句话的变体——“哈哈哈哈哈”;工作群里同事丢了个翻车段子,后面跟着一整列“哈哈哈哈哈”;哪怕是气氛严肃的讨论帖,…

2026/10/15 0:54:45 阅读更多 →
MATLAB实现SMOTE小样本过采样:边界校验与混合特征处理

MATLAB实现SMOTE小样本过采样:边界校验与混合特征处理

1. 项目概述:为什么小样本分类问题值得你花20分钟认真读完这段话在某高校实验室做图像识别项目时,我拿到一份工业缺陷检测数据集:总共3200张图片,其中正常样本2980张,而真正关键的“微裂纹”缺陷样本只有220张——不到…

2026/10/15 0:53:44 阅读更多 →

最新新闻

KNN图像识别入门:从PCA降维到手写数字分类实战

KNN图像识别入门:从PCA降维到手写数字分类实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 1:37:04 阅读更多 →
52pojie 的 IoT 固件分析:熵值会告诉你哪一段藏了东西

52pojie 的 IoT 固件分析:熵值会告诉你哪一段藏了东西

副题:魔术字、香农熵、结构体——拆开一个固件镜像的三把钥匙 摘要:吾爱破解软件调试区里有一篇《IoT 漏洞分析之模拟环境搭建》(6037 回复),同区还有《记一次手机付费充电设备研究》(7196)。这…

2026/10/15 1:37:04 阅读更多 →
双 11 核心链路变异测试大摸底:让关键交易状态机测试坚不可摧

双 11 核心链路变异测试大摸底:让关键交易状态机测试坚不可摧

在电商交易系统中,最核心、也最令架构师夜不能寐的领域内核,永远是有限状态机(Finite State Machine, FSM)。 在双 11 狂风暴雨般的流量洪峰冲击下,一笔交易订单在几毫秒内可能经历极其错综复杂的状态跳转:…

2026/10/15 1:37:04 阅读更多 →
u-boot-2020.07在Exynos4412上的电源时钟内存初始化实战

u-boot-2020.07在Exynos4412上的电源时钟内存初始化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 1:37:04 阅读更多 →
行业术语同义词图谱挖掘:从非结构化语料到标准化知识库的语义归一

行业术语同义词图谱挖掘:从非结构化语料到标准化知识库的语义归一

在将海量企业内部非结构化语料(如研发设计文档、售后工单、专家经验纪要、行业研报)汇聚到统一知识库并驱动 RAG(检索增强生成)系统时,工程团队往往面临一个巨大的语义鸿沟——“专有名词的极度多样性与非标准性”。 在…

2026/10/15 1:37:04 阅读更多 →
工业机器人坐标系全解析:从原理到标定实操

工业机器人坐标系全解析:从原理到标定实操

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 1:36:03 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →