学习 Java 服务开发时,把异常统一转换成“操作失败”,看起来是一种简单整齐的处理方式。调用方不用看到内部细节,接口格式也比较统一。但当所有失败都只剩下同一句话,调用方往往无法判断应该修改输入、稍后重试,还是联系维护人员。今天想整理的,是如何让错误返回既保留必要含义,又不泄漏不该暴露的信息。
可以设想一个修改收货地址的接口。订单不存在、用户无权修改、订单已经发货,以及数据存储暂时不可用,都可能导致修改没有完成。它们的结果相似,原因却不同。如果统一返回操作失败,页面可能引导用户不断重试一个根本不允许执行的操作;维护人员看到反馈时,也无法迅速缩小排查范围。错误表达的价值,就在于把下一步行动需要的信息传递出去。
首先需要区分业务拒绝与系统故障。业务拒绝是系统按照规则判断这次操作不成立,例如订单当前阶段已经不允许修改地址。系统故障则表示系统没有正常完成处理,可能暂时无法确认结果。两者不应该只靠提示文字中的某个关键词区别,而应该在接口约定中有稳定的分类,让调用方能够按约定处理。
错误标识和展示文字也承担不同职责。展示文字可以为了用户体验而调整,错误标识则需要保持足够稳定。若调用方通过比较中文提示来决定行为,一次文案修改就可能改变程序逻辑。更可靠的做法是约定明确的错误含义,文字用于解释,标识用于识别,同时避免让一个标识在不同入口代表互不相关的原因。
不过,细分并不是越多越好。只有调用方确实需要区别处理,或者排查中需要可靠区分的情况,才值得成为明确类别。如果每个底层异常都直接变成一个对外错误,内部实现变化就会不断影响接口。可以在服务边界把内部原因转换成适合当前业务的表达,同时在内部记录必要的原始原因和关联线索。
对外提示尤其要注意信息范围。数据库地址、内部路径、完整调用堆栈以及凭据都不应随着错误响应暴露出去。权限相关的错误还要结合业务考虑,避免通过不同提示让无权访问的人推断某条记录是否存在。清楚表达错误,不等于无条件公开所有细节;需要公开的是调用方有权知道、并且能够据此采取行动的信息。
重试建议也不能随意附加。暂时不可用可能适合稍后再试,但如果操作带有副作用,失败响应不一定意味着操作没有发生。比如写入已经完成,返回响应时才出错,此时盲目重试可能产生重复效果。错误约定应当说明哪些情况可以安全重试,哪些情况需要先查询结果,不能用一句“请重试”代替对业务边界的设计。
服务内部仍然需要保留失败链路。底层捕获异常以后,如果只返回一个空结果,上层就可能把系统故障误认为正常的无数据。反过来,把每个正常的业务拒绝都当作严重系统故障,也会产生大量噪声。异常转换、返回结果和日志记录应当相互配合,让外部表现和内部事实能够通过关联标识对应起来。
对于一个请求包含多项工作的情况,还需要说明失败的范围。批量修改中有一项失败,究竟是全部回滚,还是其他项已经完成?如果只返回整体失败,调用方就不知道哪些项目需要继续处理。如果业务允许部分成功,应当设计可定位的逐项结果;如果要求整体一致,则需要真实的执行机制保证,而不只是让响应看起来像一个整体。
学习时可以拿一个简单接口列出几种失败:输入缺失、规则不允许、无权访问、依赖暂不可用和结果不确定。对每一种情况,分别写下调用方应该看到什么、系统内部应该保留什么,以及下一步允许做什么。这样检查以后,错误处理就不再只是捕获异常和拼接提示。对我来说,一个好的错误返回,是在失败发生之后仍然维护清晰的约定,让调用方能够合理行动,也让维护者有依据理解发生过什么。