2. 错误分类与分层:业务异常、系统异常、基础设施异常、分层原则与职责划分
在企业级系统中,错误并非铁板一块。将错误按性质与影响范围进行分层分类,是构建可观测、可治理的错误处理骨架的第一步。本章将定义三类核心错误,并明确各层在错误处理中的职责边界。
2.1 错误的三层分类模型
根据错误的来源与可控程度,我们将错误划分为三个层次:基础设施异常、系统异常与业务异常。三者构成一个从底层到顶层的金字塔结构。
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 异常传播与包装规则
异常在层间传播时,必须遵循以下三条硬规则:
- 向上包装,不吞没:下层异常必须被上层捕获并包装为当前层语义的异常,原始异常必须作为
cause保留。 - 边界转换,不泄露:在应用层与接入层的边界,所有非业务异常必须转换为统一的系统错误码,禁止将
NullPointerException、SQLException等实现细节暴露给客户端。 - 分级记录,不重复:每层只记录自己添加的上下文信息,底层异常由全局异常处理器统一记录完整堆栈。
代码示例:异常包装与传播
// 基础设施层:将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 本章小结
- 错误分为基础设施异常、系统异常、业务异常三层,每层有明确的来源、可恢复性和处理策略。
- 分层原则:下层向上包装、边界转换、分级记录,确保异常上下文不丢失、实现细节不泄露。
- 职责划分:接入层负责统一响应,应用层负责业务判断与重试降级,领域层负责规则校验,基础设施层负责资源访问与异常包装。
- 避免反模式:不重复记录日志、不暴露系统异常、不用异常控制业务流。