Java 异常处理的分层策略与错误码设计规范
·
Java 异常处理的分层策略与错误码设计规范
标签:Java | 异常处理 | 防御性编程 | 错误码 | 工程规范 | 高级
系列:Java 进阶修炼指南 · 第一周:Spring 生态深度解析
难度:★★★☆☆
一、问题引入
// 你见过这样的代码吗?
try {
processOrder(order);
} catch (Exception e) {
// 什么异常都吞掉,日志也没有
}
或者这样的:
// 错误码随意返回,调用方完全不知道发生了什么
public int createOrder(Order order) {
if (order == null) return -1;
if (order.getAmount() <= 0) return -2;
if (order.getUserId() == null) return -3;
// ... 到底 -7 是什么错误?查文档去吧
return -7;
}
异常处理是 Java 开发中最容易写出"烂代码"的地方。本文从阿里巴巴、Google 等一线大厂的工程实践出发,系统讲解 Java 异常处理的分层策略和错误码设计规范。
二、Java 异常体系的正确认知
2.1 异常类图
Throwable
├── Error(严重系统错误,不应捕获)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── NoClassDefFoundError
│
└── Exception(程序可处理的异常)
├── RuntimeException(非受检异常 - Unchecked)
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ ├── IllegalStateException
│ └── UnsupportedOperationException
│
└── 其他 Exception(受检异常 - Checked)
├── IOException
├── SQLException
└── ClassNotFoundException
2.2 受检异常 vs 非受检异常的选择原则
| 场景 | 异常类型 | 理由 |
|---|---|---|
| 调用方可以合理恢复 | 受检异常 | 强制调用方处理 |
| 编程错误(NPE/参数错误) | 非受检异常 | 修复代码,无法恢复 |
| 框架内部异常转换 | 非受检异常 | 避免污染业务代码签名 |
| IO/网络可重试操作 | 受检异常 | 调用方决定是否重试 |
Effective Java 建议:优先使用非受检异常,避免受检异常过度膨胀方法签名。
三、异常处理的分层策略
3.1 分层架构中的异常职责
┌─────────────────────────────────────┐
│ Controller 层 │ ← 异常转换:→ 统一响应格式
│ (处理 HTTP 请求/参数校验) │ 业务异常 → ErrorCode
├─────────────────────────────────────┤ 系统异常 → 500
│ Service 层 │ ← 业务异常抛出:BizException
│ (业务逻辑/事务管理) │ 参数非法 → IllegalArgument
├─────────────────────────────────────┤
│ Manager/Domain 层 │ ← 领域异常:DomainException
│ (领域模型/核心业务规则) │ 业务规则违反
├─────────────────────────────────────┤
│ Repository/DAO 层 │ ← 数据访问异常转换
│ (数据库/缓存/外部服务) │ SQLException → DataAccessException
├─────────────────────────────────────┤
│ Infrastructure 层 │ ← 原始异常:IOException, SQLException
│ (第三方 SDK/HTTP 调用) │
└─────────────────────────────────────┘
3.2 各层的异常处理规范
Controller 层:统一异常拦截
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 业务异常 → 结构化错误响应
@ExceptionHandler(BizException.class)
public ResponseEntity<ErrorResponse> handleBizException(BizException e) {
log.warn("Business error: [{}] {}", e.getErrorCode(), e.getMessage());
ErrorResponse response = new ErrorResponse(e.getErrorCode(), e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
// 参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest()
.body(new ErrorResponse("PARAM_INVALID", message));
}
// 系统异常 → 5xx,不暴露细节
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("System error", e);
String traceId = MDC.get("traceId");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("SYSTEM_ERROR",
"系统繁忙,请稍后重试(traceId: " + traceId + ")"));
}
}
Service 层:业务异常定义
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
@Transactional
public Order createOrder(CreateOrderRequest request) {
// 参数校验(防御性编程:不信任任何输入)
Assert.notNull(request, "订单请求不能为空");
Assert.notNull(request.getUserId(), "用户ID不能为空");
Assert.isTrue(request.getAmount() != null && request.getAmount().compareTo(BigDecimal.ZERO) > 0,
"订单金额必须大于0");
// 库存检查
boolean sufficient = inventoryService.checkAndDeduct(request.getSkuId(), request.getQuantity());
if (!sufficient) {
// 业务异常:调用方可以明确处理
throw new BizException(ErrorCode.INVENTORY_INSUFFICIENT,
String.format("商品[%s]库存不足,需求:%d", request.getSkuId(), request.getQuantity()));
}
// 订单幂等检查
Order existingOrder = orderRepository.findByIdempotentKey(request.getIdempotentKey());
if (existingOrder != null) {
// 返回已有订单(幂等)
return existingOrder;
}
// 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setAmount(request.getAmount());
order.setStatus(OrderStatus.CREATED);
try {
orderRepository.save(order);
} catch (DuplicateKeyException e) {
// 数据层异常 → 转换为业务异常
throw new BizException(ErrorCode.ORDER_DUPLICATE, "订单重复提交");
}
return order;
}
}
Repository 层:异常转换
@Repository
@RequiredArgsConstructor
public class OrderRepositoryImpl implements OrderRepository {
private final OrderMapper orderMapper;
@Override
public void save(Order order) {
try {
orderMapper.insert(order);
} catch (DataIntegrityViolationException e) {
// 数据库约束违反 → 转换为可理解的业务异常
throw new DuplicateKeyException("订单已存在: " + order.getId(), e);
} catch (SQLException e) {
// SQL 异常 → 转换为数据访问异常
throw new DataAccessException("数据库操作失败", e);
}
}
}
四、错误码设计规范(阿里巴巴规范实践)
4.1 错误码结构设计
public interface ErrorCode {
String getCode(); // 错误码
String getMessage(); // 默认错误信息
int getHttpStatus(); // HTTP 状态码
}
public enum SystemErrorCode implements ErrorCode {
// 成功
SUCCESS("00000", "success", 200),
// 一级宏观错误码:A 用户端错误
PARAM_INVALID("A0001", "用户请求参数错误", 400),
PARAM_MISSING("A0002", "缺少必填参数", 400),
PARAM_TYPE_ERROR("A0003", "参数类型错误", 400),
USER_NOT_FOUND("A0100", "用户不存在", 404),
USER_ALREADY_EXISTS("A0101", "用户已存在", 409),
USER_PASSWORD_ERROR("A0102", "用户名或密码错误", 401),
USER_NO_PERMISSION("A0103", "用户无权限", 403),
ORDER_NOT_FOUND("A0200", "订单不存在", 404),
ORDER_STATUS_ERROR("A0201", "订单状态不正确", 400),
ORDER_ALREADY_PAID("A0202", "订单已支付", 409),
INVENTORY_INSUFFICIENT("A0203", "库存不足", 400),
// 一级宏观错误码:B 系统错误
SYSTEM_ERROR("B0001", "系统执行出错", 500),
SYSTEM_TIMEOUT("B0002", "系统执行超时", 504),
SYSTEM_RESOURCE_EXHAUSTED("B0003", "系统资源耗尽", 503),
// 一级宏观错误码:C 第三方服务错误
SERVICE_CALL_ERROR("C0001", "调用第三方服务出错", 502),
SERVICE_RESPONSE_ERROR("C0002", "第三方服务响应异常", 502),
SERVICE_TIMEOUT("C0003", "调用第三方服务超时", 504),
PAYMENT_FAILED("C0300", "支付失败", 400),
SMS_SEND_FAILED("C0400", "短信发送失败", 400);
private final String code;
private final String message;
private final int httpStatus;
SystemErrorCode(String code, String message, int httpStatus) {
this.code = code;
this.message = message;
this.httpStatus = httpStatus;
}
@Override public String getCode() { return code; }
@Override public String getMessage() { return message; }
@Override public int getHttpStatus() { return httpStatus; }
}
4.2 错误码设计原则
| 原则 | 说明 | 示例 |
|---|---|---|
| 有序递增 | 按业务域分组,预留空间 | A01xx 用户,A02xx 订单,A03xx 商品 |
| 宏观分类 | A=用户端, B=系统, C=第三方 | 快速定位问题归属 |
| 不暴露细节 | 系统异常隐藏内部信息 | 不返回堆栈给客户端 |
| 可追溯 | 错误码 + traceId 关联日志 | 快速定位问题 |
4.3 统一响应结构
@Data
public class ApiResponse<T> {
private boolean success;
private String code; // 业务错误码
private String message; // 用户可读信息
private T data; // 业务数据
private String traceId; // 追踪ID
private long timestamp;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(true);
response.setCode("00000");
response.setMessage("success");
response.setData(data);
response.setTraceId(MDC.get("traceId"));
response.setTimestamp(System.currentTimeMillis());
return response;
}
public static <T> ApiResponse<T> error(ErrorCode errorCode) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(false);
response.setCode(errorCode.getCode());
response.setMessage(errorCode.getMessage());
response.setTraceId(MDC.get("traceId"));
response.setTimestamp(System.currentTimeMillis());
return response;
}
}
五、防御性编程的 7 条铁律
5.1 铁律清单
@Service
public class DefensiveProgrammingDemo {
// 1. 不信任任何输入(NULL 检查 + 范围校验)
public void rule1(String userId, Integer age, BigDecimal amount) {
Objects.requireNonNull(userId, "userId must not be null");
if (age == null || age < 0 || age > 150) {
throw new IllegalArgumentException("age must be between 0 and 150");
}
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("amount must be positive");
}
}
// 2. 防御性复制(防止外部修改内部状态)
public class User {
private final List<String> tags;
public User(List<String> tags) {
// 防御性复制,防止外部 List 被修改
this.tags = new ArrayList<>(Objects.requireNonNull(tags));
}
public List<String> getTags() {
// 返回不可修改视图或副本
return Collections.unmodifiableList(tags);
}
}
// 3. 断路保护(防止级联故障)
@CircuitBreaker(name = "paymentService", fallbackMethod = "paymentFallback")
public void processPayment(String orderId) {
paymentService.charge(orderId);
}
private void paymentFallback(String orderId, Exception e) {
log.error("Payment failed for order: {}", orderId, e);
// 记录待处理,异步补偿
paymentRetryQueue.enqueue(orderId);
}
// 4. 资源必须关闭(try-with-resources)
public void rule4(String filePath) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(filePath), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
process(line);
}
} catch (IOException e) {
throw new BizException(ErrorCode.SYSTEM_ERROR, "文件读取失败: " + filePath);
}
}
// 5. 并发安全(不可变对象优先)
public class ImmutableConfig {
private final String appName;
private final int maxConnections;
private final List<String> whiteList;
public ImmutableConfig(String appName, int maxConnections, List<String> whiteList) {
this.appName = appName;
this.maxConnections = maxConnections;
this.whiteList = List.copyOf(whiteList); // JDK10+ 不可变拷贝
}
// 只有 getter,无 setter → 天然线程安全
}
// 6. 日志记录完整上下文
public void rule6(String userId, Order order) {
try {
processOrder(order);
} catch (BizException e) {
// 记录完整上下文,便于排查
log.error("Order processing failed. userId={}, orderId={}, amount={}, errorCode={}",
userId, order.getId(), order.getAmount(), e.getErrorCode(), e);
throw e;
}
}
// 7. 断言与前置条件(Guava Preconditions)
public void rule7(String email, int pageSize) {
Preconditions.checkArgument(email != null && email.contains("@"),
"Invalid email format: %s", email);
Preconditions.checkArgument(pageSize > 0 && pageSize <= 100,
"pageSize must be in range [1, 100], got: %s", pageSize);
}
}
六、总结
| 层次 | 职责 | 禁止事项 |
|---|---|---|
| Controller | 异常转换 → 统一响应 | 不要写业务逻辑 |
| Service | 业务校验 → 抛业务异常 | 不要捕获所有异常吞掉 |
| Repository | 技术异常 → 业务异常转换 | 不要让 SQLException 上浮 |
| GlobalHandler | 兜底 + 日志 + 监控 | 不要暴露堆栈给客户端 |
核心原则:
- 早失败:在方法入口快速校验参数,失败立即抛出
- 不吞异常:捕获的异常要么处理,要么转换后抛出
- 分层转换:技术异常 → 业务异常 → 用户友好响应
- 完整日志:异常必须带上下文(traceId、参数、错误码)
- 不可变优先:用不可变对象减少防御性代码
更多推荐
所有评论(0)