--- title: "01-异常处理最佳实践指南" created: 2025-12-01 tags: - 项目 aliases: - 异常处理最佳实践指南 --- # 异常处理最佳实践指南 ## 核心设计理念 ### 三层异常分类 ![[三层异常分类-aaf0cb88.jpg]] ### 错误类型区分 | 错误类型 | 状态码 | 处理方式 | 返回内容 | | --- | --- | --- | --- | | **CLIENT** | 4xxxx | 返回原样 | message + description | | **SERVER** | 5xxxx | 隐藏敏感信息 | 通用消息 + Trace ID | | **SUCCESS** | 0 | 正常返回 | 成功消息 + 数据 | --- ## 架构概览 ### 类设计关系图 ```text ErrorCode (枚举) ├─ code: int ├─ message: String ├─ description: String └─ type: ErrorType ├─ CLIENT ├─ SERVER └─ SUCCESS ↓ 用于构造 BusinessException (异常) ├─ code: int ├─ message: String ├─ description: String └─ errorType: ErrorType ↓ 捕获并处理 GlobalExceptionHandler (处理器) └─ 生成 BaseResponse BaseResponse (响应) ├─ code: int ├─ message: String ├─ description: String ├─ data: T ├─ timestamp: long └─ traceId: String ``` ### 响应格式示例 **成功响应** ```json { "code": 0, "message": "success", "description": null, "data": { "id": 1, "username": "john" }, "timestamp": 1704067200000, "traceId": null } ``` **客户端错误响应** ```json { "code": 10001, "message": "user not found", "description": "User with id 999 does not exist", "data": null, "timestamp": 1704067200000, "traceId": null } ``` **服务器错误响应** ```json { "code": 50000, "message": "System error, please contact support", "description": "系统错误,请联系管理员", "data": null, "timestamp": 1704067200000, "traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } ``` --- ## 各模块详解 ### 1. ErrorCode 枚举设计 #### 编码规范 ```text 0 → 成功 40000 → 客户端基础错误(4开头) 50000 → 服务器基础错误(5开头) 1xxxx → 用户模块错误 2xxxx → 作品模块错误 3xxxx → 销售模块错误 ``` #### 为什么要加 description 字段 ```java // ❌ 不够好 throw new BusinessException(ErrorCode.USER_NOT_FOUND); // 响应:{"code": 10001, "message": "user not found"} // 问题:用户不知道是哪个用户,前端无法给出有意义的提示 // ✅ 更好 throw new BusinessException(ErrorCode.USER_NOT_FOUND, "User with id 999 does not exist"); // 响应:{"code": 10001, "message": "user not found", "description": "User with id 999 does not exist"} // 优点:提供详细信息,前端可以显示具体的原因 ``` #### ErrorType 的作用 ```java // CLIENT 错误:直接返回给客户端 ErrorCode.USER_NOT_FOUND(10001, "user not found", "...", ErrorType.CLIENT) // → 响应中包含 description,前端可以展示给用户 // SERVER 错误:隐藏敏感信息,返回 Trace ID ErrorCode.DATABASE_ERROR(50100, "database error", "...", ErrorType.SERVER) // → 响应中隐藏真实错误,只返回 Trace ID,用于后台追踪 ``` ### 2. BusinessException 异常类 #### 构造方式对比 ```java // 方式1:仅使用错误码(推荐简单场景) throw new BusinessException(ErrorCode.USER_NOT_FOUND); // 方式2:错误码 + 自定义描述(推荐大多数场景) throw new BusinessException( ErrorCode.USER_NOT_FOUND, "User with id " + userId + " does not exist" ); // 方式3:完整构造(通常不需要直接使用) throw new BusinessException( "user not found", 10001, "User with id " + userId + " does not exist", ErrorType.CLIENT ); ``` #### 为什么要继承 RuntimeException ```java RuntimeException ├─ 优点1:无需在方法签名中声明 throws ├─ 优点2:能被 @Transactional 正确识别进行回滚 ├─ 优点3:支持链式处理 └─ 缺点:需要程序员显式处理(通过异常处理器) ``` ### 3. ThrowUtils 工具类 #### 快速异常抛出 ```java // 场景1:参数校验 Long userId = 10L; ThrowUtils.throwIf(userId == null || userId <= 0, ErrorCode.PARAMS_ERROR); // 场景2:对象为null User user = userMapper.selectById(userId); ThrowUtils.throwIfNull(user, ErrorCode.USER_NOT_FOUND); // 场景3:字符串为空 String email = getUserInput(); ThrowUtils.throwIfBlank(email, ErrorCode.PARAMS_ERROR); // 场景4:集合为空 List users = userMapper.selectByIds(ids); ThrowUtils.throwIfEmpty(users, ErrorCode.NOT_FOUND); // 场景5:业务条件检查 int price = 50; ThrowUtils.throwIf(price < 100, ErrorCode.BID_PRICE_TOO_LOW, "Minimum price is 100"); // 场景6:复杂逻辑 User user = userMapper.selectById(userId); ThrowUtils.throwIfNull(user, ErrorCode.USER_NOT_FOUND); ThrowUtils.throwIf(!user.isActive(), ErrorCode.FORBIDDEN, "User account is inactive"); ``` #### 为什么使用 ThrowUtils ```java // ❌ 传统写法(冗长) User user = userMapper.selectById(userId); if (user == null) { throw new BusinessException(ErrorCode.USER_NOT_FOUND); } // ✅ 使用 ThrowUtils(简洁) User user = ThrowUtils.throwIfNull( userMapper.selectById(userId), ErrorCode.USER_NOT_FOUND ); // 优点: // 1. 代码更简洁,减少 if-then-throw 样板代码 // 2. 返回原对象,支持链式调用 // 3. 增强代码可读性 // 4. 检查和异常处理合二为一 ``` ### 4. ResultUtils 响应工具 #### 响应构造示例 ```java // 成功响应(带数据) return ResultUtils.success(userData); // → {"code": 0, "message": "success", "data": {...}, "timestamp": ...} // 成功响应(无数据) return ResultUtils.success(); // → {"code": 0, "message": "success", "data": null, "timestamp": ...} // 错误响应(使用错误码) return ResultUtils.error(ErrorCode.USER_NOT_FOUND); // → {"code": 10001, "message": "user not found", "description": "...", "timestamp": ...} // 错误响应(自定义描述) return ResultUtils.error(ErrorCode.USER_NOT_FOUND, "User with id 999 not found"); // → {"code": 10001, "message": "user not found", "description": "User with id 999 not found", "timestamp": ...} // 服务器错误响应 return ResultUtils.serverError(traceId, "Database connection failed"); // → {"code": 50000, "message": "System error, please contact support", "traceId": "...", "timestamp": ...} ``` #### @JsonInclude 的作用 ```java @JsonInclude(JsonInclude.Include.NON_NULL) public class BaseResponse { ... } // 作用:序列化时忽略 null 值字段 // 成功响应不会包含 traceId 字段(为 null) // 服务器错误响应不会包含 data 字段(为 null) // 结果:响应体更小,网络传输更快 ``` ### 5. GlobalExceptionHandler 处理器 #### 异常处理流程 ![[异常处理流程-ff80fb01.jpg]] #### Trace ID 的作用 ```text 客户端收到错误响应: { "code": 50000, "message": "System error, please contact support", "traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } 用户:我收到错误了,Trace ID 是 a1b2c3d4... 后台团队: 1. 在日志系统中搜索 Trace ID 2. 找到该 Trace ID 对应的所有日志 3. 快速定位问题原因 4. 反馈给用户 ``` #### 日志记录示例 ```text 业务异常日志(INFO 级别): 2024-01-01 10:00:00 [http-nio-8080-exec-1] WARN GlobalExceptionHandler - Business exception at [GET] /api/user/999 - [10001] BusinessException [code=10001, message=user not found, description=User with id 999 does not exist, type=CLIENT] 系统异常日志(ERROR 级别): 2024-01-01 10:00:00 [http-nio-8080-exec-1] ERROR GlobalExceptionHandler - Unexpected exception [NullPointerException] at [POST] /api/user - UserService.java:45#createUser() - IP: 192.168.1.100 - Trace: a1b2c3d4... - java.lang.NullPointerException: Cannot invoke method on null object at com.example.user.service.impl.UserServiceImpl.createUser(UserService.java:45) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ... ``` --- ## 使用场景 ### 场景1:参数校验 ```java @PostMapping("/users") public BaseResponse createUser(@RequestBody @Valid CreateUserRequest request) { // @Valid 触发参数验证 // 如果验证失败,自动被 handleValidationException 处理 // 无需手动处理 UserDTO user = userService.createUser(request); return ResultUtils.success(user); } // DTO 中使用 JSR303 注解 public class CreateUserRequest { @NotBlank(message = "Username cannot be empty") private String username; @NotBlank(message = "Password cannot be empty") private String password; @Email(message = "Invalid email format") private String email; } ``` ### 场景2:业务规则检查 ```java public void updateUserEmail(Long userId, String newEmail) { // 检查用户是否存在 User user = userMapper.selectById(userId); ThrowUtils.throwIfNull(user, ErrorCode.USER_NOT_FOUND, "User with id " + userId + " not found"); // 检查邮箱是否已被使用 User existing = userMapper.selectByEmail(newEmail); ThrowUtils.throwIf( existing != null && !existing.getId().equals(userId), ErrorCode.EMAIL_ALREADY_EXISTS, "Email '" + newEmail + "' is already in use" ); user.setEmail(newEmail); userMapper.updateById(user); } ``` ### 场景3:外部服务调用失败 ```java public void uploadArtwork(Artwork artwork) { try { // 调用文件服务上传 fileService.upload(artwork.getFile()); } catch (Exception e) { log.error("File upload failed for artwork {}", artwork.getId(), e); throw new BusinessException( ErrorCode.ARTWORK_UPLOAD_FAILED, "Failed to upload file: " + e.getMessage() ); } } ``` ### 场景4:数据库操作失败 ```java public void deleteUser(Long userId) { try { int result = userMapper.deleteById(userId); ThrowUtils.throwIf(result == 0, ErrorCode.USER_NOT_FOUND); } catch (Exception e) { log.error("Database error while deleting user {}", userId, e); // 不捕获异常,让全局处理器处理 // 会被识别为服务器错误,返回 Trace ID throw e; } } ``` --- ## 最佳实践 ### ✅ DO(应该做) #### 1. 为不同的业务错误定义专有的 ErrorCode ```java // ✅ 好的做法 public enum ErrorCode { USER_NOT_FOUND(10001, "user not found", "..."), PASSWORD_ERROR(10002, "password error", "..."), EMAIL_ALREADY_EXISTS(10003, "email already exists", "..."), } // ❌ 不好的做法 public enum ErrorCode { ERROR(40000, "error", "..."), // 太通用,无法区分 } ``` #### 2. 给异常添加足够的上下文信息 ```java // ✅ 好的做法 throw new BusinessException( ErrorCode.USER_NOT_FOUND, "User with id " + userId + " does not exist" ); // ❌ 不好的做法 throw new BusinessException(ErrorCode.USER_NOT_FOUND); // 前端收到的响应没有具体信息,无法知道是哪个用户 ``` #### 3. 使用 ThrowUtils 简化异常抛出 ```java // ✅ 好的做法 ThrowUtils.throwIfNull(user, ErrorCode.USER_NOT_FOUND); // ❌ 不好的做法 if (user == null) { throw new BusinessException(ErrorCode.USER_NOT_FOUND); } ``` #### 4. 明确区分 CLIENT 和 SERVER 错误 ```java // ✅ 好的做法 // 用户输入有问题 ErrorCode.PARAMS_ERROR(40000, "...", ErrorType.CLIENT) // 服务器内部问题 ErrorCode.DATABASE_ERROR(50100, "...", ErrorType.SERVER) // ❌ 不好的做法 // 所有错误都标记为 SERVER // 这样用户输入错误也会返回 Trace ID,很奇怪 ``` #### 5. 在关键操作前进行校验 ```java // ✅ 好的做法 public void transferMoney(Long fromUserId, Long toUserId, BigDecimal amount) { // 参数检查 ThrowUtils.throwIf(amount.compareTo(BigDecimal.ZERO) <= 0, ErrorCode.PARAMS_ERROR); // 业务检查 User fromUser = ThrowUtils.throwIfNull( userMapper.selectById(fromUserId), ErrorCode.USER_NOT_FOUND ); ThrowUtils.throwIf(fromUser.getBalance().compareTo(amount) < 0, ErrorCode.INSUFFICIENT_BALANCE); // 执行操作 ... } // ❌ 不好的做法 public void transferMoney(Long fromUserId, Long toUserId, BigDecimal amount) { // 直接执行,出错后才发现问题 userMapper.transferMoney(fromUserId, toUserId, amount); } ``` ### ❌ DON'T(不应该做) #### 1. 不要吞掉异常 ```java // ❌ 错误 try { User user = userMapper.selectById(userId); } catch (Exception e) { // 什么都不做,问题被隐藏了 } // ✅ 正确 try { User user = userMapper.selectById(userId); } catch (Exception e) { log.error("Failed to query user {}", userId, e); throw new BusinessException(ErrorCode.DATABASE_ERROR, e.getMessage()); } ``` #### 2. 不要在异常消息中暴露敏感信息 ```java // ❌ 错误(SERVER 错误) throw new BusinessException( ErrorCode.EXTERNAL_SERVICE_ERROR, "Failed to connect to https://payment.example.com on port 8443 due to SSL certificate error" ); // 前端会显示这个,安全隐患 // ✅ 正确 throw new BusinessException( ErrorCode.EXTERNAL_SERVICE_ERROR, "Payment service temporarily unavailable, please try again later" ); ``` #### 3. 不要混淆 message 和 description ```java // ❌ 错误 ErrorCode.USER_NOT_FOUND(10001, "User with id 999 does not exist", "...") // message 太长,应该放在 description 中 // ✅ 正确 ErrorCode.USER_NOT_FOUND(10001, "user not found", "User with id {id} does not exist") // message 简短通用,description 包含具体信息 ``` #### 4. 不要过度使用链式调用造成嵌套过深 ```java // ❌ 错误 ThrowUtils.throwIfNull( ThrowUtils.throwIfEmpty( ThrowUtils.throwIfNull(...), ErrorCode.NOT_FOUND ), ErrorCode.PARAMS_ERROR ); // 难以理解 // ✅ 正确 List users = userMapper.selectByIds(ids); ThrowUtils.throwIfEmpty(users, ErrorCode.NOT_FOUND); User user = users.get(0); ThrowUtils.throwIfNull(user, ErrorCode.USER_NOT_FOUND); ``` #### 5. 不要直接在 Controller 层进行业务逻辑检查 ```java // ❌ 错误 @PostMapping("/transfer") public BaseResponse transfer(@RequestBody TransferRequest req) { if (req.getAmount() <= 0) { return ResultUtils.error(ErrorCode.PARAMS_ERROR, "Amount must be positive"); } // 其他检查... return ResultUtils.success(transferService.transfer(req)); } // Controller 充斥着业务逻辑 // ✅ 正确 @PostMapping("/transfer") public BaseResponse transfer(@RequestBody @Valid TransferRequest req) { // 参数验证由 @Valid 和全局处理器处理 // 业务检查由 Service 层处理 return ResultUtils.success(transferService.transfer(req)); } // Service 层 public void transfer(TransferRequest req) { ThrowUtils.throwIf(req.getAmount().compareTo(ZERO) <= 0, ErrorCode.PARAMS_ERROR); // 业务逻辑和检查混在一起 ... } ``` --- ## 常见错误 ### 错误1:异常类型判断错误 ```java // ❌ 错误 throw new BusinessException(ErrorCode.DATABASE_ERROR, "..."); // DATABASE_ERROR 是 SERVER 类型,但在业务层抛出 // 业务层应该抛出 CLIENT 类型的错误,不知道具体原因就抛出 SERVER 错误 // ✅ 正确 try { // 数据库操作 } catch (Exception e) { log.error("Database error", e); throw new BusinessException(ErrorCode.DATABASE_ERROR, e.getMessage()); } // 只在真正的系统异常时抛出 SERVER 类型错误 ``` ### 错误2:错误码设计不合理 ```java // ❌ 错误 // 错误码太分散,难以维护 BID_PRICE_ERROR(30002, "...") BID_QUANTITY_ERROR(30003, "...") BID_INVALID_ERROR(30004, "...") BID_TIMEOUT_ERROR(30005, "...") // 如果每个小错误都要一个错误码,会导致枚举爆炸 // ✅ 正确 BID_FAILED(30002, "...", ErrorType.CLIENT) // 使用通用的错误码 + 不同的 description 来区分 throw new BusinessException(ErrorCode.BID_FAILED, "Bid price too low"); throw new BusinessException(ErrorCode.BID_FAILED, "Bid quantity invalid"); throw new BusinessException(ErrorCode.BID_FAILED, "Auction has closed"); ``` ### 错误3:没有考虑线程安全 ```java // ❌ 错误(如果异常处理器中使用了共享状态) @RestControllerAdvice public class GlobalExceptionHandler { private List errorLog = new ArrayList<>(); // 共享状态 @ExceptionHandler(Exception.class) public BaseResponse handle(Exception e) { errorLog.add(e.getMessage()); // 线程不安全! return ...; } } // ✅ 正确 @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public BaseResponse handle(Exception e) { log.error("Exception occurred", e); // 使用 SLF4J,已处理线程安全 return ...; } } ``` ### 错误4:日志过度记录 ```java // ❌ 错误 @ExceptionHandler(BusinessException.class) public BaseResponse handle(BusinessException e) { log.error("Business exception", e); // 业务异常不需要打印 ERROR 日志 return ...; } // ✅ 正确 @ExceptionHandler(BusinessException.class) public BaseResponse handle(BusinessException e) { if (e.getErrorType() == ErrorType.SERVER) { log.error("Business exception", e); // 只有 SERVER 类型才打 ERROR } else { log.warn("Business exception", e); // CLIENT 类型打 WARN } return ...; } // 或更好的做法 @ExceptionHandler(BusinessException.class) public BaseResponse handle(BusinessException e) { if (e.getErrorType() == ErrorType.CLIENT) { log.debug("Client error: {}", e); // 客户端错误级别降低到 DEBUG } else { log.error("Server error", e); } return ...; } ``` --- ## 总结 1. **统一规范**:所有异常都以相同的格式返回 2. **清晰分类**:CLIENT/SERVER 错误类型区分明确 3. **信息完整**:包含 code/message/description/traceId 4. **追踪能力**:服务器错误通过 Trace ID 快速定位 5. **简洁易用**:ThrowUtils 和 ResultUtils 简化开发 6. **可维护性**:业务逻辑集中在 Service 层,易于管理 **关键点**: - ✅ 业务检查在 Service 层,参数检查在 Controller/DTO 层 - ✅ 只有真正的系统异常才抛出 SERVER 类型错误 - ✅ 给每个错误提供足够的上下文信息 - ✅ 使用 Trace ID 追踪服务器错误 - ✅ 保持 message 简短通用,description 详细具体 --- **项目分区导航**:⬅️ [[2-Learning/05-项目/05-最佳实践/01-MVC分层与项目规范/05-ZwwwSpringBootBaseTemplate工程模板|05-ZwwwSpringBootBaseTemplate工程模板]] | 01-异常处理最佳实践指南 | ➡️ [[02-异常处理最佳实践代码|02-异常处理最佳实践代码]]