ExceptionHandler 异常处理
RuoYi-Plus 提供的全局异常处理机制,统一处理应用中的各种异常情况。
📋 异常体系
异常类型
java
// 基础业务异常
public class ServiceException extends RuntimeException {
private Integer code;
private String message;
private String detailMessage;
public ServiceException() {}
public ServiceException(String message) {
this.message = message;
}
public ServiceException(String message, Integer code) {
this.message = message;
this.code = code;
}
public ServiceException(String message, Throwable cause) {
super(message, cause);
this.message = message;
}
// getter/setter...
}
// 用户异常
public class UserException extends ServiceException {
public UserException(String message) {
super(message);
}
public UserException(String message, Integer code) {
super(message, code);
}
}
// 认证异常
public class AuthException extends ServiceException {
public AuthException(String message) {
super(message);
}
public AuthException(String message, Integer code) {
super(message, code);
}
}
// 权限异常
public class PermissionException extends ServiceException {
public PermissionException(String message) {
super(message);
}
public PermissionException(String message, Integer code) {
super(message, code);
}
}异常码定义
java
// 异常码常量
public interface ErrorCode {
// 系统异常 (1000-1999)
int SYSTEM_ERROR = 1000;
int SYSTEM_BUSY = 1001;
int SYSTEM_TIMEOUT = 1002;
// 参数异常 (2000-2999)
int PARAM_ERROR = 2000;
int PARAM_MISSING = 2001;
int PARAM_INVALID = 2002;
// 认证异常 (3000-3999)
int AUTH_FAILED = 3000;
int TOKEN_INVALID = 3001;
int TOKEN_EXPIRED = 3002;
// 权限异常 (4000-4999)
int PERMISSION_DENIED = 4000;
int ACCESS_FORBIDDEN = 4001;
// 业务异常 (5000-9999)
int USER_NOT_FOUND = 5000;
int USER_EXISTS = 5001;
int PASSWORD_ERROR = 5002;
int ACCOUNT_DISABLED = 5003;
}
// 异常信息枚举
public enum ErrorCodeEnum {
SUCCESS(200, "操作成功"),
SYSTEM_ERROR(ErrorCode.SYSTEM_ERROR, "系统异常"),
PARAM_ERROR(ErrorCode.PARAM_ERROR, "参数错误"),
AUTH_FAILED(ErrorCode.AUTH_FAILED, "认证失败"),
PERMISSION_DENIED(ErrorCode.PERMISSION_DENIED, "权限不足"),
USER_NOT_FOUND(ErrorCode.USER_NOT_FOUND, "用户不存在");
private final Integer code;
private final String message;
ErrorCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
// getter...
}🎯 全局异常处理器
核心处理器
java
/**
* 全局异常处理器
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(ServiceException.class)
public R<Void> handleServiceException(ServiceException e, HttpServletRequest request) {
Integer code = e.getCode();
if (ObjectUtil.isNull(code)) {
code = HttpStatus.INTERNAL_SERVER_ERROR.value();
}
log.error("请求地址'{}',发生业务异常.", request.getRequestURI(), e);
return R.fail(code, e.getMessage());
}
/**
* 参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public R<Void> handleValidException(MethodArgumentNotValidException e) {
log.error("参数验证异常", e);
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return R.fail(ErrorCode.PARAM_ERROR, message);
}
/**
* 参数绑定异常
*/
@ExceptionHandler(BindException.class)
public R<Void> handleBindException(BindException e) {
log.error("参数绑定异常", e);
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return R.fail(ErrorCode.PARAM_ERROR, message);
}
/**
* 参数类型不匹配异常
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public R<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
log.error("参数类型不匹配异常", e);
String message = "参数类型不匹配: " + e.getName();
return R.fail(ErrorCode.PARAM_ERROR, message);
}
/**
* 请求方式不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public R<Void> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
return R.fail(HttpStatus.METHOD_NOT_ALLOWED.value(), e.getMessage());
}
/**
* 权限异常
*/
@ExceptionHandler(NotPermissionException.class)
public R<Void> handleNotPermissionException(NotPermissionException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
return R.fail(ErrorCode.PERMISSION_DENIED, "没有访问权限,请联系管理员授权");
}
/**
* 认证异常
*/
@ExceptionHandler(NotLoginException.class)
public R<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
return R.fail(ErrorCode.AUTH_FAILED, "认证失败,无法访问系统资源");
}
/**
* 数据库异常
*/
@ExceptionHandler(SQLException.class)
public R<Void> handleSQLException(SQLException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生数据库异常.", requestURI, e);
return R.fail("数据库操作异常,请联系管理员");
}
/**
* 空指针异常
*/
@ExceptionHandler(NullPointerException.class)
public R<Void> handleNullPointerException(NullPointerException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生空指针异常.", requestURI, e);
return R.fail("空指针异常");
}
/**
* 系统异常
*/
@ExceptionHandler(Exception.class)
public R<Void> handleException(Exception e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生系统异常.", requestURI, e);
return R.fail("系统异常,请联系管理员");
}
/**
* 自定义验证异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public R<Void> constraintViolationException(ConstraintViolationException e) {
log.error("参数验证异常", e);
String message = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
return R.fail(ErrorCode.PARAM_ERROR, message);
}
/**
* 演示模式异常
*/
@ExceptionHandler(DemoModeException.class)
public R<Void> handleDemoModeException(DemoModeException e) {
return R.fail("演示模式,不允许操作");
}
}异步异常处理
java
/**
* 异步异常处理器
*/
@Component
@Slf4j
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {
log.error("异步任务执行异常 - Method: {}, Exception: {}",
method.getName(), throwable.getMessage(), throwable);
// 记录异常信息到数据库
recordAsyncException(method, throwable, objects);
// 发送异常通知
notifyException(method, throwable);
}
private void recordAsyncException(Method method, Throwable throwable, Object... params) {
try {
AsyncExceptionLog log = new AsyncExceptionLog();
log.setMethodName(method.getName());
log.setClassName(method.getDeclaringClass().getName());
log.setExceptionMessage(throwable.getMessage());
log.setExceptionType(throwable.getClass().getName());
log.setParameters(JSON.toJSONString(params));
log.setStackTrace(ExceptionUtil.stacktraceToString(throwable));
log.setCreateTime(new Date());
// 保存到数据库
SpringUtils.getBean(IAsyncExceptionLogService.class).save(log);
} catch (Exception e) {
log.error("记录异步异常失败", e);
}
}
private void notifyException(Method method, Throwable throwable) {
// 发送邮件通知
// 发送短信通知
// 推送到监控系统
}
}
/**
* 异步配置
*/
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler();
}
}🔧 异常使用
业务层异常
java
@Service
public class UserServiceImpl implements IUserService {
/**
* 新增用户
*/
public Long insertUser(UserBo bo) {
SysUser user = BeanUtil.toBean(bo, SysUser.class);
// 验证用户名唯一性
if (!checkUserNameUnique(user)) {
throw new ServiceException("用户名已存在");
}
// 验证邮箱唯一性
if (StringUtils.isNotEmpty(user.getEmail()) && !checkEmailUnique(user)) {
throw new ServiceException("邮箱地址已存在");
}
// 验证手机号唯一性
if (StringUtils.isNotEmpty(user.getPhonenumber()) && !checkPhoneUnique(user)) {
throw new ServiceException("手机号码已存在");
}
try {
return baseMapper.insertUser(user);
} catch (Exception e) {
log.error("新增用户失败", e);
throw new ServiceException("新增用户失败", e);
}
}
/**
* 修改用户
*/
public Boolean updateUser(UserBo bo) {
SysUser user = BeanUtil.toBean(bo, SysUser.class);
// 验证用户是否存在
SysUser existUser = baseMapper.selectById(user.getUserId());
if (ObjectUtil.isNull(existUser)) {
throw new UserException("用户不存在", ErrorCode.USER_NOT_FOUND);
}
// 验证用户状态
if (!"0".equals(existUser.getStatus())) {
throw new UserException("账户已被禁用", ErrorCode.ACCOUNT_DISABLED);
}
// 系统管理员不能被修改
if (existUser.isAdmin()) {
throw new PermissionException("不允许修改系统管理员", ErrorCode.PERMISSION_DENIED);
}
return baseMapper.updateById(user) > 0;
}
/**
* 删除用户
*/
public Boolean deleteUser(Long userId) {
// 验证用户是否存在
SysUser user = baseMapper.selectById(userId);
if (ObjectUtil.isNull(user)) {
throw new UserException("用户不存在", ErrorCode.USER_NOT_FOUND);
}
// 系统管理员不能被删除
if (user.isAdmin()) {
throw new PermissionException("不允许删除系统管理员", ErrorCode.PERMISSION_DENIED);
}
// 检查用户是否有关联数据
if (hasRelatedData(userId)) {
throw new ServiceException("用户存在关联数据,不能删除");
}
return baseMapper.deleteById(userId) > 0;
}
/**
* 重置密码
*/
public Boolean resetPassword(String userName, String password) {
SysUser user = baseMapper.selectUserByUserName(userName);
if (ObjectUtil.isNull(user)) {
throw new UserException("用户不存在", ErrorCode.USER_NOT_FOUND);
}
if (!"0".equals(user.getStatus())) {
throw new UserException("账户已被禁用,无法重置密码", ErrorCode.ACCOUNT_DISABLED);
}
// 验证密码复杂度
if (!PasswordUtils.isValidPassword(password)) {
throw new ServiceException("密码不符合复杂度要求");
}
user.setPassword(SecurityUtils.encryptPassword(password));
user.setUpdateTime(new Date());
return baseMapper.updateById(user) > 0;
}
private boolean hasRelatedData(Long userId) {
// 检查是否有关联的数据
return false;
}
}Controller层异常
java
@RestController
@RequestMapping("/system/user")
@Validated
public class SysUserController extends BaseController {
@Resource
private IUserService userService;
/**
* 获取用户详细信息
*/
@SaCheckPermission("system:user:query")
@GetMapping("/{userId}")
public R<UserVo> getInfo(@PathVariable("userId") @NotNull(message = "用户ID不能为空") Long userId) {
try {
UserVo user = userService.selectUserById(userId);
return R.ok(user);
} catch (UserException e) {
return R.fail(e.getCode(), e.getMessage());
} catch (Exception e) {
log.error("获取用户信息失败", e);
return R.fail("获取用户信息失败");
}
}
/**
* 新增用户
*/
@SaCheckPermission("system:user:add")
@Log(title = "用户管理", operType = DictOperType.INSERT)
@RepeatSubmit()
@PostMapping
public R<Void> add(@Validated(AddGroup.class) @RequestBody UserBo user) {
if (!userService.checkUserNameUnique(user)) {
return R.fail("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
}
if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) {
return R.fail("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
}
if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) {
return R.fail("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
}
user.setCreateBy(getUsername());
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
return toAjax(userService.insertUser(user));
}
/**
* 批量删除用户
*/
@SaCheckPermission("system:user:remove")
@Log(title = "用户管理", operType = DictOperType.DELETE)
@DeleteMapping("/{userIds}")
public R<Void> remove(@PathVariable Long[] userIds) {
if (ArrayUtil.contains(userIds, getUserId())) {
return R.fail("当前用户不能删除");
}
return toAjax(userService.deleteUserByIds(userIds));
}
}🚨 异常监控
异常统计
java
/**
* 异常统计服务
*/
@Service
@Slf4j
public class ExceptionStatisticsService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
private static final String EXCEPTION_COUNT_KEY = "exception:count:";
private static final String EXCEPTION_DETAIL_KEY = "exception:detail:";
/**
* 记录异常
*/
public void recordException(String requestUri, Exception exception) {
String today = DateUtil.today();
String exceptionType = exception.getClass().getSimpleName();
// 统计今日异常总数
String totalKey = EXCEPTION_COUNT_KEY + "total:" + today;
redisTemplate.opsForValue().increment(totalKey);
redisTemplate.expire(totalKey, Duration.ofDays(7));
// 统计今日各类型异常数量
String typeKey = EXCEPTION_COUNT_KEY + "type:" + exceptionType + ":" + today;
redisTemplate.opsForValue().increment(typeKey);
redisTemplate.expire(typeKey, Duration.ofDays(7));
// 统计今日各接口异常数量
String uriKey = EXCEPTION_COUNT_KEY + "uri:" + requestUri + ":" + today;
redisTemplate.opsForValue().increment(uriKey);
redisTemplate.expire(uriKey, Duration.ofDays(7));
// 记录异常详情
recordExceptionDetail(requestUri, exception);
}
private void recordExceptionDetail(String requestUri, Exception exception) {
try {
ExceptionDetail detail = new ExceptionDetail();
detail.setRequestUri(requestUri);
detail.setExceptionType(exception.getClass().getName());
detail.setExceptionMessage(exception.getMessage());
detail.setStackTrace(ExceptionUtil.stacktraceToString(exception));
detail.setOccurTime(new Date());
String detailKey = EXCEPTION_DETAIL_KEY + System.currentTimeMillis();
redisTemplate.opsForValue().set(detailKey, detail, Duration.ofDays(1));
} catch (Exception e) {
log.error("记录异常详情失败", e);
}
}
/**
* 获取异常统计
*/
public ExceptionStatistics getStatistics(String date) {
ExceptionStatistics statistics = new ExceptionStatistics();
// 获取总异常数
String totalKey = EXCEPTION_COUNT_KEY + "total:" + date;
Object totalCount = redisTemplate.opsForValue().get(totalKey);
statistics.setTotalCount(totalCount != null ? (Integer) totalCount : 0);
// 获取各类型异常统计
Set<String> typeKeys = redisTemplate.keys(EXCEPTION_COUNT_KEY + "type:*:" + date);
Map<String, Integer> typeStatistics = new HashMap<>();
if (CollUtil.isNotEmpty(typeKeys)) {
for (String key : typeKeys) {
String type = key.split(":")[2];
Object count = redisTemplate.opsForValue().get(key);
typeStatistics.put(type, count != null ? (Integer) count : 0);
}
}
statistics.setTypeStatistics(typeStatistics);
return statistics;
}
}
/**
* 异常详情
*/
@Data
public class ExceptionDetail {
private String requestUri;
private String exceptionType;
private String exceptionMessage;
private String stackTrace;
private Date occurTime;
}
/**
* 异常统计
*/
@Data
public class ExceptionStatistics {
private Integer totalCount;
private Map<String, Integer> typeStatistics;
private Map<String, Integer> uriStatistics;
}异常告警
java
/**
* 异常告警服务
*/
@Service
@Slf4j
public class ExceptionAlertService {
@Resource
private IMailService mailService;
@Resource
private ISmsService smsService;
@Value("${alert.exception.threshold:10}")
private Integer exceptionThreshold;
@Value("${alert.exception.email}")
private String alertEmail;
@Value("${alert.exception.phone}")
private String alertPhone;
/**
* 检查是否需要告警
*/
@Scheduled(fixedRate = 300000) // 每5分钟检查一次
public void checkExceptionAlert() {
String today = DateUtil.today();
String currentHour = DateUtil.format(new Date(), "yyyy-MM-dd:HH");
// 检查最近一小时的异常数量
String hourKey = EXCEPTION_COUNT_KEY + "hour:" + currentHour;
Object hourCount = redisTemplate.opsForValue().get(hourKey);
Integer count = hourCount != null ? (Integer) hourCount : 0;
if (count >= exceptionThreshold) {
sendAlert(count, currentHour);
}
}
private void sendAlert(Integer count, String timeRange) {
String subject = "系统异常告警";
String content = String.format("时间范围: %s\n异常数量: %d\n已超过告警阈值: %d",
timeRange, count, exceptionThreshold);
// 发送邮件告警
if (StringUtils.isNotBlank(alertEmail)) {
try {
mailService.sendSimpleMail(alertEmail, subject, content);
} catch (Exception e) {
log.error("发送异常告警邮件失败", e);
}
}
// 发送短信告警
if (StringUtils.isNotBlank(alertPhone)) {
try {
smsService.sendAlert(alertPhone, content);
} catch (Exception e) {
log.error("发送异常告警短信失败", e);
}
}
}
}🌐 异常国际化
异常消息国际化配置
java
/**
* 异常消息国际化服务
*/
@Service
@RequiredArgsConstructor
public class ExceptionMessageService {
private final MessageSource messageSource;
/**
* 获取国际化异常消息
*/
public String getMessage(String code) {
return getMessage(code, null);
}
/**
* 获取带参数的国际化异常消息
*/
public String getMessage(String code, Object[] args) {
try {
Locale locale = LocaleContextHolder.getLocale();
return messageSource.getMessage(code, args, locale);
} catch (NoSuchMessageException e) {
return code;
}
}
/**
* 获取指定语言的异常消息
*/
public String getMessage(String code, Object[] args, Locale locale) {
try {
return messageSource.getMessage(code, args, locale);
} catch (NoSuchMessageException e) {
return code;
}
}
}
/**
* 国际化异常消息配置
*/
@Configuration
public class MessageSourceConfig {
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasenames(
"classpath:i18n/messages",
"classpath:i18n/exceptions"
);
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(3600);
messageSource.setFallbackToSystemLocale(true);
return messageSource;
}
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
resolver.setSupportedLocales(Arrays.asList(
Locale.SIMPLIFIED_CHINESE,
Locale.US,
Locale.JAPAN
));
return resolver;
}
}国际化消息文件
properties
# exceptions_zh_CN.properties
error.user.not.found=用户不存在
error.user.disabled=用户账号已被禁用
error.user.password.error=密码错误,还有{0}次尝试机会
error.auth.failed=认证失败,请重新登录
error.permission.denied=您没有权限执行此操作
error.param.missing=参数{0}不能为空
error.param.invalid=参数{0}格式不正确
error.system.busy=系统繁忙,请稍后重试
error.rate.limit.exceeded=请求过于频繁,请{0}秒后重试
# exceptions_en_US.properties
error.user.not.found=User not found
error.user.disabled=User account has been disabled
error.user.password.error=Wrong password, {0} attempts remaining
error.auth.failed=Authentication failed, please login again
error.permission.denied=You do not have permission to perform this action
error.param.missing=Parameter {0} is required
error.param.invalid=Parameter {0} format is invalid
error.system.busy=System is busy, please try again later
error.rate.limit.exceeded=Too many requests, please retry in {0} seconds支持国际化的异常类
java
/**
* 支持国际化的业务异常
*/
public class I18nServiceException extends ServiceException {
private String messageKey;
private Object[] args;
public I18nServiceException(String messageKey) {
super(messageKey);
this.messageKey = messageKey;
}
public I18nServiceException(String messageKey, Object... args) {
super(messageKey);
this.messageKey = messageKey;
this.args = args;
}
public I18nServiceException(String messageKey, Integer code) {
super(messageKey, code);
this.messageKey = messageKey;
}
public I18nServiceException(String messageKey, Integer code, Object... args) {
super(messageKey, code);
this.messageKey = messageKey;
this.args = args;
}
public String getMessageKey() {
return messageKey;
}
public Object[] getArgs() {
return args;
}
}
/**
* 国际化异常处理器
*/
@RestControllerAdvice
@RequiredArgsConstructor
@Slf4j
public class I18nExceptionHandler {
private final ExceptionMessageService messageService;
@ExceptionHandler(I18nServiceException.class)
public R<Void> handleI18nException(I18nServiceException e, HttpServletRequest request) {
String message = messageService.getMessage(e.getMessageKey(), e.getArgs());
Integer code = e.getCode() != null ? e.getCode() : HttpStatus.INTERNAL_SERVER_ERROR.value();
log.error("请求地址'{}',发生业务异常: {}", request.getRequestURI(), message, e);
return R.fail(code, message);
}
}🔗 异常链处理
异常链追踪
java
/**
* 异常链追踪工具
*/
public class ExceptionChainTracer {
/**
* 获取异常链中的根因异常
*/
public static Throwable getRootCause(Throwable throwable) {
Throwable cause = throwable.getCause();
if (cause == null) {
return throwable;
}
// 防止循环引用导致的死循环
Set<Throwable> visited = new HashSet<>();
while (cause != null && !visited.contains(cause)) {
visited.add(cause);
Throwable nextCause = cause.getCause();
if (nextCause == null) {
return cause;
}
cause = nextCause;
}
return throwable;
}
/**
* 获取完整的异常链
*/
public static List<Throwable> getExceptionChain(Throwable throwable) {
List<Throwable> chain = new ArrayList<>();
Set<Throwable> visited = new HashSet<>();
Throwable current = throwable;
while (current != null && !visited.contains(current)) {
chain.add(current);
visited.add(current);
current = current.getCause();
}
return chain;
}
/**
* 格式化异常链
*/
public static String formatExceptionChain(Throwable throwable) {
List<Throwable> chain = getExceptionChain(throwable);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < chain.size(); i++) {
Throwable t = chain.get(i);
if (i > 0) {
sb.append("\n Caused by: ");
}
sb.append(t.getClass().getName())
.append(": ")
.append(t.getMessage());
}
return sb.toString();
}
/**
* 检查异常链中是否包含指定类型的异常
*/
public static <T extends Throwable> boolean containsException(
Throwable throwable, Class<T> exceptionType) {
return findException(throwable, exceptionType) != null;
}
/**
* 在异常链中查找指定类型的异常
*/
@SuppressWarnings("unchecked")
public static <T extends Throwable> T findException(
Throwable throwable, Class<T> exceptionType) {
List<Throwable> chain = getExceptionChain(throwable);
for (Throwable t : chain) {
if (exceptionType.isInstance(t)) {
return (T) t;
}
}
return null;
}
}
/**
* 增强的异常处理器(支持异常链分析)
*/
@RestControllerAdvice
@RequiredArgsConstructor
@Slf4j
public class EnhancedExceptionHandler {
@ExceptionHandler(Exception.class)
public R<Void> handleException(Exception e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
// 分析异常链
Throwable rootCause = ExceptionChainTracer.getRootCause(e);
String chainInfo = ExceptionChainTracer.formatExceptionChain(e);
log.error("请求地址'{}',发生异常.\n异常链: {}", requestURI, chainInfo, e);
// 根据根因异常类型返回不同的错误信息
if (rootCause instanceof SQLException) {
return R.fail("数据库操作异常");
} else if (rootCause instanceof IOException) {
return R.fail("IO操作异常");
} else if (rootCause instanceof TimeoutException) {
return R.fail("操作超时,请稍后重试");
}
return R.fail("系统异常,请联系管理员");
}
}异常包装与转换
java
/**
* 异常包装器
*/
public class ExceptionWrapper {
/**
* 包装为业务异常
*/
public static ServiceException wrapAsServiceException(Throwable throwable) {
if (throwable instanceof ServiceException) {
return (ServiceException) throwable;
}
// 根据原始异常类型决定错误码和消息
if (throwable instanceof IllegalArgumentException) {
return new ServiceException("参数错误: " + throwable.getMessage(),
ErrorCode.PARAM_ERROR);
}
if (throwable instanceof AccessDeniedException) {
return new PermissionException("权限不足: " + throwable.getMessage(),
ErrorCode.PERMISSION_DENIED);
}
if (throwable instanceof DataAccessException) {
return new ServiceException("数据访问异常",
ErrorCode.SYSTEM_ERROR);
}
return new ServiceException("系统异常: " + throwable.getMessage(),
ErrorCode.SYSTEM_ERROR);
}
/**
* 安全地执行操作,将异常转换为业务异常
*/
public static <T> T executeWithWrapping(Supplier<T> supplier) {
try {
return supplier.get();
} catch (Exception e) {
throw wrapAsServiceException(e);
}
}
/**
* 安全地执行操作(无返回值)
*/
public static void executeWithWrapping(Runnable action) {
try {
action.run();
} catch (Exception e) {
throw wrapAsServiceException(e);
}
}
}
/**
* 异常转换器接口
*/
public interface ExceptionTransformer {
/**
* 是否支持该异常类型
*/
boolean supports(Throwable throwable);
/**
* 转换异常
*/
ServiceException transform(Throwable throwable);
}
/**
* SQL异常转换器
*/
@Component
public class SqlExceptionTransformer implements ExceptionTransformer {
@Override
public boolean supports(Throwable throwable) {
return throwable instanceof SQLException ||
ExceptionChainTracer.containsException(throwable, SQLException.class);
}
@Override
public ServiceException transform(Throwable throwable) {
SQLException sqlException = ExceptionChainTracer.findException(
throwable, SQLException.class);
if (sqlException != null) {
String sqlState = sqlException.getSQLState();
int errorCode = sqlException.getErrorCode();
// 根据SQL状态码返回友好消息
if (sqlState != null && sqlState.startsWith("23")) {
return new ServiceException("数据违反约束条件", ErrorCode.PARAM_ERROR);
}
if (errorCode == 1062) { // MySQL重复键错误
return new ServiceException("数据已存在,请勿重复提交", ErrorCode.PARAM_ERROR);
}
}
return new ServiceException("数据库操作异常", ErrorCode.SYSTEM_ERROR);
}
}
/**
* 异常转换器链
*/
@Component
@RequiredArgsConstructor
public class ExceptionTransformerChain {
private final List<ExceptionTransformer> transformers;
/**
* 转换异常
*/
public ServiceException transform(Throwable throwable) {
for (ExceptionTransformer transformer : transformers) {
if (transformer.supports(throwable)) {
return transformer.transform(throwable);
}
}
return ExceptionWrapper.wrapAsServiceException(throwable);
}
}📊 异常分级处理
异常级别定义
java
/**
* 异常级别枚举
*/
public enum ExceptionLevel {
/**
* 严重:系统无法正常运行
*/
CRITICAL(1, "严重"),
/**
* 错误:功能无法正常使用
*/
ERROR(2, "错误"),
/**
* 警告:可能影响用户体验
*/
WARNING(3, "警告"),
/**
* 信息:一般业务异常
*/
INFO(4, "信息");
private final int level;
private final String description;
ExceptionLevel(int level, String description) {
this.level = level;
this.description = description;
}
public int getLevel() {
return level;
}
public String getDescription() {
return description;
}
}
/**
* 可分级的异常接口
*/
public interface LeveledException {
/**
* 获取异常级别
*/
ExceptionLevel getLevel();
/**
* 是否需要告警
*/
default boolean needsAlert() {
return getLevel().getLevel() <= ExceptionLevel.ERROR.getLevel();
}
/**
* 是否需要记录到数据库
*/
default boolean needsPersistence() {
return getLevel().getLevel() <= ExceptionLevel.WARNING.getLevel();
}
}
/**
* 分级业务异常
*/
public class LeveledServiceException extends ServiceException implements LeveledException {
private final ExceptionLevel level;
public LeveledServiceException(String message, ExceptionLevel level) {
super(message);
this.level = level;
}
public LeveledServiceException(String message, Integer code, ExceptionLevel level) {
super(message, code);
this.level = level;
}
@Override
public ExceptionLevel getLevel() {
return level;
}
public static LeveledServiceException critical(String message) {
return new LeveledServiceException(message, ExceptionLevel.CRITICAL);
}
public static LeveledServiceException error(String message) {
return new LeveledServiceException(message, ExceptionLevel.ERROR);
}
public static LeveledServiceException warning(String message) {
return new LeveledServiceException(message, ExceptionLevel.WARNING);
}
public static LeveledServiceException info(String message) {
return new LeveledServiceException(message, ExceptionLevel.INFO);
}
}分级异常处理器
java
/**
* 分级异常处理器
*/
@RestControllerAdvice
@RequiredArgsConstructor
@Slf4j
public class LeveledExceptionHandler {
private final ExceptionAlertService alertService;
private final ExceptionPersistenceService persistenceService;
private final MeterRegistry meterRegistry;
@ExceptionHandler(LeveledServiceException.class)
public R<Void> handleLeveledException(LeveledServiceException e, HttpServletRequest request) {
ExceptionLevel level = e.getLevel();
String requestUri = request.getRequestURI();
// 根据级别记录不同级别的日志
switch (level) {
case CRITICAL:
log.error("[CRITICAL] 请求'{}',发生严重异常: {}", requestUri, e.getMessage(), e);
break;
case ERROR:
log.error("[ERROR] 请求'{}',发生错误: {}", requestUri, e.getMessage(), e);
break;
case WARNING:
log.warn("[WARNING] 请求'{}',发生警告: {}", requestUri, e.getMessage());
break;
case INFO:
default:
log.info("[INFO] 请求'{}',发生业务异常: {}", requestUri, e.getMessage());
break;
}
// 记录指标
meterRegistry.counter("exception.count",
"level", level.name(),
"uri", requestUri
).increment();
// 判断是否需要告警
if (e.needsAlert()) {
alertService.sendAlert(e, request);
}
// 判断是否需要持久化
if (e.needsPersistence()) {
persistenceService.save(e, request);
}
Integer code = e.getCode() != null ? e.getCode() : HttpStatus.INTERNAL_SERVER_ERROR.value();
return R.fail(code, e.getMessage());
}
}
/**
* 异常持久化服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ExceptionPersistenceService {
private final ExceptionLogMapper exceptionLogMapper;
/**
* 保存异常记录
*/
@Async
public void save(LeveledServiceException e, HttpServletRequest request) {
try {
ExceptionLog logEntity = new ExceptionLog();
logEntity.setLevel(e.getLevel().name());
logEntity.setMessage(e.getMessage());
logEntity.setCode(e.getCode());
logEntity.setRequestUri(request.getRequestURI());
logEntity.setRequestMethod(request.getMethod());
logEntity.setStackTrace(ExceptionUtil.stacktraceToString(e));
logEntity.setUserAgent(request.getHeader("User-Agent"));
logEntity.setIpAddress(ServletUtils.getClientIP(request));
logEntity.setUserId(SecurityUtils.getUserIdOrNull());
logEntity.setCreateTime(LocalDateTime.now());
exceptionLogMapper.insert(logEntity);
} catch (Exception ex) {
log.error("保存异常日志失败", ex);
}
}
}🚦 限流异常处理
限流异常定义
java
/**
* 限流异常
*/
public class RateLimitException extends ServiceException {
private String limitKey;
private long waitTime;
public RateLimitException(String message) {
super(message, ErrorCode.RATE_LIMIT_EXCEEDED);
}
public RateLimitException(String message, String limitKey, long waitTime) {
super(message, ErrorCode.RATE_LIMIT_EXCEEDED);
this.limitKey = limitKey;
this.waitTime = waitTime;
}
public String getLimitKey() {
return limitKey;
}
public long getWaitTime() {
return waitTime;
}
}
/**
* 限流异常处理器
*/
@RestControllerAdvice
@Slf4j
public class RateLimitExceptionHandler {
@ExceptionHandler(RateLimitException.class)
public R<Void> handleRateLimitException(RateLimitException e, HttpServletRequest request) {
String requestUri = request.getRequestURI();
String clientIp = ServletUtils.getClientIP(request);
log.warn("请求被限流 - URI: {}, IP: {}, Key: {}, 等待时间: {}秒",
requestUri, clientIp, e.getLimitKey(), e.getWaitTime());
// 设置重试响应头
HttpServletResponse response = ServletUtils.getResponse();
response.setHeader("Retry-After", String.valueOf(e.getWaitTime()));
response.setHeader("X-RateLimit-Limit-Key", e.getLimitKey());
return R.fail(HttpStatus.TOO_MANY_REQUESTS.value(),
String.format("请求过于频繁,请%d秒后重试", e.getWaitTime()));
}
}限流切面实现
java
/**
* 限流注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 限流key前缀
*/
String key() default "";
/**
* 限流类型
*/
LimitType limitType() default LimitType.DEFAULT;
/**
* 时间窗口(秒)
*/
int time() default 60;
/**
* 最大请求次数
*/
int count() default 100;
/**
* 提示消息
*/
String message() default "请求过于频繁,请稍后再试";
}
/**
* 限流类型
*/
public enum LimitType {
DEFAULT, // 默认(IP + 接口)
IP, // 按IP限流
USER, // 按用户限流
INTERFACE // 按接口限流
}
/**
* 限流切面
*/
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class RateLimitAspect {
private final RedissonClient redissonClient;
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
String limitKey = buildLimitKey(point, rateLimit);
RRateLimiter rateLimiter = redissonClient.getRateLimiter(limitKey);
// 初始化限流器
rateLimiter.trySetRate(
RateType.OVERALL,
rateLimit.count(),
rateLimit.time(),
RateIntervalUnit.SECONDS
);
// 尝试获取令牌
if (!rateLimiter.tryAcquire()) {
long waitTime = rateLimiter.availablePermits() > 0 ? 0 :
calculateWaitTime(rateLimiter, rateLimit);
throw new RateLimitException(rateLimit.message(), limitKey, waitTime);
}
return point.proceed();
}
private String buildLimitKey(ProceedingJoinPoint point, RateLimit rateLimit) {
StringBuilder key = new StringBuilder("rate_limit:");
if (StringUtils.isNotBlank(rateLimit.key())) {
key.append(rateLimit.key()).append(":");
}
MethodSignature signature = (MethodSignature) point.getSignature();
String methodName = signature.getDeclaringTypeName() + "." + signature.getName();
switch (rateLimit.limitType()) {
case IP:
key.append("ip:").append(ServletUtils.getClientIP());
break;
case USER:
Long userId = SecurityUtils.getUserIdOrNull();
key.append("user:").append(userId != null ? userId : "anonymous");
break;
case INTERFACE:
key.append("interface:").append(methodName);
break;
case DEFAULT:
default:
key.append(ServletUtils.getClientIP())
.append(":")
.append(methodName);
}
return key.toString();
}
private long calculateWaitTime(RRateLimiter rateLimiter, RateLimit rateLimit) {
// 简化计算,实际应根据令牌桶算法计算
return rateLimit.time();
}
}🌐 分布式异常处理
远程调用异常
java
/**
* 远程服务异常
*/
public class RemoteServiceException extends ServiceException {
private String serviceName;
private String methodName;
private int httpStatus;
public RemoteServiceException(String serviceName, String methodName, String message) {
super(message);
this.serviceName = serviceName;
this.methodName = methodName;
}
public RemoteServiceException(String serviceName, String methodName,
String message, int httpStatus) {
super(message);
this.serviceName = serviceName;
this.methodName = methodName;
this.httpStatus = httpStatus;
}
public String getServiceName() {
return serviceName;
}
public String getMethodName() {
return methodName;
}
public int getHttpStatus() {
return httpStatus;
}
}
/**
* 远程调用异常处理器
*/
@RestControllerAdvice
@Slf4j
public class RemoteExceptionHandler {
@ExceptionHandler(RemoteServiceException.class)
public R<Void> handleRemoteException(RemoteServiceException e, HttpServletRequest request) {
log.error("远程服务调用失败 - Service: {}, Method: {}, Status: {}, Message: {}",
e.getServiceName(), e.getMethodName(), e.getHttpStatus(), e.getMessage(), e);
// 根据HTTP状态码返回不同的错误信息
if (e.getHttpStatus() == 503) {
return R.fail("服务暂不可用,请稍后重试");
} else if (e.getHttpStatus() == 504) {
return R.fail("服务响应超时,请稍后重试");
}
return R.fail("远程服务调用失败");
}
@ExceptionHandler(FeignException.class)
public R<Void> handleFeignException(FeignException e, HttpServletRequest request) {
log.error("Feign调用异常 - Status: {}, Message: {}",
e.status(), e.getMessage(), e);
if (e.status() == -1) {
return R.fail("服务不可用,请检查网络连接");
} else if (e.status() >= 500) {
return R.fail("服务端异常,请稍后重试");
} else if (e.status() >= 400) {
return R.fail("请求参数错误");
}
return R.fail("服务调用失败");
}
}熔断降级异常
java
/**
* 熔断异常
*/
public class CircuitBreakerException extends ServiceException {
private String circuitBreakerName;
private CircuitBreakerState state;
public CircuitBreakerException(String circuitBreakerName, CircuitBreakerState state) {
super("服务熔断中,请稍后重试");
this.circuitBreakerName = circuitBreakerName;
this.state = state;
}
public String getCircuitBreakerName() {
return circuitBreakerName;
}
public CircuitBreakerState getState() {
return state;
}
}
/**
* 熔断状态
*/
public enum CircuitBreakerState {
CLOSED, // 关闭(正常)
OPEN, // 打开(熔断中)
HALF_OPEN // 半开(恢复中)
}
/**
* 降级异常
*/
public class FallbackException extends ServiceException {
private String fallbackMethod;
private Throwable originalException;
public FallbackException(String message, String fallbackMethod, Throwable originalException) {
super(message);
this.fallbackMethod = fallbackMethod;
this.originalException = originalException;
}
public String getFallbackMethod() {
return fallbackMethod;
}
public Throwable getOriginalException() {
return originalException;
}
}
/**
* 熔断降级异常处理器
*/
@RestControllerAdvice
@Slf4j
public class CircuitBreakerExceptionHandler {
@ExceptionHandler(CircuitBreakerException.class)
public R<Void> handleCircuitBreakerException(CircuitBreakerException e) {
log.warn("熔断器触发 - Name: {}, State: {}",
e.getCircuitBreakerName(), e.getState());
return R.fail(HttpStatus.SERVICE_UNAVAILABLE.value(),
"服务暂时不可用,请稍后重试");
}
@ExceptionHandler(FallbackException.class)
public R<Void> handleFallbackException(FallbackException e) {
log.warn("服务降级 - Fallback: {}, Original: {}",
e.getFallbackMethod(), e.getOriginalException().getMessage());
return R.fail("服务降级中,部分功能暂不可用");
}
}分布式事务异常
java
/**
* 分布式事务异常
*/
public class DistributedTransactionException extends ServiceException {
private String transactionId;
private TransactionPhase phase;
public DistributedTransactionException(String transactionId, TransactionPhase phase,
String message) {
super(message);
this.transactionId = transactionId;
this.phase = phase;
}
public String getTransactionId() {
return transactionId;
}
public TransactionPhase getPhase() {
return phase;
}
}
/**
* 事务阶段
*/
public enum TransactionPhase {
PREPARE, // 准备阶段
COMMIT, // 提交阶段
ROLLBACK // 回滚阶段
}
/**
* 分布式事务异常处理器
*/
@RestControllerAdvice
@RequiredArgsConstructor
@Slf4j
public class DistributedTransactionExceptionHandler {
private final TransactionCompensationService compensationService;
@ExceptionHandler(DistributedTransactionException.class)
public R<Void> handleDistributedTransactionException(
DistributedTransactionException e, HttpServletRequest request) {
log.error("分布式事务异常 - TxId: {}, Phase: {}, Message: {}",
e.getTransactionId(), e.getPhase(), e.getMessage(), e);
// 触发事务补偿
if (e.getPhase() == TransactionPhase.COMMIT) {
compensationService.compensate(e.getTransactionId());
}
return R.fail("操作失败,请稍后重试");
}
}🧪 测试中的异常验证
单元测试异常验证
java
/**
* 异常测试示例
*/
@ExtendWith(MockitoExtension.class)
class ExceptionHandlingTest {
@Mock
private UserMapper userMapper;
@InjectMocks
private UserServiceImpl userService;
@Test
@DisplayName("用户不存在时应抛出UserException")
void shouldThrowUserExceptionWhenUserNotFound() {
// Given
Long userId = 999L;
when(userMapper.selectById(userId)).thenReturn(null);
// When & Then
UserException exception = assertThrows(UserException.class, () -> {
userService.getUserById(userId);
});
assertEquals(ErrorCode.USER_NOT_FOUND, exception.getCode());
assertEquals("用户不存在", exception.getMessage());
}
@Test
@DisplayName("验证异常链包含预期的根因")
void shouldContainExpectedRootCause() {
// Given
SQLException sqlException = new SQLException("Connection refused");
DataAccessException dataException = new DataAccessException("DB Error", sqlException) {};
// When
Throwable rootCause = ExceptionChainTracer.getRootCause(dataException);
// Then
assertInstanceOf(SQLException.class, rootCause);
assertEquals("Connection refused", rootCause.getMessage());
}
@Test
@DisplayName("验证异常转换正确")
void shouldTransformExceptionCorrectly() {
// Given
IllegalArgumentException original = new IllegalArgumentException("Invalid parameter");
// When
ServiceException transformed = ExceptionWrapper.wrapAsServiceException(original);
// Then
assertEquals(ErrorCode.PARAM_ERROR, transformed.getCode());
assertTrue(transformed.getMessage().contains("参数错误"));
}
@Test
@DisplayName("验证限流异常包含正确的等待时间")
void shouldContainCorrectWaitTime() {
// Given
long waitTime = 30;
RateLimitException exception = new RateLimitException(
"请求过于频繁", "test_key", waitTime);
// Then
assertEquals("test_key", exception.getLimitKey());
assertEquals(30, exception.getWaitTime());
}
}集成测试异常验证
java
/**
* 全局异常处理器集成测试
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class GlobalExceptionHandlerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
@DisplayName("业务异常应返回正确的错误码和消息")
void shouldReturnCorrectErrorForServiceException() throws Exception {
mockMvc.perform(get("/api/user/999")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ErrorCode.USER_NOT_FOUND))
.andExpect(jsonPath("$.msg").value("用户不存在"));
}
@Test
@DisplayName("参数验证失败应返回400错误")
void shouldReturnBadRequestForValidationError() throws Exception {
String invalidJson = "{\"userName\":\"\"}";
mockMvc.perform(post("/api/user")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidJson))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ErrorCode.PARAM_ERROR));
}
@Test
@DisplayName("权限不足应返回403错误")
void shouldReturnForbiddenForPermissionDenied() throws Exception {
mockMvc.perform(delete("/api/admin/user/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ErrorCode.PERMISSION_DENIED));
}
@Test
@DisplayName("限流时应返回429错误")
void shouldReturnTooManyRequestsForRateLimit() throws Exception {
// 模拟超过限流阈值的请求
for (int i = 0; i < 10; i++) {
mockMvc.perform(get("/api/rate-limited")
.contentType(MediaType.APPLICATION_JSON));
}
mockMvc.perform(get("/api/rate-limited")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(HttpStatus.TOO_MANY_REQUESTS.value()))
.andExpect(header().exists("Retry-After"));
}
}⚠️ 注意事项
1. 异常处理最佳实践
- 明确异常类型:使用具体的异常类型而非通用Exception
- 携带上下文:异常消息应包含足够的上下文信息便于排查
- 避免吞噬异常:捕获异常后要么处理要么重新抛出
- 异常日志:不同级别的异常使用不同级别的日志记录
2. 性能考虑
- 避免频繁创建异常:异常创建涉及堆栈信息收集,开销较大
- 缓存异常实例:对于固定的业务异常可以预先创建并缓存
- 异步记录日志:异常日志的持久化应异步执行,避免影响主流程
3. 安全考虑
- 不暴露敏感信息:生产环境不要将堆栈信息返回给客户端
- 参数化消息:使用参数化消息防止注入攻击
- 限制日志大小:限制异常消息和堆栈的记录长度
4. 国际化考虑
- 统一消息管理:所有异常消息通过MessageSource管理
- 参数化占位符:使用{0}、{1}等占位符而非字符串拼接
- 默认语言:始终提供默认语言的消息作为兜底
5. 监控告警
- 设置合理阈值:根据业务量设置异常告警阈值
- 分级告警:不同级别的异常采用不同的告警方式
- 避免告警疲劳:聚合相似异常,避免重复告警
6. 分布式环境
- 链路追踪:异常信息应包含TraceId便于分布式追踪
- 服务标识:记录发生异常的服务名称和实例
- 超时设置:合理设置远程调用超时,避免级联故障
ExceptionHandler 为 RuoYi-Plus 提供了完整的异常处理机制,通过统一的异常处理、详细的异常记录和实时的异常监控,确保系统的稳定性和可维护性。
