异常处理
概述
ruoyi-common-core 提供了一套完整的异常处理体系,采用分层设计和统一管理的方式,确保系统异常的规范化处理。该体系支持国际化消息、错误码管理、异常分类等功能,为系统提供稳定可靠的异常处理机制。
异常体系架构
层次结构
BaseException (基础异常)
├── BaseBusinessException (业务异常基类)
│ ├── ServiceException (通用业务异常)
│ └── SseException (SSE专用异常)
└── 模块化异常
├── UserException (用户异常)
├── FileException (文件异常)
└── CaptchaException (验证码异常)
设计原则
- 分层设计: 基础异常 → 业务异常 → 模块异常
- 国际化支持: 所有异常消息支持多语言
- 错误码管理: 统一的错误码体系
- 链式操作: 支持流式API调用
- 向下兼容: 保持API稳定性
核心异常类
1. BaseException (基础异常类)
所有自定义异常的根基类,提供国际化消息和错误码支持。
java
public abstract class BaseException extends RuntimeException {
private String module; // 所属模块
private String code; // 错误码
private Object[] args; // 错误码参数
private String defaultMessage; // 默认错误消息
}
核心特性:
- 自动国际化消息处理
- 支持模块化错误管理
- 错误码参数化支持
- 异常链传递
使用示例:
java
// 基础构造
throw new UserException("user.not.found", userId);
// 带模块信息
throw new UserException("user", "password.invalid", null, "密码错误");
// 带原因异常
throw new FileException("file.upload.failed", cause);
2. BaseBusinessException (业务异常基类)
继承自 BaseException,专门用于业务逻辑异常处理。
java
public abstract class BaseBusinessException extends BaseException {
private Integer businessCode; // 业务错误码
private String detailMessage; // 详细错误信息
// 支持链式调用
public BaseBusinessException setMessage(String message) { /* ... */ }
public BaseBusinessException setBusinessCode(Integer code) { /* ... */ }
public BaseBusinessException setDetailMessage(String detail) { /* ... */ }
}
适用场景:
- 业务规则验证失败
- 数据状态不正确
- 业务流程异常
使用示例:
java
// 链式设置
throw new ServiceException("订单状态错误")
.setBusinessCode(4001)
.setDetailMessage("订单已支付,无法取消");
// 快速创建
throw ServiceException.of("库存不足", 4002);
模块化异常
1. ServiceException (通用业务异常)
最常用的业务异常类,适用于大部分业务场景。
java
public final class ServiceException extends BaseBusinessException {
// 基础构造方法
public ServiceException(String message);
public ServiceException(String message, Integer businessCode);
public ServiceException(String message, Integer businessCode, String detailMessage);
// 静态工厂方法
public static ServiceException of(String message);
public static ServiceException of(String message, Integer businessCode);
// 条件抛出
public static void throwIf(boolean condition, String message);
public static void notNull(Object object, String message);
}
使用场景:
java
// 参数校验
ServiceException.notNull(user, "用户不能为空");
// 条件判断
ServiceException.throwIf(balance < amount, "余额不足");
// 业务规则
if (order.getStatus().equals("PAID")) {
throw ServiceException.of("订单已支付,无法修改", 4001);
}
// 国际化消息
throw ServiceException.of("order.status.invalid");
2. UserException (用户异常)
专门处理用户相关的异常,如登录、注册、权限等。
java
public class UserException extends BaseException {
private static final String MODULE = "user";
// 构造方法
public UserException(String code, Object... args);
public UserException(String code, Object[] args, String defaultMessage);
// 静态工厂方法
public static UserException of(String code, Object... args);
public static void throwIf(boolean condition, String code, Object... args);
public static void notNull(Object object, String code, Object... args);
}
常用错误码:
java
public interface UserErrors {
String ACCOUNT_NOT_EXISTS = "user.account.not.exists";
String PASSWORD_MISMATCH = "user.password.mismatch";
String ACCOUNT_DISABLED = "user.account.disabled";
String SESSION_EXPIRED = "user.session.expired";
String PASSWORD_RETRY_LOCKED = "user.password.retry.locked";
}
使用示例:
java
// 账户不存在
throw UserException.of("user.account.not.exists", username);
// 密码错误次数
throw UserException.of("user.password.retry.count", retryCount);
// 账户锁定
throw UserException.of("user.password.retry.locked", retryCount, lockTime);
// 条件检查
UserException.throwIf(user == null, "user.not.found", userId);
3. FileException (文件异常)
专门处理文件操作相关的异常。
java
public class FileException extends BaseException {
private static final String MODULE = "file";
public static FileException of(String code, Object... args);
public static FileException of(String defaultMessage);
}
使用示例:
java
// 文件大小超限
throw FileException.of("file.upload.size.exceed.limit", maxSize);
// 文件类型不支持
throw FileException.of("file.upload.type.not.supported");
// 文件上传失败
throw FileException.of("文件上传失败: " + e.getMessage());
4. CaptchaException (验证码异常)
处理验证码相关的异常。
java
// 验证码错误
public class CaptchaException extends UserException {
public static CaptchaException of() {
return new CaptchaException();
}
}
// 验证码过期
public class CaptchaExpireException extends UserException {
public static CaptchaExpireException of() {
return new CaptchaExpireException();
}
}
使用示例:
java
// 验证码校验
if (!captchaService.validate(code, uuid)) {
throw CaptchaException.of();
}
// 验证码过期
if (captchaService.isExpired(uuid)) {
throw CaptchaExpireException.of();
}
国际化支持
消息键定义
异常消息支持国际化,通过 I18nKeys 接口统一管理消息键。
java
public interface I18nKeys {
interface User {
String ACCOUNT_NOT_EXISTS = "user.account.not.exists";
String PASSWORD_MISMATCH = "user.password.mismatch";
String ACCOUNT_DISABLED = "user.account.disabled";
String SESSION_EXPIRED = "user.session.expired";
String PASSWORD_RETRY_LOCKED = "user.password.retry.locked";
}
interface FileUpload {
String SIZE_EXCEED_LIMIT = "file.upload.size.exceed.limit";
String TYPE_NOT_SUPPORTED = "file.upload.type.not.supported";
String FAILED = "file.upload.failed";
}
interface VerifyCode {
String CAPTCHA_INVALID = "verify.code.captcha.invalid";
String CAPTCHA_EXPIRED = "verify.code.captcha.expired";
String SMS_INVALID = "verify.code.sms.invalid";
}
}
自动消息处理
BaseException 会自动识别国际化键并进行消息转换。
java
@Override
public String getMessage() {
String message = null;
// 检查是否为国际化键格式
if (StringUtils.isNotBlank(code) && RegexValidator.isValidI18nKey(code)) {
message = MessageUtils.message(code, args);
}
if (message == null) {
message = defaultMessage;
}
return message;
}
多语言配置
messages.properties (默认-中文)
properties
user.account.not.exists=对不起, 您的账号:{0} 不存在
user.password.mismatch=用户不存在/密码错误
user.account.disabled=对不起,您的账号:{0} 已禁用,请联系管理员
user.password.retry.locked=密码输入错误{0}次,帐户锁定{1}分钟
file.upload.size.exceed.limit=上传的文件大小超出限制的文件大小!允许的文件最大大小是:{0}MB!
messages_en.properties (英文)
properties
user.account.not.exists=Sorry, your account: {0} does not exist
user.password.mismatch=User does not exist/password error
user.account.disabled=Sorry, your account: {0} has been disabled, please contact the administrator
user.password.retry.locked=Password input error {0} times, account locked for {1} minutes
file.upload.size.exceed.limit=The uploaded file size exceeds the limit! Maximum allowed file size is: {0}MB!
与响应体系集成
异常自动转换
异常可以自动转换为统一的响应格式 R<T>。
java
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ServiceException.class)
public R<Void> handleServiceException(ServiceException e) {
return R.fail(e.getMessage());
}
@ExceptionHandler(UserException.class)
public R<Void> handleUserException(UserException e) {
return R.fail(e.getMessage());
}
@ExceptionHandler(FileException.class)
public R<Void> handleFileException(FileException e) {
return R.fail(e.getMessage());
}
}
业务逻辑中的使用
java
@Service
public class UserService {
public UserVo login(LoginBo bo) {
// 用户不存在
User user = userMapper.selectByUsername(bo.getUsername());
UserException.notNull(user, "user.account.not.exists", bo.getUsername());
// 账户被禁用
UserException.throwIf("0".equals(user.getStatus()),
"user.account.disabled", user.getUsername());
// 密码错误
if (!passwordEncoder.matches(bo.getPassword(), user.getPassword())) {
throw UserException.of("user.password.mismatch");
}
return MapstructUtils.convert(user, UserVo.class);
}
public void updateUser(UserBo bo) {
// 参数校验
ServiceException.notNull(bo.getId(), "用户ID不能为空");
// 业务校验
User existUser = userMapper.selectById(bo.getId());
ServiceException.throwIf(existUser == null, "用户不存在");
// 状态检查
if ("1".equals(existUser.getDelFlag())) {
throw ServiceException.of("用户已删除,无法修改", 4001);
}
}
}
异常处理最佳实践
1. 异常选择指南
java
// ✅ 推荐: 使用具体的异常类型
throw UserException.of("user.not.found", userId);
// ❌ 不推荐: 使用通用异常
throw new RuntimeException("用户不存在");
// ✅ 推荐: 业务异常使用ServiceException
throw ServiceException.of("订单状态错误", 4001);
// ✅ 推荐: 条件检查使用静态方法
ServiceException.throwIf(amount <= 0, "金额必须大于0");
2. 错误消息规范
java
// ✅ 推荐: 使用国际化键
throw UserException.of("user.password.invalid");
// ✅ 推荐: 带参数的国际化
throw UserException.of("user.login.locked", lockTime);
// ❌ 不推荐: 硬编码中文消息
throw new ServiceException("用户名不能为空");
// ✅ 推荐: 英文默认消息
throw ServiceException.of("Username cannot be empty");
3. 异常链传递
java
// ✅ 推荐: 保留原异常信息
try {
userMapper.insert(user);
} catch (DataAccessException e) {
throw ServiceException.of("用户创建失败", e);
}
// ✅ 推荐: 记录详细错误
catch (Exception e) {
log.error("用户服务异常: userId={}, error={}", userId, e.getMessage(), e);
throw ServiceException.of("系统内部错误");
}
4. 业务异常设计
java
@Service
public class OrderService {
public void cancelOrder(Long orderId) {
Order order = orderMapper.selectById(orderId);
// 参数校验异常
ServiceException.notNull(order, "订单不存在");
// 业务规则异常
if ("PAID".equals(order.getStatus())) {
throw ServiceException.of("order.cannot.cancel.paid", 4001)
.setDetailMessage("订单号: " + order.getOrderNo());
}
if ("DELIVERED".equals(order.getStatus())) {
throw ServiceException.of("order.cannot.cancel.delivered", 4002);
}
// 执行取消逻辑
order.setStatus("CANCELLED");
orderMapper.updateById(order);
}
}
5. 异常日志记录
java
@Component
public class ExceptionLogger {
private static final Logger log = LoggerFactory.getLogger(ExceptionLogger.class);
@EventListener
public void handleServiceException(ServiceException e) {
// 业务异常记录为WARN级别
log.warn("业务异常: code={}, message={}", e.getBusinessCode(), e.getMessage());
}
@EventListener
public void handleUserException(UserException e) {
// 用户异常记录为INFO级别
log.info("用户异常: module={}, code={}, message={}",
e.getModule(), e.getCode(), e.getMessage());
}
@EventListener
public void handleSystemException(Exception e) {
// 系统异常记录为ERROR级别
log.error("系统异常: ", e);
}
}
自定义异常
创建模块异常
java
// 1. 继承BaseException
public class PaymentException extends BaseException {
private static final String MODULE = "payment";
public PaymentException(String code, Object... args) {
super(MODULE, code, args, null);
}
public PaymentException(String code, Object[] args, String defaultMessage) {
super(MODULE, code, args, defaultMessage);
}
// 静态工厂方法
public static PaymentException of(String code, Object... args) {
return new PaymentException(code, args);
}
public static void throwIf(boolean condition, String code, Object... args) {
if (condition) {
throw new PaymentException(code, args);
}
}
}
// 2. 定义错误码
public interface PaymentErrors {
String INSUFFICIENT_BALANCE = "payment.insufficient.balance";
String PAYMENT_EXPIRED = "payment.expired";
String INVALID_PAYMENT_METHOD = "payment.method.invalid";
}
// 3. 使用示例
public class PaymentService {
public void processPayment(PaymentBo bo) {
// 余额不足
PaymentException.throwIf(balance < amount,
PaymentErrors.INSUFFICIENT_BALANCE, balance, amount);
// 支付方式无效
if (!isValidPaymentMethod(bo.getMethod())) {
throw PaymentException.of(PaymentErrors.INVALID_PAYMENT_METHOD, bo.getMethod());
}
}
}
特殊异常处理
java
// SSE推送异常
public class SseException extends BaseBusinessException {
public SseException(String message) {
super(message);
}
public static SseException of(String message) {
return new SseException(message);
}
public static SseException of(String message, Integer businessCode) {
return new SseException(message, businessCode);
}
}
// 使用示例
@RestController
public class SseController {
@GetMapping("/sse")
public SseEmitter subscribe() {
try {
return sseService.createConnection();
} catch (Exception e) {
throw SseException.of("SSE连接创建失败: " + e.getMessage());
}
}
}
注意事项
1. 性能考虑
- 异常创建有性能开销,避免在循环中频繁抛出
- 使用静态工厂方法减少对象创建
- 异常信息避免过长,影响日志性能
2. 内存管理
- 避免在异常中保存大对象引用
- 及时释放异常相关资源
- 注意异常对象的生命周期
3. 安全考虑
- 不要在异常消息中暴露敏感信息
- 对外接口的异常消息要做脱敏处理
- 记录异常日志时注意数据安全
4. 国际化注意
- 异常消息的参数顺序在不同语言中可能不同
- 使用占位符时要考虑语法结构差异
- 测试多语言环境下的异常显示
5. 向后兼容
- 新增异常类型要保持API稳定
- 错误码一旦发布不要随意修改
- 消息格式变更要考虑客户端兼容性