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 兜底 + 日志 + 监控 不要暴露堆栈给客户端

核心原则

  1. 早失败:在方法入口快速校验参数,失败立即抛出
  2. 不吞异常:捕获的异常要么处理,要么转换后抛出
  3. 分层转换:技术异常 → 业务异常 → 用户友好响应
  4. 完整日志:异常必须带上下文(traceId、参数、错误码)
  5. 不可变优先:用不可变对象减少防御性代码

更多推荐