3. 分层设计总览:三层架构中的错误处理职责划分
在企业级业务系统中,错误处理不是散落在各处的补丁,而是贯穿 Controller(表现层)、Service(业务层)、Repository(数据层) 的骨架逻辑。每一层对错误的关注点、处理方式、返回形态都截然不同。只有明确划分职责,才能避免异常被吞没、重复包装或泄露内部细节。
3.1 三层职责速览
| 层次 | 核心关注点 | 错误处理职责 | 典型异常类型 |
|---|---|---|---|
| Controller | 请求解析、响应格式化 | 参数校验、异常翻译、统一响应封装 | MethodArgumentNotValidException, HttpMessageNotReadableException |
| Service | 业务规则、流程编排 | 业务异常抛出、事务回滚、领域校验 | BizException, InsufficientBalanceException, OrderStateException |
| Repository | 数据持久化、外部资源访问 | 数据异常包装、重试策略、连接异常转换 | DataAccessException, OptimisticLockException, TimeoutException |
核心原则:异常向上传递时,每层只添加本层语义的上下文,不修改异常类型,不吞没根因。Controller 是异常流的终点,负责将异常翻译为用户可读的响应。
3.2 分层错误处理架构图
下图展示了三层架构中异常从产生到响应的完整流动路径,以及每层的处理节点:
3.3 各层详细职责
3.3.1 Controller 层:校验与翻译
- 参数校验:使用 JSR-380 Bean Validation(如
@Valid、@NotBlank)在请求到达业务逻辑前拦截格式错误。 - 异常翻译:通过
@ControllerAdvice或@ExceptionHandler将底层异常转换为标准错误响应。 - 响应封装:所有返回(成功/失败)统一使用
Result<T>结构,包含 code、message、data 字段。
// Controller 层异常处理示例
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.error(ErrorCode.PARAM_INVALID, msg);
}
@ExceptionHandler(BizException.class)
public Result<Void> handleBiz(BizException ex) {
return Result.error(ex.getCode(), ex.getMessage());
}
}
3.3.2 Service 层:业务异常与事务
- 业务规则校验:校验订单状态、账户余额、库存数量等不可由参数校验覆盖的领域规则。
- 业务异常抛出:校验失败时抛出
BizException(或子类),携带业务错误码和提示信息。 - 事务边界:使用
@Transactional确保异常时数据回滚,避免脏数据。
// Service 层业务异常抛出示例
@Service
public class OrderService {
public void createOrder(CreateOrderRequest request) {
// 业务规则校验
if (request.getAmount() <= 0) {
throw new BizException(ErrorCode.INVALID_AMOUNT, "订单金额必须大于0");
}
UserAccount account = accountRepository.findById(request.getUserId())
.orElseThrow(() -> new BizException(ErrorCode.USER_NOT_FOUND, "用户不存在"));
if (account.getBalance() < request.getAmount()) {
throw new BizException(ErrorCode.INSUFFICIENT_BALANCE, "余额不足");
}
// 业务逻辑...
}
}
3.3.3 Repository 层:数据异常与重试
- 数据访问异常:捕获数据库连接失败、唯一键冲突、乐观锁异常等底层异常。
- 异常包装转换:将
DataIntegrityViolationException等转换为业务可理解的异常(如DuplicateKeyException)。 - 重试与降级:对网络抖动、临时锁等待等场景配置重试策略(如 Spring Retry)。
// Repository 层异常包装示例
@Repository
public class OrderRepositoryImpl implements OrderRepository {
@Override
public Order save(Order order) {
try {
return jpaRepository.save(order);
} catch (DataIntegrityViolationException e) {
// 唯一键冲突转换为业务异常
throw new BizException(ErrorCode.DUPLICATE_ORDER, "订单号重复", e);
} catch (OptimisticLockException e) {
// 乐观锁异常,可触发重试
throw new BizException(ErrorCode.CONCURRENT_MODIFICATION, "数据已被修改,请重试", e);
}
}
}
设计要点:Repository 层不应抛出原始 JDBC 或 ORM 异常,必须包装为业务异常或自定义数据异常,避免 SQL 细节泄露到上层。
3.4 异常流动对比表
| 场景 | 异常产生层 | 处理层 | 最终响应 |
|---|---|---|---|
| 请求参数缺失 | Controller | Controller | 400 PARAM_INVALID |
| 用户余额不足 | Service | Controller | 400 INSUFFICIENT_BALANCE |
| 数据库唯一键冲突 | Repository | Repository → Service | 409 DUPLICATE_KEY |
| 乐观锁更新失败 | Repository | Repository(重试) | 重试成功或 409 |
| 外部服务超时 | Repository | Repository(降级) | 503 SERVICE_UNAVAILABLE |
3.5 分层设计避坑清单
常见错误:
- ❌ Controller 层直接捕获 SQLException 并返回原始错误信息(泄露数据库结构)。
- ❌ Service 层捕获所有异常后返回 null 或空对象(导致调用方无法区分成功与失败)。
- ❌ Repository 层抛出 RuntimeException 但不包装(上层无法识别业务语义)。
- ❌ 所有层都使用 try-catch 打印日志后重新抛出(导致日志重复、堆栈丢失)。
分层错误处理的核心在于 各司其职:Repository 关注数据完整性,Service 关注业务规则,Controller 关注用户体验。异常在层间传递时只增加上下文,不改变本质,最终在 Controller 层完成统一翻译。这套骨架让错误处理变得可预测、可测试、可维护。