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 异常分类的视觉结构
下图展示了三层异常体系的继承关系与关键字段:
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 异常分类与熔断降级的联动设计
三层异常分类直接决定了熔断器的行为策略。下图展示了异常类型与熔断决策的对应关系:
3.6 最佳实践清单
- 禁止异常膨胀:每个微服务内部,业务异常子类不超过 20 个,系统异常不超过 5 个,第三方异常按外部服务数量定义。
- 错误码枚举化:使用
enum定义错误码常量,避免魔法字符串散落在代码中。 - traceId 自动注入:在
BaseException构造函数中,通过MDC.get("traceId")自动填充,无需手动传入。 - 第三方异常必须携带 serviceName:熔断器按 serviceName 分组统计失败率,否则所有外部服务共享一个熔断阈值。
- 系统异常保留原始 cause:便于告警系统提取根因,避免在日志中丢失堆栈信息。
扩展思考:在 Spring Cloud 环境中,
ThirdPartyException 可以与 Sentinel 或 Resilience4j 的 RecordResultPredicate 结合,实现“按异常类型决定是否计入熔断统计”的精细化控制。