3. 分层设计总览:三层架构中的错误处理职责划分

在企业级业务系统中,错误处理不是散落在各处的补丁,而是贯穿 Controller(表现层)、Service(业务层)、Repository(数据层) 的骨架逻辑。每一层对错误的关注点、处理方式、返回形态都截然不同。只有明确划分职责,才能避免异常被吞没、重复包装或泄露内部细节。

3.1 三层职责速览

层次 核心关注点 错误处理职责 典型异常类型
Controller 请求解析、响应格式化 参数校验、异常翻译、统一响应封装 MethodArgumentNotValidException, HttpMessageNotReadableException
Service 业务规则、流程编排 业务异常抛出、事务回滚、领域校验 BizException, InsufficientBalanceException, OrderStateException
Repository 数据持久化、外部资源访问 数据异常包装、重试策略、连接异常转换 DataAccessException, OptimisticLockException, TimeoutException
核心原则:异常向上传递时,每层只添加本层语义的上下文,不修改异常类型,不吞没根因。Controller 是异常流的终点,负责将异常翻译为用户可读的响应。

3.2 分层错误处理架构图

下图展示了三层架构中异常从产生到响应的完整流动路径,以及每层的处理节点:

三层架构错误处理职责划分 三层架构错误处理职责划分 Controller 层 Service 层 Repository 层 参数校验 异常翻译器 统一响应封装 业务规则校验 业务异常抛出 事务回滚控制 数据访问异常 异常包装转换 重试 / 降级 异常向上传递:Repository → Service → Controller(最终翻译为用户响应)

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 层完成统一翻译。这套骨架让错误处理变得可预测、可测试、可维护。