3. 自定义异常体系:业务异常、系统异常、第三方异常的分类与设计原则

在微服务架构中,异常不再是简单的“成功/失败”二元状态。一个请求可能穿越网关、业务服务、第三方 SDK、数据库连接池等多个边界。如果所有异常都使用 RuntimeException 或 Exception 抛出,调用方将无法区分“用户余额不足”与“数据库连接池耗尽”之间的本质区别,导致熔断策略误判、告警风暴、排查困难。

本章将建立一套三层异常分类体系,并给出每个层次的命名规范、继承结构、字段设计原则,以及如何在 Spring Boot 全局处理器中统一捕获与转换。

3.1 三层异常分类模型

根据异常来源与处理策略的不同,我们将异常划分为三个核心类别:

异常类别 来源 典型场景 处理策略 HTTP 状态码
业务异常 应用内部业务逻辑 余额不足、订单重复、权限不足 返回明确错误码 + 提示信息,不触发熔断 400 / 403 / 409
系统异常 应用基础设施或运行时 数据库连接失败、磁盘 IO 错误、OOM 触发告警 + 熔断降级,返回 500 500 / 503
第三方异常 外部服务或 SDK 调用 支付网关超时、短信服务拒绝、OpenFeign 调用失败 包装为可重试或可降级异常,触发熔断 502 / 504
设计原则:业务异常是“预期内的错误”,系统异常是“基础设施故障”,第三方异常是“外部依赖不可用”。三者必须使用不同的异常类,避免在 @ControllerAdvice 中通过字符串匹配或 instanceof 链式判断。

3.2 异常基类与继承结构

所有自定义异常继承自同一个抽象基类 BaseException,该基类包含三个核心字段:code(错误码)、message(用户可见提示)、traceId(链路追踪 ID)。

public abstract class BaseException extends RuntimeException {
    private final String code;
    private final String message;
    private String traceId;

    public BaseException(String code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }

    // getters 略
    public void setTraceId(String traceId) { this.traceId = traceId; }
}

三个子类分别对应三层分类:

// 业务异常:预期内的逻辑错误
public class BusinessException extends BaseException {
    public BusinessException(String code, String message) {
        super(code, message);
    }
}

// 系统异常:基础设施或运行时故障
public class SystemException extends BaseException {
    private final Throwable cause;

    public SystemException(String code, String message, Throwable cause) {
        super(code, message);
        this.cause = cause;
    }
}

// 第三方异常:外部服务调用失败
public class ThirdPartyException extends BaseException {
    private final String serviceName;   // 外部服务标识
    private final int httpStatus;       // 原始 HTTP 状态码

    public ThirdPartyException(String code, String message,
                               String serviceName, int httpStatus) {
        super(code, message);
        this.serviceName = serviceName;
        this.httpStatus = httpStatus;
    }
}
命名规范:错误码使用 BIZ_、SYS_、THD_ 前缀,例如 BIZ_BALANCE_INSUFFICIENT、SYS_DB_CONNECTION_LOST、THD_PAYMENT_TIMEOUT。避免使用纯数字错误码,提高可读性。

3.3 异常分类的视觉结构

下图展示了三层异常体系的继承关系与关键字段:

三层异常体系继承结构 BaseException (abstract) code : String | message : String | traceId : String BusinessException 无额外字段 预期内逻辑错误 SystemException cause : Throwable 基础设施故障 ThirdPartyException serviceName : String httpStatus : int 设计要点 • 基类必须为 abstract,禁止直接实例化 • 每个子类只携带与自身分类强相关的字段 • ThirdPartyException 必须记录 serviceName,用于熔断分组

3.4 全局异常处理器中的分类捕获

在 Spring Boot 的 @RestControllerAdvice 中,按照“业务 → 第三方 → 系统 → 兜底”的顺序定义处理器方法,确保每个异常落入正确的分类逻辑:

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 1. 业务异常:返回 400,不触发告警
    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleBusiness(BusinessException ex) {
        log.warn("业务异常: code={}, message={}", ex.getCode(), ex.getMessage());
        return new ErrorResponse(ex.getCode(), ex.getMessage());
    }

    // 2. 第三方异常:返回 502,记录服务名,触发熔断计数
    @ExceptionHandler(ThirdPartyException.class)
    @ResponseStatus(HttpStatus.BAD_GATEWAY)
    public ErrorResponse handleThirdParty(ThirdPartyException ex) {
        log.error("第三方异常: service={}, status={}, code={}",
                  ex.getServiceName(), ex.getHttpStatus(), ex.getCode());
        // 通知熔断器增加失败计数
        CircuitBreakerManager.recordFailure(ex.getServiceName());
        return new ErrorResponse(ex.getCode(), "外部服务暂不可用: " + ex.getServiceName());
    }

    // 3. 系统异常:返回 500,打印完整堆栈,触发告警
    @ExceptionHandler(SystemException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleSystem(SystemException ex) {
        log.error("系统异常: code={}, message={}", ex.getCode(), ex.getMessage(), ex.getCause());
        AlertManager.sendAlert(ex);
        return new ErrorResponse(ex.getCode(), "系统繁忙,请稍后重试");
    }

    // 4. 兜底:未知异常
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleUnknown(Exception ex) {
        log.error("未分类异常", ex);
        return new ErrorResponse("SYS_UNKNOWN", "服务器内部错误");
    }
}
避坑提示:不要将 BusinessException 的处理器放在 Exception 兜底之后,否则业务异常会被当作 500 处理。Spring 的 @ExceptionHandler 匹配顺序遵循“最具体优先”,但显式排序更安全。

3.5 异常分类与熔断降级的联动设计

三层异常分类直接决定了熔断器的行为策略。下图展示了异常类型与熔断决策的对应关系:

异常分类 → 熔断降级决策链路 异常发生 BusinessException ThirdPartyException SystemException 不触发熔断,返回提示 增加熔断计数,可重试 直接熔断,触发告警 核心原则:业务异常不熔断,第三方异常按服务分组熔断,系统异常全局熔断

3.6 最佳实践清单

  • 禁止异常膨胀:每个微服务内部,业务异常子类不超过 20 个,系统异常不超过 5 个,第三方异常按外部服务数量定义。
  • 错误码枚举化:使用 enum 定义错误码常量,避免魔法字符串散落在代码中。
  • traceId 自动注入:在 BaseException 构造函数中,通过 MDC.get("traceId") 自动填充,无需手动传入。
  • 第三方异常必须携带 serviceName:熔断器按 serviceName 分组统计失败率,否则所有外部服务共享一个熔断阈值。
  • 系统异常保留原始 cause:便于告警系统提取根因,避免在日志中丢失堆栈信息。
扩展思考:在 Spring Cloud 环境中,ThirdPartyException 可以与 Sentinel 或 Resilience4j 的 RecordResultPredicate 结合,实现“按异常类型决定是否计入熔断统计”的精细化控制。