异常处理是软件工程中最重要的基础能力之一。良好的异常处理机制能够显著提升服务的稳定性、可维护性和用户体验。本文档从设计原则、分类体系、实现规范到最佳实践,提供一套完整的异常处理指南。
异常处理的根本目的在于:
核心原则:异常处理不是"掩盖问题",而是"优雅地处理问题"。
| 维度 | 业务异常 | 系统异常 |
|---|---|---|
| 产生原因 | 违反业务规则 | 系统运行故障 |
| 用户感知 | 需要明确告知用户 | 原则上不让用户感知 |
| 处理方式 | 返回明确的错误码和提示 | 记录日志、触发告警、自动恢复 |
| 示例 | 余额不足、参数校验失败 | 数据库连接断开、内存溢出 |
关键决策:只将具有业务含义的异常暴露给用户。系统自身的问题,原则上不要让用户有感知。
反模式警示:不要使用异常处理机制处理正常情况下就会出现的业务情况。
❌ 错误示例:
try {
withdraw(account, amount);
} catch (InsufficientBalanceException e) {
// 余额不足是正常业务场景,不应该用异常处理
return Result.fail("余额不足");
}
✅ 正确做法:
BalanceCheckResult result = account.canWithdraw(amount);
if (!result.isAllowed()) {
return Result.fail(result.getReason()); // 正常业务流程分支
}
withdraw(account, amount);
判断标准:如果该"异常"在业务上预期会发生(如余额不足、库存不足),则不应使用异常机制处理。
异常情况的检查与发现,应与正常业务流程分离。这体现了**关注点分离(Separation of Concerns)**的设计原则。
✅ 推荐做法:
一但发现不太符合当下场景的情况,直接终止当前方法的执行,并抛出异常。
好处:
❌ 延迟失败的危害:
public void processOrder(Order order) {
validateOrder(order); // 如果这里不抛异常...
saveToDatabase(order); // ...数据可能已经部分写入
sendNotification(order); // ...通知可能已经发出
// 最后才发现问题,回滚成本极高
}
"业务异常"常常被用来泛指所有非正向主流程的业务场景。但是这些场景的处理,在实现上,是否需要异常机制处理这些场景,需要谨慎判断,避免出现使用异常机制处理正常业务的情况。
典型案例分析:
案例:提现余额不足
对于"提现"操作,账户余额不足时,会提现失败。这种场景,一般并不建议使用异常来处理,而建议把这种场景做为常规业务流程中的一种。即使使用异常机制来处理,也应当与系统异常严格区分出来。
业务异常的核心要求:
一个业务异常需要达成如下几个要求:
业务异常的实现形式:
业务异常通常表现为自定义的异常类(因为需要携带自定义的信息及异常发生的上下文):
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
private final Map<String, Object> context;
public BusinessException(ErrorCode errorCode, String message, Map<String, Object> context) {
super(message);
this.errorCode = errorCode;
this.context = context;
}
// Getters...
}
技术异常可以分为两类:
可预见的技术异常应当在系统设计时,通过预先设计的异常处理机制进行规避。避免异常的发生。
常见可预见异常:
| 异常类型 | 预防措施 | 监控告警 |
|---|---|---|
| 磁盘满 | 磁盘容量监控、日志轮转 | 磁盘使用率 > 80% |
| FileDescriptor 用尽 | 连接池配置、资源限制 | FD 使用率监控 |
| 网络中断 | 重试机制、熔断降级 | 网络连通性监控 |
| 第三方服务不可用 | 熔断器、降级策略 | 服务健康检查 |
处理原则:
不可预见的异常,应当能在服务内,以统一的方式全局地进行处理,避免巴洛克式异常处理(即,每个地方都做自己的异常处理,每个地方的异常处理方式又非常相似。)
常见不可预见异常:
NullPointerExceptionIllegalArgumentExceptionIndexOutOfBoundsException解决策略:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnknownException(Exception ex, WebRequest request) {
// 1. 记录详细日志
log.error("Unknown exception occurred", ex);
// 2. 发送告警通知
alertService.sendAlert("UNKNOWN_EXCEPTION", ex.getMessage());
// 3. 返回用户友好的错误信息(不暴露技术细节)
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("SYSTEM_ERROR", "系统繁忙,请稍后重试"));
}
}
自定义异常尽量从问题本身的性质出发定义异常,避免以具体场景、发送方、处理方的不同而定义不同的异常类型,以避免异常数量的膨胀。
❌ 反模式:按场景定义异常
// 异常数量膨胀,难以管理
public class WithdrawInsufficientBalanceException extends Exception {}
public class TransferInsufficientBalanceException extends Exception {}
public class PaymentInsufficientBalanceException extends Exception {}
✅ 正确做法:按性质定义异常
// 统一的余额不足异常,通过上下文区分场景
public class InsufficientBalanceException extends BusinessException {
public InsufficientBalanceException(String operationType, BigDecimal required, BigDecimal available) {
super(ErrorCode.INSUFFICIENT_BALANCE,
String.format("%s 操作需要 %s,当前可用余额 %s", operationType, required, available),
Map.of("operation", operationType, "required", required, "available", available));
}
}
异常命名规范:
| 命名模式 | 示例 | 适用场景 |
|---|---|---|
[性质]Exception |
ValidationException |
通用异常 |
[性质]Error |
ConfigurationError |
严重错误 |
[动作][性质]Exception |
PaymentTimeoutException |
特定动作异常 |
异常传递应遵循以下原则:
new Exception("msg", original))✅ 正确的异常转换:
public void createOrder(OrderRequest request) {
try {
database.save(order);
} catch (SQLException e) {
// 转换异常,增加业务上下文
throw new OrderCreationException("订单创建失败,订单号: " + request.getOrderNo(), e);
}
}
❌ 错误的异常处理:
try {
// 某些操作
} catch (Exception e) {
// ❌ 吞没异常,问题被隐藏
}
try {
// 某些操作
} catch (Exception e) {
throw new RuntimeException("出错了"); // ❌ 丢失原始堆栈
}
异常日志是排查问题的重要依据,应遵循以下规范:
必须记录的信息:
日志级别选择:
| 异常类型 | 日志级别 | 说明 |
|---|---|---|
| 业务异常(预期内) | WARN | 用户操作导致的异常 |
| 可预见的系统异常 | ERROR | 系统问题,需要关注 |
| 不可预见的异常 | ERROR + 告警 | 可能代表 Bug,需要立即处理 |
日志格式示例:
[2024-01-15 10:23:45] [ERROR] [requestId=abc123] [userId=10086]
Payment failed for order: ORDER_20240115001
Exception: InsufficientBalanceException: 支付需要 1000.00,当前余额 500.00
at com.example.PaymentService.process(PaymentService.java:45)
...
异常处理与事务管理密切相关:
规则:
Spring 示例:
@Transactional(rollbackFor = BusinessException.class, noRollbackFor = PartialSuccessException.class)
public void processOrder(Order order) {
// 业务逻辑
}
在设计异常体系时,检查以下要点:
错误码是异常体系的重要组成部分:
推荐格式:[系统标识]_[模块]_[错误类型]_[序号]
示例:
PAY_001_001 —— 支付系统-账户模块-余额不足ORD_002_003 —— 订单系统-创建模块-商品已下架错误码分类:
| 类型 | 范围 | 说明 |
|---|---|---|
| 系统级错误 | SYS_xxx |
基础设施问题 |
| 业务级错误 | [业务]_xxx |
业务规则违反 |
| 参数错误 | PARAM_xxx |
输入校验失败 |
| 权限错误 | AUTH_xxx |
认证授权问题 |
在微服务架构中,异常需要在服务间传递:
策略:
示例:
{
"success": false,
"errorCode": "PAY_001_001",
"errorMessage": "余额不足",
"traceId": "abc123-def456",
"timestamp": "2024-01-15T10:23:45Z",
"details": {
"required": 1000.00,
"available": 500.00
}
}
建立完善的异常监控体系:
监控维度:
告警策略:
受检异常 vs 非受检异常:
推荐模式:
// 使用 Optional 避免 NullPointerException
Optional<User> user = userRepository.findById(id);
// 使用 Result 类型处理业务失败
Result<Order> result = orderService.create(request);
if (result.isFailure()) {
return ResponseEntity.badRequest().body(result.getError());
}
EAFP vs LBYL:
自定义异常:
class BusinessError(Exception):
def __init__(self, code: str, message: str, context: dict = None):
self.code = code
self.context = context or {}
super().__init__(message)
# 使用
raise BusinessError("PAY_001_001", "余额不足", {"required": 1000, "available": 500})
Go 没有异常机制,使用 error 返回值:
规范:
fmt.Errorf("...: %w", err) 包装错误var ErrInsufficientBalance = errors.New("余额不足")
func Withdraw(account string, amount decimal.Decimal) error {
balance, err := getBalance(account)
if err != nil {
return fmt.Errorf("查询余额失败: %w", err)
}
if balance.LessThan(amount) {
return fmt.Errorf("%w: 需要 %s, 可用 %s", ErrInsufficientBalance, amount, balance)
}
// ...
}
良好的异常处理是系统稳定性的基石。核心要点:
记住:异常处理的目标不是"消灭异常",而是"优雅地处理异常"。一个从不抛出异常的系统,可能是一个从不检查错误的系统。