作为系统架构设计师,在进行系统架构设计时,异常处理(Exception Handling)的设计往往能体现一个团队编码规范质量,直接对性能分析、故障发现、故障诊断等起到重要作用。
异常处理中架构设计思考:业务逻辑异常是否应该当做Exception?异常分为几类?异常的编码范式是什么?当面对一个可能为null的字段时,究竟是该优雅地使用 Objects.isNull() 进行常规前置判断,还是果断地抛出一个异常?这背后隐藏着对架构设计与编程规范深层次的考量。
本文分享关于 Exception 处理的架构设计思考。
注:本文不描述Java基础知识,如Error和Exception的区别,受检异常和非受检异常等。
异常 Exception
在软件系统架构中,异常(Exception)是系统应对非预期事件的保底机制。一个设计良好的异常处理体系,是系统健壮性的最后一道防线。
异常对系统的影响
失控的异常处理会对系统产生多维度的负面影响:
- 服务可用性:未捕获的运行时异常(RuntimeException)可能导致单次请求中断,甚至在极端情况下导致线程池枯竭。
- 资源损耗:异常的抛出涉及堆栈跟踪(Stack Trace)的生成,这在 CPU 和内存上都有明显的开销。在并发高发期,海量异常会引发“异常风暴”,极大损害系统吞吐量。
- 用户体验与安全:直接将原始堆栈信息返回给调用方,不仅用户体验糟糕,更可能泄露数据表名、内部路径等敏感信息,造成安全隐患。
异常处理:全局视野与编程规范
作为系统架构师,设计异常处理机制时,核心目标是:语义清晰、链路可追溯、对内严谨、对外友好。
全局异常处理,与异常大类
mindmap
root((异常处理 Exception Handling))
全局异常处理
统一处理 Result<T>
异常分层
服务端系统异常(SystemException)
外部/前端接口异常(InterfaceException)
业务异常(BizException)
异常治理
与业务解耦合: 抛出与记录日志
异常优先级: 日志级别
全局异常处理 (Global Exception Handling)
在微服务架构中,强烈建议利用 @RestControllerAdvice 实现统一异常捕获(以 Spring 框架为例)。这种设计实现了业务逻辑与异常处理系统的完全解耦:
- 统一返回格式:确保对内严谨、对外友好。
- 核心分层设计:通过对异常进行科学分层,实现差异化的响应与治理策略。
下面探讨异常的分类。
服务端系统异常(SystemException)
最常见的异常,空指针、栈溢出、请求拒绝、网络中断、服务器宕机等。屏蔽底层堆栈,统一返回“系统繁忙”,并由开发人员通过监控与日志重点关注并排查。
当发生服务端系统异常时,服务往往会降级,严重时会引发系统崩溃,导致整个服务不可用。因此,服务端系统异常应当作为异常处理的最高优先级。
外部/前端接口异常(InterfaceException)
由于非法入参、URL 拼接错误、绕过前端校验等导致的契约违背,也可能是用户偶然进入错误逻辑。明确反馈错误至调用方(如 400 Bad Request),并在后台记录安全审计日志,防御性地阻断海量非法请求。
出现此类错误,意味着:
- 调用方违反了接口调用契约,没有进行完备的前置校验
- 用户偶然触发,代码逻辑存在漏洞,导致用户陷入逻辑黑洞(有点类似于电子游戏中,玩家卡到空气墙外了)
- 黑客攻击,拼接请求,绕过前端校验,可能存在DDos攻击
这类异常虽然不会导致系统崩溃,但会给系统带来安全隐患,或会给用户带来不好的体验。因此,这类异常应作为次优先级。
业务异常(BizException)
属于业务流程的一部分,是系统运行时“预料中”的限制(如:余额不足、账户密码错误),应当返回面向用户的友好提示,引导用户完成正确操作。
业务异常是具有争议的,用户账户密码错误、余额不足等是否应该当做异常,有以下两种观点:
观点一:业务异常(失败)属于代码逻辑中期望发生的,属于if-else范畴,不应当作为异常抛出,而应使用如Result.success()或Result.fail(),在业务逻辑上进行区分,工程师无需关注。
观点二:业务异常(失败)应当作为Exception抛出,因为虽然失败是预料中允许发生的,但失败是小概率事件,作为异常处理,利于快速发现和定位问题。
举一个例子,比如用户登录时发生账户密码错误。在观点一中这属于期望发生的,是正常的业务逻辑,无需关注。在观点二中,登录失败是小概率事件,应当抛出UsernamePasswordException异常,比如短时间内出现了大量的登录失败,工程师应有所察觉,需要关注代码实现上是否出现了bug。
这两种思想都有一定道理,观点一关注系统稳定性,观点二关注业务逻辑正确性。
因此,业务异常是可选的,优先级是最低的,但也值得关注。
【用户登录账户密码连续错误5次,可能马上投诉电话就来了】
【用户充值多次失败,营收可能就流失了】
因此,对于敏感类业务,应当将业务逻辑失败视为异常抛出。另一方面,如果非敏感业务的业务逻辑失败都视为异常,会造成异常淹没、异常噪声,最终导致工程师注意力分散。
😄逻辑相对论:都重要 = 都不重要,全部都是异常 = 没有异常,哈哈!
异常治理与日志
既然出现了异常,那么必然要对异常进行响应、处置和修复,称之为异常治理。
业务功能代码实现与异常治理解耦合是推荐的处理方式。程序员只负责抛出异常,异常处理交给其他进程,既减轻了代码的复杂度,也可以避免异常处理可能导致主进程阻塞甚至崩溃,因小失大。
最佳实践:将异常抛出并记录到日志中,再由日志服务异步地进行收集、分析、告警。
异常级别与日志级别
异常是有优先级的,可以与日志级别对应起来:
| 异常类型 | 日志级别 | 描述 | 告警权重 |
|---|---|---|---|
| 业务异常 | WARN | 允许发生的业务失败(如余额不足、账户密码错误) | 20 WARN |
| 外部/前端接口异常 | ERROR | 非法入参、URL拼接错误、绕过前端校验等契约违背 | 5 ERROR |
| 服务端系统异常 | FATAL | 空指针、栈溢出、请求拒绝、网络中断、服务器宕机等 | 1 FATAL |
异常日志汇聚成为指标,进行告警,告警规则基于上述告警权重关系,仅供参考。
总结
本文从系统架构设计的角度,探讨了异常处理的架构设计思路。 核心要点回顾:
-
全局异常处理机制:通过统一异常捕获中心实现业务逻辑与异常处理的解耦,确保对内严谨、对外友好。
-
异常分层设计:将异常科学地分为三个层次:
- 服务端系统异常(SystemException):最高优先级,需立即响应
- 外部/前端接口异常(InterfaceException):次优先级,关注安全与契约
- 业务异常(BizException):可选处理,根据业务敏感性决定
-
异常治理策略:采用异步日志记录与告警机制,避免异常处理阻塞主业务流程,同时建立基于权重的告警规则。
异常处理不仅是技术问题,更是架构思维的体现,需要平衡系统稳定性与业务正确性,避免"异常淹没"现象。在下一篇文章中,我们将深入探讨异常处理的编码实践,包括具体的代码实现模式、最佳实践案例,以及如何在日常开发中落地这些架构设计理念。
关注微信公众号,获取运维资讯
如果此篇文章对你有所帮助,感谢你的点赞与收藏,也欢迎在评论区友好交流。
微信搜索关注公众号:持续运维