2. 错误分类与分层:业务异常、系统异常、基础设施异常、分层原则与职责划分

在企业级系统中,错误并非铁板一块。将错误按性质与影响范围进行分层分类,是构建可观测、可治理的错误处理骨架的第一步。本章将定义三类核心错误,并明确各层在错误处理中的职责边界。

2.1 错误的三层分类模型

根据错误的来源与可控程度,我们将错误划分为三个层次:基础设施异常、系统异常与业务异常。三者构成一个从底层到顶层的金字塔结构。

错误三层分类模型 基础设施异常 网络中断 · 磁盘故障 · 数据库连接池耗尽 · 内存溢出 系统异常 配置错误 · 依赖服务超时 · 序列化失败 · 资源未释放 框架级异常 · 中间件异常 · 第三方SDK异常 业务异常 余额不足 · 订单状态冲突 · 权限校验失败 参数校验不通过 · 业务规则违例 越靠近底层,错误越不可预测、恢复成本越高;越靠近顶层,错误越可预期、越适合用业务逻辑处理 分层核心原则:下层错误向上层映射时,必须保留原始上下文,禁止吞没

2.2 三类异常的详细定义与特征

维度 基础设施异常 系统异常 业务异常
来源 硬件、操作系统、网络、中间件 应用框架、依赖服务、资源管理 业务规则、用户输入、状态机
可恢复性 通常不可恢复,需人工介入 部分可恢复(重试、降级) 可恢复(修正输入或状态)
影响范围 全局或集群级别 服务实例或模块级别 单个请求或事务级别
响应方式 告警 + 自动容灾 + 人工恢复 熔断、限流、降级、重试 返回错误码 + 友好提示
日志级别 ERROR / FATAL ERROR / WARN WARN / INFO
典型示例 磁盘IO错误、网络分区、OOM Redis连接超时、JSON解析失败 账户冻结、库存不足
设计原则:业务异常必须显式定义并文档化,作为API契约的一部分;系统异常应封装为内部异常,避免泄露实现细节;基础设施异常应尽早检测并触发自动恢复流程。

2.3 分层原则与职责划分

错误处理的分层原则遵循关注点分离与职责单一。每一层只处理自己能力范围内的错误,并将无法处理的错误向上层抛转。

2.3.1 四层职责矩阵

层次 职责 处理策略 禁止行为
接入层
(API Gateway / Filter)
全局异常捕获、请求校验、统一响应格式 捕获所有未处理异常,转换为标准错误响应 不得修改业务错误码;不得吞没异常堆栈
应用层
(Controller / Service)
业务异常判断、参数校验、事务边界控制 主动抛出业务异常;捕获系统异常并重试或降级 不得将系统异常直接返回给客户端
领域层
(Domain / Manager)
核心业务规则校验、状态机约束 抛出领域异常,携带业务上下文 不得依赖基础设施细节
基础设施层
(DAO / RPC / MQ)
资源访问、网络通信、数据持久化 将基础设施异常包装为系统异常,保留原始异常链 不得在底层直接记录日志或发送告警

2.3.2 异常传播与包装规则

异常在层间传播时,必须遵循以下三条硬规则:

  1. 向上包装,不吞没:下层异常必须被上层捕获并包装为当前层语义的异常,原始异常必须作为 cause 保留。
  2. 边界转换,不泄露:在应用层与接入层的边界,所有非业务异常必须转换为统一的系统错误码,禁止将 NullPointerException、SQLException 等实现细节暴露给客户端。
  3. 分级记录,不重复:每层只记录自己添加的上下文信息,底层异常由全局异常处理器统一记录完整堆栈。
代码示例:异常包装与传播
// 基础设施层:将SQL异常包装为系统异常
public class UserRepository {
    public User findById(Long id) {
        try {
            return jdbcTemplate.queryForObject(...);
        } catch (DataAccessException e) {
            // 包装为系统异常,保留原始cause
            throw new SystemException("DB_QUERY_FAILED", "用户查询失败", e);
        }
    }
}

// 应用层:捕获系统异常,决定是否重试或降级
public class UserService {
    public UserDto getUser(Long id) {
        try {
            User user = userRepository.findById(id);
            return UserDto.from(user);
        } catch (SystemException e) {
            // 如果是可重试的异常(如连接超时),执行重试
            if (e.isRetryable()) {
                return retryTemplate.execute(ctx -> getUser(id));
            }
            // 否则转换为业务友好的错误码
            throw new BusinessException("USER_SERVICE_UNAVAILABLE", "用户服务暂时不可用");
        }
    }
}

// 接入层:全局异常处理器,统一响应格式
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusiness(BusinessException e) {
        return ResponseEntity.status(400)
            .body(new ErrorResponse(e.getCode(), e.getMessage()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleUnknown(Exception e) {
        // 记录完整堆栈,但返回通用错误
        log.error("未捕获异常", e);
        return ResponseEntity.status(500)
            .body(new ErrorResponse("INTERNAL_ERROR", "系统内部错误"));
    }
}

2.4 分层错误处理的典型流程

以下流程图展示了一个请求从接入层到基础设施层,再返回错误响应的完整路径:

分层错误处理流程示例 接入层 应用层 领域层 基础设施层 异常向上传播 异常向上传播 关键规则:基础设施层异常 → 领域层包装为系统异常 → 应用层决定重试/降级 → 接入层统一响应 每一层只处理自己能力范围内的错误,超出能力范围则向上抛转,绝不吞没

2.5 常见反模式与避坑指南

反模式1:在底层记录日志并抛出异常
后果:日志重复、难以定位根因。正确做法:底层只抛异常,由全局处理器统一记录。
反模式2:将系统异常直接返回给前端
后果:暴露实现细节,存在安全风险。正确做法:在接入层统一转换为标准错误码。
反模式3:使用异常控制业务逻辑流程
后果:性能低下、代码可读性差。正确做法:业务分支用条件判断,异常只用于真正的异常情况。

2.6 本章小结

  • 错误分为基础设施异常、系统异常、业务异常三层,每层有明确的来源、可恢复性和处理策略。
  • 分层原则:下层向上包装、边界转换、分级记录,确保异常上下文不丢失、实现细节不泄露。
  • 职责划分:接入层负责统一响应,应用层负责业务判断与重试降级,领域层负责规则校验,基础设施层负责资源访问与异常包装。
  • 避免反模式:不重复记录日志、不暴露系统异常、不用异常控制业务流。