异常处理最佳实践指南
核心设计理念
三层异常分类
错误类型区分
| 错误类型 | 状态码 | 处理方式 | 返回内容 |
|---|---|---|---|
| CLIENT | 4xxxx | 返回原样 | message + description |
| SERVER | 5xxxx | 隐藏敏感信息 | 通用消息 + Trace ID |
| SUCCESS | 0 | 正常返回 | 成功消息 + 数据 |
架构概览
类设计关系图
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
响应格式示例
成功响应
{
"code": 0,
"message": "success",
"description": null,
"data": {
"id": 1,
"username": "john"
},
"timestamp": 1704067200000,
"traceId": null
}
客户端错误响应
{
"code": 10001,
"message": "user not found",
"description": "User with id 999 does not exist",
"data": null,
"timestamp": 1704067200000,
"traceId": null
}
服务器错误响应
{
"code": 50000,
"message": "System error, please contact support",
"description": "系统错误,请联系管理员",
"data": null,
"timestamp": 1704067200000,
"traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
各模块详解
1. ErrorCode 枚举设计
编码规范
0 → 成功
40000 → 客户端基础错误(4开头)
50000 → 服务器基础错误(5开头)
1xxxx → 用户模块错误
2xxxx → 作品模块错误
3xxxx → 销售模块错误
为什么要加 description 字段
// ❌ 不够好
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 的作用
// 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 异常类
构造方式对比
// 方式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
RuntimeException
├─ 优点1:无需在方法签名中声明 throws
├─ 优点2:能被 @Transactional 正确识别进行回滚
├─ 优点3:支持链式处理
└─ 缺点:需要程序员显式处理(通过异常处理器)
3. ThrowUtils 工具类
快速异常抛出
// 场景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<User> 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
// ❌ 传统写法(冗长)
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 响应工具
响应构造示例
// 成功响应(带数据)
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 的作用
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BaseResponse<T> { ... }
// 作用:序列化时忽略 null 值字段
// 成功响应不会包含 traceId 字段(为 null)
// 服务器错误响应不会包含 data 字段(为 null)
// 结果:响应体更小,网络传输更快
5. GlobalExceptionHandler 处理器
异常处理流程
Trace ID 的作用
客户端收到错误响应:
{
"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. 反馈给用户
日志记录示例
业务异常日志(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:参数校验
@PostMapping("/users")
public BaseResponse<UserDTO> 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:业务规则检查
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:外部服务调用失败
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:数据库操作失败
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
// ✅ 好的做法
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. 给异常添加足够的上下文信息
// ✅ 好的做法
throw new BusinessException(
ErrorCode.USER_NOT_FOUND,
"User with id " + userId + " does not exist"
);
// ❌ 不好的做法
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
// 前端收到的响应没有具体信息,无法知道是哪个用户
3. 使用 ThrowUtils 简化异常抛出
// ✅ 好的做法
ThrowUtils.throwIfNull(user, ErrorCode.USER_NOT_FOUND);
// ❌ 不好的做法
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
4. 明确区分 CLIENT 和 SERVER 错误
// ✅ 好的做法
// 用户输入有问题
ErrorCode.PARAMS_ERROR(40000, "...", ErrorType.CLIENT)
// 服务器内部问题
ErrorCode.DATABASE_ERROR(50100, "...", ErrorType.SERVER)
// ❌ 不好的做法
// 所有错误都标记为 SERVER
// 这样用户输入错误也会返回 Trace ID,很奇怪
5. 在关键操作前进行校验
// ✅ 好的做法
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. 不要吞掉异常
// ❌ 错误
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. 不要在异常消息中暴露敏感信息
// ❌ 错误(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
// ❌ 错误
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. 不要过度使用链式调用造成嵌套过深
// ❌ 错误
ThrowUtils.throwIfNull(
ThrowUtils.throwIfEmpty(
ThrowUtils.throwIfNull(...),
ErrorCode.NOT_FOUND
),
ErrorCode.PARAMS_ERROR
);
// 难以理解
// ✅ 正确
List<User> users = userMapper.selectByIds(ids);
ThrowUtils.throwIfEmpty(users, ErrorCode.NOT_FOUND);
User user = users.get(0);
ThrowUtils.throwIfNull(user, ErrorCode.USER_NOT_FOUND);
5. 不要直接在 Controller 层进行业务逻辑检查
// ❌ 错误
@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:异常类型判断错误
// ❌ 错误
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:错误码设计不合理
// ❌ 错误
// 错误码太分散,难以维护
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:没有考虑线程安全
// ❌ 错误(如果异常处理器中使用了共享状态)
@RestControllerAdvice
public class GlobalExceptionHandler {
private List<String> 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:日志过度记录
// ❌ 错误
@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 ...;
}
总结
- 统一规范:所有异常都以相同的格式返回
- 清晰分类:CLIENT/SERVER 错误类型区分明确
- 信息完整:包含 code/message/description/traceId
- 追踪能力:服务器错误通过 Trace ID 快速定位
- 简洁易用:ThrowUtils 和 ResultUtils 简化开发
- 可维护性:业务逻辑集中在 Service 层,易于管理
关键点:
- ✅ 业务检查在 Service 层,参数检查在 Controller/DTO 层
- ✅ 只有真正的系统异常才抛出 SERVER 类型错误
- ✅ 给每个错误提供足够的上下文信息
- ✅ 使用 Trace ID 追踪服务器错误
- ✅ 保持 message 简短通用,description 详细具体
项目分区导航:⬅️ 05-ZwwwSpringBootBaseTemplate工程模板 | 01-异常处理最佳实践指南 | ➡️ 02-异常处理最佳实践代码
💬 评论