2. 错误处理基础:异常与错误的区别、错误码设计原则、错误信息分类

在构建企业级业务系统时,错误处理并非简单的“try-catch”堆砌,而是一套贯穿开发与生产环境的语义体系。本章从三个核心维度建立共识:异常与错误的本质区别、错误码的设计原则以及错误信息的分类标准。只有打好这个基础,后续的日志治理、告警收敛、环境适配才有统一的语言。

2.1 异常与错误的本质区别

很多团队将“异常”和“错误”混用,但在架构层面,二者有明确的职责边界。下表从触发源、可恢复性、处理策略三个维度进行对比:

对比维度 异常(Exception) 错误(Error)
触发源 业务逻辑违规、资源暂时不可用、参数校验失败 系统级故障、硬件失效、内存溢出、栈溢出
可恢复性 通常可恢复(重试、降级、补偿) 通常不可恢复,需人工介入或重启进程
处理策略 调用方主动捕获并决策(重试/回滚/返回降级结果) 记录现场快照后快速失败,避免数据污染
开发环境关注点 堆栈可读性、断点命中、参数上下文 内存转储、线程快照、GC日志
生产环境关注点 错误码、链路追踪、业务影响面 告警通知、自动重启、健康检查
架构原则:异常是业务契约的一部分,错误是基础设施的边界。在代码中,Exception 应被设计为可预见、可分类、可处理;Error 应被设计为不可静默吞噬,必须向上传播至全局处理器。

以下 SVG 展示了异常与错误在调用链中的不同传播路径与处理节点:

异常与错误传播路径对比 异常 vs 错误:传播路径与处理节点 异常路径(可恢复) 业务方法抛出 BusinessException 调用方捕获并决策 重试 / 降级 / 返回默认值 业务正常结束 错误路径(不可恢复) 系统抛出 OutOfMemoryError 全局异常处理器捕获 记录现场快照 + 快速失败 进程重启 / 人工介入 关键区别:异常携带业务上下文,错误携带系统快照;异常可重试,错误必须止损。

2.2 错误码设计原则

错误码是开发与生产环境之间最直接的“翻译器”。一套好的错误码体系,能让开发者在日志中秒级定位问题,也能让监控系统自动聚合同类故障。以下是经过多个企业级项目验证的六条核心原则:

  1. 唯一性:每个错误码全局唯一,不重复、不重载。禁止同一个错误码在不同模块表示不同含义。
  2. 可扩展性:编码结构预留模块位和版本位,避免因新增业务场景导致整体重构。
  3. 可读性:错误码本身携带分类信息,例如 PAY-4001 表示支付模块的客户端参数错误。
  4. 稳定性:已发布的错误码不可删除、不可变更含义;废弃码仅标记为“已弃用”,保留占位。
  5. 层级化:错误码按“系统级 → 模块级 → 业务级”分层定义,不同层级由不同团队管理。
  6. 环境透明:错误码在开发、测试、预发、生产环境保持一致,不因环境不同而改变含义。
推荐编码结构:{模块前缀}-{HTTP状态码简写}{错误序号},例如 ORD-4001(订单模块,400 类参数错误,序号 01)。这种结构在日志聚合时可直接按前缀和状态码分类,无需额外解析。

以下表格展示了一个典型的企业级错误码分级示例:

层级 错误码范围 示例 管理方
系统级 SYS-0001 ~ SYS-9999 SYS-0500 内部服务不可用 基础架构团队
模块级 PAY-0001 ~ PAY-9999 PAY-4003 支付金额超限 支付团队
业务级 ORD-0001 ~ ORD-9999 ORD-4001 订单商品库存不足 订单业务团队
第三方集成级 THD-0001 ~ THD-9999 THD-5021 物流网关超时 集成对接团队

2.3 错误信息分类

错误信息不能只有堆栈和错误码,还需要按用途分类,以便不同角色(开发者、运维、客服、终端用户)都能获取到有效信息。我们将错误信息分为以下四类:

  • 技术诊断信息:面向开发者和运维,包含完整堆栈、线程 ID、请求追踪 ID、数据库连接状态等。仅在开发环境和生产环境的调试白名单中输出。
  • 业务上下文信息:面向业务开发人员,包含当前请求参数、用户 ID、订单号、触发规则等。用于快速复现业务异常场景。
  • 用户友好信息:面向终端用户,使用自然语言描述,不暴露技术细节。例如“当前服务繁忙,请稍后重试”。
  • 审计合规信息:面向合规与安全团队,包含操作时间、操作人、操作类型、数据变更前后快照。不可被用户或普通开发者查看。

以下 SVG 展示了四类错误信息在开发与生产环境中的输出策略差异:

错误信息分类与环境输出策略 错误信息分类与环境输出策略 技术诊断信息 堆栈 / 线程 / 追踪ID 业务上下文信息 参数 / 用户 / 订单号 用户友好信息 自然语言描述 审计合规信息 操作人 / 数据快照 开发环境:全部输出(含完整堆栈 + 参数快照 + 调试上下文) 生产环境:仅输出用户友好信息 + 脱敏后的业务上下文 + 错误码 生产环境调试白名单:按请求头 / IP / 用户ID 动态开启技术诊断信息 审计合规信息在任何环境都不应出现在日志或响应体中,仅通过安全审计通道导出。

2.4 代码示例:错误码与信息分类的落地实现

以下 Java 代码片段展示了如何将错误码、异常分类与信息分层结合,实现一个可适配开发与生产环境的错误处理骨架:

// 错误码枚举,携带模块前缀、HTTP状态码、用户友好信息
public enum ErrorCode {
    ORD_4001("ORD-4001", 400, "订单商品库存不足", "当前商品库存不足,请调整购买数量"),
    PAY_5002("PAY-5002", 500, "支付网关超时", "支付服务暂时不可用,请稍后重试"),
    SYS_0500("SYS-0500", 500, "内部服务不可用", "系统繁忙,请稍后再试");

    private final String code;
    private final int httpStatus;
    private final String techMessage;   // 技术诊断信息
    private final String userMessage;   // 用户友好信息

    // 构造方法、getter 省略
}

// 业务异常,携带错误码和业务上下文
public class BusinessException extends RuntimeException {
    private final ErrorCode errorCode;
    private final Map<String, Object> context;  // 业务上下文信息

    public BusinessException(ErrorCode errorCode, Map<String, Object> context) {
        super(errorCode.getTechMessage());
        this.errorCode = errorCode;
        this.context = context;
    }

    // 根据环境输出不同信息
    public Object toResponse(boolean isDebugEnabled) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", errorCode.getCode());
        result.put("message", errorCode.getUserMessage());
        if (isDebugEnabled) {
            result.put("debug", context);          // 仅开发/调试环境输出
            result.put("stack", this.getStackTrace());
        }
        return result;
    }
}
生产环境陷阱:切勿在生产环境将 context 或 stack 直接序列化到 HTTP 响应中。这会导致敏感数据泄露(如用户手机号、支付密码)和响应体过大。始终通过环境标志或白名单机制控制技术信息的输出。