异常处理
概述
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());
}
}
}异常链跟踪与调试
异常追踪标识
在分布式系统中,异常追踪需要关联请求ID便于问题定位。
java
/**
* 可追踪异常接口
*/
public interface TraceableException {
/**
* 获取追踪ID
*/
String getTraceId();
/**
* 获取跨度ID
*/
String getSpanId();
/**
* 获取异常发生时间
*/
LocalDateTime getOccurredAt();
}
/**
* 带追踪信息的业务异常
*/
public class TraceableServiceException extends ServiceException
implements TraceableException {
private final String traceId;
private final String spanId;
private final LocalDateTime occurredAt;
public TraceableServiceException(String message) {
super(message);
this.traceId = TraceContext.getTraceId();
this.spanId = TraceContext.getSpanId();
this.occurredAt = LocalDateTime.now();
}
public static TraceableServiceException of(String message) {
return new TraceableServiceException(message);
}
@Override
public String getTraceId() {
return traceId;
}
@Override
public String getSpanId() {
return spanId;
}
@Override
public LocalDateTime getOccurredAt() {
return occurredAt;
}
}异常上下文收集器
java
/**
* 异常上下文收集器
* 自动收集异常发生时的上下文信息
*/
@Component
@Slf4j
public class ExceptionContextCollector {
@Autowired
private HttpServletRequest request;
/**
* 收集异常上下文
*/
public ExceptionContext collect(Exception e) {
ExceptionContext context = new ExceptionContext();
// 基础信息
context.setExceptionType(e.getClass().getName());
context.setMessage(e.getMessage());
context.setTimestamp(LocalDateTime.now());
// 请求信息
if (request != null) {
context.setRequestUri(request.getRequestURI());
context.setRequestMethod(request.getMethod());
context.setClientIp(ServletUtils.getClientIP(request));
context.setUserAgent(request.getHeader("User-Agent"));
}
// 用户信息
try {
LoginUser loginUser = LoginHelper.getLoginUser();
if (loginUser != null) {
context.setUserId(loginUser.getUserId());
context.setUsername(loginUser.getUsername());
context.setTenantId(loginUser.getTenantId());
}
} catch (Exception ignored) {
// 未登录状态忽略
}
// 追踪信息
context.setTraceId(TraceContext.getTraceId());
context.setSpanId(TraceContext.getSpanId());
// 堆栈信息(仅保留前20行)
context.setStackTrace(getStackTrace(e, 20));
return context;
}
/**
* 获取堆栈信息
*/
private String getStackTrace(Exception e, int maxLines) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
String[] lines = sw.toString().split("\n");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < Math.min(lines.length, maxLines); i++) {
sb.append(lines[i]).append("\n");
}
if (lines.length > maxLines) {
sb.append("... ").append(lines.length - maxLines).append(" more lines\n");
}
return sb.toString();
}
}
/**
* 异常上下文实体
*/
@Data
public class ExceptionContext {
/** 异常类型 */
private String exceptionType;
/** 异常消息 */
private String message;
/** 发生时间 */
private LocalDateTime timestamp;
/** 请求URI */
private String requestUri;
/** 请求方法 */
private String requestMethod;
/** 客户端IP */
private String clientIp;
/** User-Agent */
private String userAgent;
/** 用户ID */
private Long userId;
/** 用户名 */
private String username;
/** 租户ID */
private String tenantId;
/** 追踪ID */
private String traceId;
/** 跨度ID */
private String spanId;
/** 堆栈信息 */
private String stackTrace;
}异常调试工具
java
/**
* 异常调试工具类
*/
public final class ExceptionDebugUtils {
private ExceptionDebugUtils() {}
/**
* 格式化异常信息用于调试
*/
public static String formatForDebug(Exception e) {
StringBuilder sb = new StringBuilder();
sb.append("=== Exception Debug Info ===\n");
sb.append("Type: ").append(e.getClass().getName()).append("\n");
sb.append("Message: ").append(e.getMessage()).append("\n");
sb.append("Time: ").append(LocalDateTime.now()).append("\n");
if (e instanceof BaseException be) {
sb.append("Module: ").append(be.getModule()).append("\n");
sb.append("Code: ").append(be.getCode()).append("\n");
sb.append("Args: ").append(Arrays.toString(be.getArgs())).append("\n");
}
if (e instanceof BaseBusinessException bbe) {
sb.append("BusinessCode: ").append(bbe.getBusinessCode()).append("\n");
sb.append("DetailMessage: ").append(bbe.getDetailMessage()).append("\n");
}
// 根因分析
Throwable rootCause = getRootCause(e);
if (rootCause != e) {
sb.append("Root Cause: ").append(rootCause.getClass().getName()).append("\n");
sb.append("Root Message: ").append(rootCause.getMessage()).append("\n");
}
sb.append("=== End Debug Info ===\n");
return sb.toString();
}
/**
* 获取异常根因
*/
public static Throwable getRootCause(Throwable t) {
Throwable cause = t;
while (cause.getCause() != null && cause.getCause() != cause) {
cause = cause.getCause();
}
return cause;
}
/**
* 检查异常链中是否包含指定类型
*/
public static boolean containsExceptionType(Throwable t, Class<? extends Throwable> type) {
Throwable current = t;
while (current != null) {
if (type.isInstance(current)) {
return true;
}
current = current.getCause();
}
return false;
}
/**
* 从异常链中提取指定类型的异常
*/
@SuppressWarnings("unchecked")
public static <T extends Throwable> Optional<T> extractException(
Throwable t, Class<T> type) {
Throwable current = t;
while (current != null) {
if (type.isInstance(current)) {
return Optional.of((T) current);
}
current = current.getCause();
}
return Optional.empty();
}
}异常指标监控
异常计数器
java
/**
* 异常监控指标收集器
*/
@Component
@Slf4j
public class ExceptionMetricsCollector {
private final MeterRegistry meterRegistry;
// 异常计数器
private final ConcurrentMap<String, Counter> exceptionCounters =
new ConcurrentHashMap<>();
// 异常发生时间分布
private final Timer exceptionTimer;
public ExceptionMetricsCollector(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.exceptionTimer = Timer.builder("exception.handling.time")
.description("异常处理耗时")
.register(meterRegistry);
}
/**
* 记录异常发生
*/
public void recordException(Exception e) {
String exceptionType = e.getClass().getSimpleName();
// 增加异常计数
Counter counter = exceptionCounters.computeIfAbsent(exceptionType,
type -> Counter.builder("exception.count")
.tag("type", type)
.tag("module", getModule(e))
.description("异常发生次数")
.register(meterRegistry));
counter.increment();
// 记录业务码分布
if (e instanceof BaseBusinessException bbe && bbe.getBusinessCode() != null) {
Counter.builder("exception.business.code")
.tag("code", String.valueOf(bbe.getBusinessCode()))
.register(meterRegistry)
.increment();
}
}
/**
* 记录异常处理耗时
*/
public void recordHandlingTime(Runnable task) {
exceptionTimer.record(task);
}
/**
* 获取异常模块
*/
private String getModule(Exception e) {
if (e instanceof BaseException be) {
return StringUtils.defaultIfBlank(be.getModule(), "unknown");
}
return "system";
}
/**
* 获取异常统计摘要
*/
public Map<String, Long> getExceptionSummary() {
Map<String, Long> summary = new HashMap<>();
exceptionCounters.forEach((type, counter) ->
summary.put(type, (long) counter.count()));
return summary;
}
}异常告警服务
java
/**
* 异常告警服务
* 当异常频率超过阈值时触发告警
*/
@Service
@Slf4j
public class ExceptionAlertService {
@Autowired
private NotificationService notificationService;
// 异常计数窗口(滑动窗口)
private final ConcurrentMap<String, SlidingWindowCounter> windowCounters =
new ConcurrentHashMap<>();
// 告警阈值配置
@Value("${exception.alert.threshold:100}")
private int alertThreshold;
@Value("${exception.alert.window-seconds:60}")
private int windowSeconds;
/**
* 记录异常并检查是否需要告警
*/
public void recordAndCheck(Exception e) {
String key = e.getClass().getSimpleName();
SlidingWindowCounter counter = windowCounters.computeIfAbsent(key,
k -> new SlidingWindowCounter(windowSeconds));
long count = counter.incrementAndGet();
// 检查是否超过阈值
if (count >= alertThreshold && shouldAlert(key)) {
triggerAlert(e, count);
}
}
/**
* 检查是否应该发送告警(防止告警风暴)
*/
private boolean shouldAlert(String exceptionType) {
String alertKey = "exception:alert:" + exceptionType;
// 使用Redis实现告警抑制,5分钟内只告警一次
return RedisUtils.setObjectIfAbsent(alertKey, "1", Duration.ofMinutes(5));
}
/**
* 触发告警
*/
private void triggerAlert(Exception e, long count) {
ExceptionAlert alert = new ExceptionAlert();
alert.setExceptionType(e.getClass().getName());
alert.setMessage(e.getMessage());
alert.setCount(count);
alert.setWindowSeconds(windowSeconds);
alert.setThreshold(alertThreshold);
alert.setTimestamp(LocalDateTime.now());
log.warn("异常告警: {} 在{}秒内发生{}次,超过阈值{}",
alert.getExceptionType(), windowSeconds, count, alertThreshold);
// 发送通知
notificationService.sendAlert(alert);
}
}
/**
* 滑动窗口计数器
*/
public class SlidingWindowCounter {
private final int windowSeconds;
private final ConcurrentLinkedQueue<Long> timestamps = new ConcurrentLinkedQueue<>();
public SlidingWindowCounter(int windowSeconds) {
this.windowSeconds = windowSeconds;
}
public long incrementAndGet() {
long now = System.currentTimeMillis();
long windowStart = now - (windowSeconds * 1000L);
// 添加当前时间戳
timestamps.add(now);
// 清理过期时间戳
while (!timestamps.isEmpty() && timestamps.peek() < windowStart) {
timestamps.poll();
}
return timestamps.size();
}
}异常统计报表
java
/**
* 异常统计报表服务
*/
@Service
@Slf4j
public class ExceptionReportService {
@Autowired
private ExceptionLogMapper exceptionLogMapper;
/**
* 生成日报
*/
@Scheduled(cron = "0 0 8 * * ?")
public void generateDailyReport() {
LocalDate yesterday = LocalDate.now().minusDays(1);
ExceptionReport report = buildReport(yesterday, yesterday);
sendReport(report);
}
/**
* 生成周报
*/
@Scheduled(cron = "0 0 9 ? * MON")
public void generateWeeklyReport() {
LocalDate endDate = LocalDate.now().minusDays(1);
LocalDate startDate = endDate.minusDays(6);
ExceptionReport report = buildReport(startDate, endDate);
sendReport(report);
}
/**
* 构建异常报表
*/
public ExceptionReport buildReport(LocalDate startDate, LocalDate endDate) {
ExceptionReport report = new ExceptionReport();
report.setStartDate(startDate);
report.setEndDate(endDate);
report.setGeneratedAt(LocalDateTime.now());
// 查询异常统计数据
List<ExceptionStat> stats = exceptionLogMapper.selectExceptionStats(
startDate.atStartOfDay(),
endDate.plusDays(1).atStartOfDay());
// 按类型分组统计
Map<String, Long> byType = stats.stream()
.collect(Collectors.groupingBy(
ExceptionStat::getExceptionType,
Collectors.summingLong(ExceptionStat::getCount)));
report.setByType(byType);
// 按模块分组统计
Map<String, Long> byModule = stats.stream()
.collect(Collectors.groupingBy(
ExceptionStat::getModule,
Collectors.summingLong(ExceptionStat::getCount)));
report.setByModule(byModule);
// 按小时分布
Map<Integer, Long> byHour = stats.stream()
.collect(Collectors.groupingBy(
ExceptionStat::getHour,
Collectors.summingLong(ExceptionStat::getCount)));
report.setByHour(byHour);
// Top 10 异常
List<ExceptionStat> top10 = stats.stream()
.sorted(Comparator.comparing(ExceptionStat::getCount).reversed())
.limit(10)
.collect(Collectors.toList());
report.setTop10Exceptions(top10);
// 总计
report.setTotalCount(stats.stream()
.mapToLong(ExceptionStat::getCount).sum());
return report;
}
/**
* 发送报表
*/
private void sendReport(ExceptionReport report) {
// 发送邮件或消息通知
log.info("异常报表已生成: {} ~ {}, 总计: {} 次异常",
report.getStartDate(), report.getEndDate(), report.getTotalCount());
}
}
/**
* 异常报表实体
*/
@Data
public class ExceptionReport {
private LocalDate startDate;
private LocalDate endDate;
private LocalDateTime generatedAt;
private Long totalCount;
private Map<String, Long> byType;
private Map<String, Long> byModule;
private Map<Integer, Long> byHour;
private List<ExceptionStat> top10Exceptions;
}分布式系统异常处理
远程调用异常包装
java
/**
* 远程调用异常
* 用于封装远程服务调用失败
*/
public class RemoteServiceException extends BaseBusinessException {
private final String serviceName;
private final String methodName;
private final int httpStatus;
private final String remoteMessage;
public RemoteServiceException(String serviceName, String methodName,
int httpStatus, String remoteMessage) {
super("远程服务调用失败: " + serviceName + "." + methodName);
this.serviceName = serviceName;
this.methodName = methodName;
this.httpStatus = httpStatus;
this.remoteMessage = remoteMessage;
}
public static RemoteServiceException of(String serviceName, String methodName,
int httpStatus, String remoteMessage) {
return new RemoteServiceException(serviceName, methodName,
httpStatus, remoteMessage);
}
// Getters...
}
/**
* Feign 异常解码器
*/
@Component
public class FeignExceptionDecoder implements ErrorDecoder {
private final ObjectMapper objectMapper;
@Override
public Exception decode(String methodKey, Response response) {
String serviceName = extractServiceName(methodKey);
String methodName = extractMethodName(methodKey);
try {
// 尝试解析远程响应体
String body = Util.toString(response.body().asReader(StandardCharsets.UTF_8));
R<?> result = objectMapper.readValue(body, R.class);
return RemoteServiceException.of(
serviceName,
methodName,
response.status(),
result.getMsg());
} catch (Exception e) {
return RemoteServiceException.of(
serviceName,
methodName,
response.status(),
"远程服务响应解析失败");
}
}
private String extractServiceName(String methodKey) {
// 从 methodKey 提取服务名
int index = methodKey.indexOf('#');
return index > 0 ? methodKey.substring(0, index) : methodKey;
}
private String extractMethodName(String methodKey) {
// 从 methodKey 提取方法名
int index = methodKey.indexOf('#');
return index > 0 ? methodKey.substring(index + 1) : methodKey;
}
}熔断降级异常
java
/**
* 熔断异常
*/
public class CircuitBreakerException extends BaseBusinessException {
private final String circuitBreakerName;
private final String state;
public CircuitBreakerException(String circuitBreakerName, String state) {
super("服务熔断: " + circuitBreakerName);
this.circuitBreakerName = circuitBreakerName;
this.state = state;
this.setBusinessCode(5001);
}
public static CircuitBreakerException of(String name) {
return new CircuitBreakerException(name, "OPEN");
}
}
/**
* 降级异常
*/
public class FallbackException extends BaseBusinessException {
private final String originalService;
private final Exception originalException;
public FallbackException(String originalService, Exception originalException) {
super("服务降级: " + originalService);
this.originalService = originalService;
this.originalException = originalException;
this.setBusinessCode(5002);
}
public static FallbackException of(String service, Exception cause) {
return new FallbackException(service, cause);
}
}
/**
* 通用降级处理器
*/
@Component
@Slf4j
public class GenericFallbackHandler {
/**
* 处理远程调用降级
*/
public <T> T handleFallback(String serviceName, String method,
Throwable throwable, Supplier<T> defaultSupplier) {
log.warn("服务降级: {}.{}, 原因: {}", serviceName, method,
throwable.getMessage());
// 记录降级指标
recordFallbackMetric(serviceName, method);
// 返回默认值或抛出降级异常
if (defaultSupplier != null) {
return defaultSupplier.get();
}
throw FallbackException.of(serviceName, (Exception) throwable);
}
private void recordFallbackMetric(String serviceName, String method) {
// 记录降级次数
Counter.builder("service.fallback")
.tag("service", serviceName)
.tag("method", method)
.register(Metrics.globalRegistry)
.increment();
}
}分布式事务异常
java
/**
* 分布式事务异常
*/
public class DistributedTransactionException extends BaseBusinessException {
private final String xid;
private final String branchId;
private final TransactionPhase phase;
public DistributedTransactionException(String xid, String branchId,
TransactionPhase phase, String message) {
super(message);
this.xid = xid;
this.branchId = branchId;
this.phase = phase;
this.setBusinessCode(5003);
}
public static DistributedTransactionException commitFailed(String xid, String branchId) {
return new DistributedTransactionException(xid, branchId,
TransactionPhase.COMMIT, "分布式事务提交失败");
}
public static DistributedTransactionException rollbackFailed(String xid, String branchId) {
return new DistributedTransactionException(xid, branchId,
TransactionPhase.ROLLBACK, "分布式事务回滚失败");
}
public enum TransactionPhase {
PREPARE, COMMIT, ROLLBACK
}
}异常恢复策略
重试机制
java
/**
* 可重试异常标记接口
*/
public interface RetryableException {
/**
* 最大重试次数
*/
default int getMaxRetries() {
return 3;
}
/**
* 重试间隔(毫秒)
*/
default long getRetryInterval() {
return 1000L;
}
/**
* 是否使用指数退避
*/
default boolean useExponentialBackoff() {
return true;
}
}
/**
* 可重试的业务异常
*/
public class RetryableServiceException extends ServiceException
implements RetryableException {
private final int maxRetries;
private final long retryInterval;
public RetryableServiceException(String message, int maxRetries, long retryInterval) {
super(message);
this.maxRetries = maxRetries;
this.retryInterval = retryInterval;
}
public static RetryableServiceException of(String message) {
return new RetryableServiceException(message, 3, 1000L);
}
@Override
public int getMaxRetries() {
return maxRetries;
}
@Override
public long getRetryInterval() {
return retryInterval;
}
}
/**
* 重试执行器
*/
@Component
@Slf4j
public class RetryExecutor {
/**
* 执行可重试操作
*/
public <T> T executeWithRetry(Supplier<T> operation, RetryableException config) {
int attempts = 0;
Exception lastException = null;
while (attempts < config.getMaxRetries()) {
try {
return operation.get();
} catch (Exception e) {
lastException = e;
attempts++;
if (attempts < config.getMaxRetries()) {
long delay = calculateDelay(config, attempts);
log.warn("操作失败,第{}次重试,延迟{}ms: {}",
attempts, delay, e.getMessage());
sleep(delay);
}
}
}
log.error("重试{}次后仍然失败", config.getMaxRetries());
throw ServiceException.of("操作失败,已重试" + config.getMaxRetries() + "次");
}
/**
* 计算重试延迟
*/
private long calculateDelay(RetryableException config, int attempt) {
if (config.useExponentialBackoff()) {
// 指数退避: delay * 2^(attempt-1)
return config.getRetryInterval() * (1L << (attempt - 1));
}
return config.getRetryInterval();
}
private void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}补偿机制
java
/**
* 可补偿异常接口
*/
public interface CompensatableException {
/**
* 获取补偿操作
*/
Runnable getCompensation();
/**
* 是否需要补偿
*/
default boolean needsCompensation() {
return true;
}
}
/**
* 补偿执行器
*/
@Component
@Slf4j
public class CompensationExecutor {
@Autowired
private CompensationLogMapper compensationLogMapper;
/**
* 执行带补偿的操作
*/
@Transactional(rollbackFor = Exception.class)
public <T> T executeWithCompensation(
Supplier<T> operation,
Runnable compensation,
String operationName) {
// 记录补偿日志
CompensationLog log = new CompensationLog();
log.setOperationName(operationName);
log.setStatus("PENDING");
log.setCreatedAt(LocalDateTime.now());
compensationLogMapper.insert(log);
try {
T result = operation.get();
// 操作成功,标记补偿日志为完成
log.setStatus("COMPLETED");
compensationLogMapper.updateById(log);
return result;
} catch (Exception e) {
// 执行补偿
try {
compensation.run();
log.setStatus("COMPENSATED");
} catch (Exception ce) {
log.setStatus("COMPENSATION_FAILED");
log.setErrorMessage(ce.getMessage());
}
compensationLogMapper.updateById(log);
throw e;
}
}
}降级策略
java
/**
* 降级策略接口
*/
public interface DegradationStrategy<T> {
/**
* 获取降级结果
*/
T degrade(Exception cause);
/**
* 判断是否应该降级
*/
boolean shouldDegrade(Exception e);
}
/**
* 默认值降级策略
*/
public class DefaultValueDegradation<T> implements DegradationStrategy<T> {
private final T defaultValue;
private final Set<Class<? extends Exception>> degradeOnExceptions;
public DefaultValueDegradation(T defaultValue,
Class<? extends Exception>... exceptions) {
this.defaultValue = defaultValue;
this.degradeOnExceptions = Set.of(exceptions);
}
@Override
public T degrade(Exception cause) {
return defaultValue;
}
@Override
public boolean shouldDegrade(Exception e) {
return degradeOnExceptions.stream()
.anyMatch(type -> type.isInstance(e));
}
}
/**
* 缓存降级策略
*/
@Component
public class CacheDegradation<T> implements DegradationStrategy<T> {
@Autowired
private RedisUtils redisUtils;
private final String cacheKeyPrefix;
public CacheDegradation(String cacheKeyPrefix) {
this.cacheKeyPrefix = cacheKeyPrefix;
}
@Override
@SuppressWarnings("unchecked")
public T degrade(Exception cause) {
// 从缓存获取上一次成功的结果
String cacheKey = cacheKeyPrefix + ":last_success";
return (T) redisUtils.getCacheObject(cacheKey);
}
@Override
public boolean shouldDegrade(Exception e) {
// 远程调用异常时降级
return e instanceof RemoteServiceException;
}
/**
* 缓存成功结果
*/
public void cacheSuccess(T result) {
String cacheKey = cacheKeyPrefix + ":last_success";
redisUtils.setCacheObject(cacheKey, result, Duration.ofHours(1));
}
}事务与异常处理
事务回滚规则
java
/**
* 事务回滚配置示例
*/
@Service
@Slf4j
public class TransactionalExceptionService {
/**
* 默认回滚规则:RuntimeException和Error会触发回滚
*/
@Transactional
public void defaultRollback() {
// ServiceException extends RuntimeException,会触发回滚
throw ServiceException.of("业务异常");
}
/**
* 指定回滚异常类型
*/
@Transactional(rollbackFor = ServiceException.class)
public void specificRollback() {
throw ServiceException.of("指定回滚");
}
/**
* 排除特定异常不回滚
*/
@Transactional(noRollbackFor = WarningException.class)
public void noRollbackForWarning() {
throw new WarningException("警告异常,不回滚");
}
/**
* 多异常规则
*/
@Transactional(
rollbackFor = {ServiceException.class, UserException.class},
noRollbackFor = {SkipException.class}
)
public void multipleRules() {
// 复杂回滚规则
}
}
/**
* 警告异常(不触发回滚)
*/
public class WarningException extends BaseBusinessException {
public WarningException(String message) {
super(message);
}
}异常后事务处理
java
/**
* 事务后异常处理器
*/
@Component
@Slf4j
public class TransactionExceptionHandler {
@Autowired
private ExceptionLogService exceptionLogService;
/**
* 事务提交后记录异常
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAfterCommit(ExceptionEvent event) {
// 事务已提交,安全地记录异常日志
exceptionLogService.saveLog(event.getException());
}
/**
* 事务回滚后处理
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleAfterRollback(ExceptionEvent event) {
log.warn("事务已回滚,异常: {}", event.getException().getMessage());
// 发送告警通知
notifyRollback(event);
}
/**
* 异常事件
*/
@Getter
@AllArgsConstructor
public static class ExceptionEvent {
private final Exception exception;
private final String transactionId;
}
}嵌套事务异常
java
/**
* 嵌套事务异常处理示例
*/
@Service
@Slf4j
public class NestedTransactionService {
@Autowired
private UserMapper userMapper;
@Autowired
private LogMapper logMapper;
/**
* 主事务
*/
@Transactional(rollbackFor = Exception.class)
public void mainTransaction(UserBo bo) {
// 保存用户
userMapper.insert(bo.toUser());
try {
// 嵌套事务:保存日志
saveLogInNewTransaction(bo);
} catch (Exception e) {
// 日志保存失败不影响主事务
log.warn("日志保存失败,但不影响主事务: {}", e.getMessage());
}
// 继续主事务逻辑...
}
/**
* 独立事务保存日志
*/
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void saveLogInNewTransaction(UserBo bo) {
OperationLog log = new OperationLog();
log.setOperation("CREATE_USER");
log.setData(JsonUtils.toJsonString(bo));
logMapper.insert(log);
// 即使这里抛异常,只回滚日志事务,不影响主事务
if (someCondition()) {
throw ServiceException.of("日志保存条件不满足");
}
}
}单元测试中的异常处理
异常断言测试
java
/**
* 异常测试示例
*/
@SpringBootTest
class ExceptionTest {
@Autowired
private UserService userService;
/**
* 测试异常抛出
*/
@Test
void testServiceException() {
// 使用 assertThrows 验证异常
ServiceException exception = assertThrows(
ServiceException.class,
() -> userService.getUser(null)
);
assertEquals("用户ID不能为空", exception.getMessage());
}
/**
* 测试异常类型和消息
*/
@Test
void testUserException() {
UserException exception = assertThrows(
UserException.class,
() -> userService.login("nonexistent", "password")
);
// 验证异常详情
assertEquals("user", exception.getModule());
assertEquals("user.account.not.exists", exception.getCode());
}
/**
* 测试业务码
*/
@Test
void testBusinessCode() {
ServiceException exception = assertThrows(
ServiceException.class,
() -> orderService.cancelOrder(123L)
);
assertEquals(4001, exception.getBusinessCode());
}
/**
* 测试异常链
*/
@Test
void testExceptionChain() {
Exception exception = assertThrows(
ServiceException.class,
() -> fileService.uploadFile(null)
);
// 验证根因
Throwable rootCause = ExceptionDebugUtils.getRootCause(exception);
assertInstanceOf(NullPointerException.class, rootCause);
}
}Mock异常测试
java
/**
* Mock异常测试
*/
@ExtendWith(MockitoExtension.class)
class MockExceptionTest {
@Mock
private UserMapper userMapper;
@InjectMocks
private UserService userService;
/**
* Mock数据库异常
*/
@Test
void testDatabaseException() {
// 模拟数据库异常
when(userMapper.selectById(anyLong()))
.thenThrow(new DataAccessException("DB连接失败") {});
ServiceException exception = assertThrows(
ServiceException.class,
() -> userService.getUser(1L)
);
assertTrue(exception.getMessage().contains("系统异常"));
}
/**
* 验证异常后的行为
*/
@Test
void testExceptionBehavior() {
when(userMapper.insert(any()))
.thenThrow(ServiceException.of("用户已存在"));
assertThrows(ServiceException.class,
() -> userService.createUser(new UserBo()));
// 验证异常后没有执行后续操作
verify(userMapper, never()).updateById(any());
}
}异常测试工具类
java
/**
* 异常测试工具类
*/
public final class ExceptionTestUtils {
private ExceptionTestUtils() {}
/**
* 断言抛出指定异常并验证消息
*/
public static <T extends Exception> T assertThrowsWithMessage(
Class<T> exceptionClass,
String expectedMessage,
Executable executable) {
T exception = assertThrows(exceptionClass, executable);
assertEquals(expectedMessage, exception.getMessage());
return exception;
}
/**
* 断言抛出ServiceException并验证业务码
*/
public static ServiceException assertServiceException(
int expectedCode,
Executable executable) {
ServiceException exception = assertThrows(ServiceException.class, executable);
assertEquals(expectedCode, exception.getBusinessCode());
return exception;
}
/**
* 断言抛出UserException并验证错误码
*/
public static UserException assertUserException(
String expectedCode,
Executable executable) {
UserException exception = assertThrows(UserException.class, executable);
assertEquals(expectedCode, exception.getCode());
return exception;
}
/**
* 断言不抛出异常
*/
public static void assertNoException(Executable executable) {
assertDoesNotThrow(executable);
}
}注意事项
1. 性能考虑
- 异常创建有性能开销,避免在循环中频繁抛出
- 使用静态工厂方法减少对象创建
- 异常信息避免过长,影响日志性能
- 堆栈跟踪获取代价较大,非必要不调用
getStackTrace() - 考虑使用异常池化技术处理高频异常场景
java
// ❌ 不推荐:循环中抛出异常
for (Item item : items) {
if (!validate(item)) {
throw ServiceException.of("验证失败: " + item.getId());
}
}
// ✅ 推荐:收集错误后统一处理
List<String> errors = new ArrayList<>();
for (Item item : items) {
if (!validate(item)) {
errors.add("验证失败: " + item.getId());
}
}
if (!errors.isEmpty()) {
throw ServiceException.of("批量验证失败: " + String.join(", ", errors));
}2. 内存管理
- 避免在异常中保存大对象引用
- 及时释放异常相关资源
- 注意异常对象的生命周期
- 异常消息字符串避免拼接大量数据
- 使用弱引用存储异常上下文中的临时对象
java
// ❌ 不推荐:保存大对象
public class BadException extends ServiceException {
private byte[] largeData; // 可能导致内存泄漏
}
// ✅ 推荐:只保存必要信息
public class GoodException extends ServiceException {
private String dataId; // 只保存ID,需要时再查询
}3. 安全考虑
- 不要在异常消息中暴露敏感信息
- 对外接口的异常消息要做脱敏处理
- 记录异常日志时注意数据安全
- 避免在异常消息中包含SQL语句、密码、密钥等敏感数据
- 生产环境关闭详细堆栈信息返回
java
// ❌ 不推荐:暴露敏感信息
throw ServiceException.of("用户密码错误: " + password);
throw ServiceException.of("SQL执行失败: " + sql);
// ✅ 推荐:脱敏处理
throw ServiceException.of("用户认证失败");
throw ServiceException.of("数据查询异常,请联系管理员");4. 国际化注意
- 异常消息的参数顺序在不同语言中可能不同
- 使用占位符时要考虑语法结构差异
- 测试多语言环境下的异常显示
- 确保所有国际化键都有对应的翻译
- 默认消息应该是通用的英文描述
java
// 中文:用户 {0} 登录失败,原因:{1}
// 英文:Login failed for user {0}, reason: {1}
// 日文:ユーザー{0}のログインに失敗しました。理由:{1}
throw UserException.of("user.login.failed", username, reason);5. 向后兼容
- 新增异常类型要保持API稳定
- 错误码一旦发布不要随意修改
- 消息格式变更要考虑客户端兼容性
- 弃用旧异常时提供迁移路径
- 版本升级时保持异常行为一致
java
// 保持向后兼容的异常升级
@Deprecated
public class OldException extends ServiceException {
// 保留旧异常,标记废弃
}
public class NewException extends ServiceException {
// 新异常提供更多功能
public static NewException fromOld(OldException old) {
// 提供从旧异常转换的方法
return new NewException(old.getMessage());
}
}6. 异常文档化
- 在方法签名中声明可能抛出的异常
- 使用 Javadoc 描述异常触发条件
- 维护异常错误码对照表
- 为 API 消费者提供异常处理指南
java
/**
* 用户登录
*
* @param username 用户名
* @param password 密码
* @return 登录用户信息
* @throws UserException 当用户不存在时抛出 user.account.not.exists
* @throws UserException 当密码错误时抛出 user.password.mismatch
* @throws UserException 当账户被锁定时抛出 user.password.retry.locked
*/
public LoginUser login(String username, String password) throws UserException {
// 实现...
}7. 日志级别规范
- ERROR: 系统级异常、数据库异常、外部服务不可用
- WARN: 业务异常、参数校验失败、可恢复的错误
- INFO: 正常的业务流程异常(如用户名已存在)
- DEBUG: 调试信息、异常堆栈详情
java
@ExceptionHandler(Exception.class)
public R<Void> handleException(Exception e) {
if (e instanceof ServiceException) {
log.warn("业务异常: {}", e.getMessage());
} else if (e instanceof UserException) {
log.info("用户异常: {}", e.getMessage());
} else {
log.error("系统异常: ", e);
}
return R.fail(e.getMessage());
}