全局异常处理与异常日志自动化采集

作者:忆笙智云官方 | 发布时间:2026-05-03 09:45 | 更新时间:2026-07-03 09:45

全局异常处理与异常日志自动化采集

引言

在软件系统中,异常处理的质量直接影响系统的稳定性和可维护性。如果没有统一的异常处理机制,开发者往往会在每个 Controller 方法中写 try-catch,导致代码冗余、异常信息丢失、日志格式不一致等问题。

Spring Boot 提供了 @RestControllerAdvice 注解,可以优雅地实现全局异常处理。本文将分享全局异常处理器的设计、业务异常与系统异常的分类处理、异常日志自动记录以及堆栈信息截取的最佳实践。

一、异常分类体系

1.1 异常分类设计

graph TD
    A[Throwable] --> B[Exception]
    B --> C[RuntimeException]
    C --> D[BaseException<br/>code + message]
    D --> E[BusinessException<br/>业务异常 5xx]
    D --> F[AuthException<br/>认证异常 401]
    D --> G[ForbiddenException<br/>授权异常 403]
    D --> H[ValidationException<br/>参数异常 400]
    D --> I[NotFoundException<br/>资源不存在 404]
    B --> J[SQLException]
    B --> K[IOException]
    B --> L[其他Checked Exception]

    style D fill:#ffcdd2,stroke:#c62828
    style E fill:#fff9c4,stroke:#f9a825
    style F fill:#e1bee7,stroke:#7b1fa2
    style G fill:#e1bee7,stroke:#7b1fa2
    style H fill:#bbdefb,stroke:#1565c0
    style I fill:#bbdefb,stroke:#1565c0

1.2 异常码规范

异常码区间 分类 示例
400 参数校验异常 缺少必填参数、格式不正确
401 认证异常 Token 过期、未登录
403 授权异常 无权限访问
404 资源不存在 数据不存在
1000-1999 用户模块 1001 用户不存在、1002 密码错误
2000-2999 权限模块 2001 角色已存在、2002 权限不足
3000-3999 业务模块 3001 订单已取消、3002 库存不足
5000-5999 系统模块 5000 系统繁忙、5001 文件上传失败

二、自定义异常类

2.1 基础异常

/**
 * 基础异常类
 * 所有自定义异常的父类
 */
public class BaseException extends RuntimeException {

    /** 错误码 */
    private final int code;

    /** 错误模块 */
    private final String module;

    /** 错误详情(用于日志,不返回给前端) */
    private final String detail;

    public BaseException(int code, String module, String message) {
        super(message);
        this.code = code;
        this.module = module;
        this.detail = null;
    }

    public BaseException(int code, String module, String message, String detail) {
        super(message);
        this.code = code;
        this.module = module;
        this.detail = detail;
    }

    public BaseException(int code, String module, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
        this.module = module;
        this.detail = cause.getMessage();
    }

    public int getCode() { return code; }
    public String getModule() { return module; }
    public String getDetail() { return detail; }
}

2.2 业务异常

/**
 * 业务异常
 * 用于可预期的业务错误
 */
public class BusinessException extends BaseException {

    public BusinessException(String message) {
        super(500, "business", message);
    }

    public BusinessException(int code, String message) {
        super(code, "business", message);
    }

    public BusinessException(int code, String module, String message) {
        super(code, module, message);
    }
}

/**
 * 认证异常
 */
public class AuthException extends BaseException {

    public AuthException(String message) {
        super(401, "auth", message);
    }
}

/**
 * 授权异常
 */
public class ForbiddenException extends BaseException {

    public ForbiddenException(String message) {
        super(403, "forbidden", message);
    }
}

/**
 * 资源不存在异常
 */
public class NotFoundException extends BaseException {

    public NotFoundException(String message) {
        super(404, "not_found", message);
    }
}

2.3 异常码枚举

/**
 * 异常码枚举
 * 统一管理所有异常码,避免硬编码
 */
public enum ErrorCode {

    // 通用异常
    SUCCESS(200, "操作成功"),
    BAD_REQUEST(400, "请求参数错误"),
    UNAUTHORIZED(401, "未登录或Token已过期"),
    FORBIDDEN(403, "无权限访问"),
    NOT_FOUND(404, "请求资源不存在"),
    INTERNAL_ERROR(500, "系统繁忙,请稍后重试"),

    // 用户模块 1xxx
    USER_NOT_FOUND(1001, "用户不存在"),
    USER_PASSWORD_ERROR(1002, "密码错误"),
    USER_DISABLED(1003, "用户已被禁用"),
    USER_LOCKED(1004, "用户已被锁定"),

    // 权限模块 2xxx
    ROLE_EXISTS(2001, "角色已存在"),
    ROLE_IN_USE(2002, "角色正在使用中,无法删除"),

    // 文件模块 5xxx
    FILE_UPLOAD_ERROR(5001, "文件上传失败"),
    FILE_NOT_FOUND(5002, "文件不存在"),
    FILE_TYPE_NOT_ALLOWED(5003, "不支持的文件类型");

    private final int code;
    private final String message;

    ErrorCode(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() { return code; }
    public String getMessage() { return message; }
}

三、全局异常处理器

3.1 核心处理器

/**
 * 全局异常处理器
 * 统一处理所有Controller层抛出的异常
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    private final ErrorLogService errorLogService;

    public GlobalExceptionHandler(ErrorLogService errorLogService) {
        this.errorLogService = errorLogService;
    }

    /**
     * 处理业务异常
     * 业务异常是可预期的,不需要记录完整堆栈
     */
    @ExceptionHandler(BaseException.class)
    public R<Void> handleBaseException(BaseException e, HttpServletRequest request) {
        log.warn("业务异常: code={}, module={}, msg={}",
                 e.getCode(), e.getModule(), e.getMessage());

        // 记录业务异常日志(简化版)
        errorLogService.recordBusinessError(e, request);

        return R.fail(e.getCode(), e.getMessage());
    }

    /**
     * 处理参数校验异常(@Valid)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R<Void> handleValidException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
                .collect(Collectors.joining("; "));

        log.warn("参数校验失败: {}", message);
        return R.fail(400, message);
    }

    /**
     * 处理参数绑定异常
     */
    @ExceptionHandler(BindException.class)
    public R<Void> handleBindException(BindException e) {
        String message = e.getFieldErrors().stream()
                .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
                .collect(Collectors.joining("; "));

        log.warn("参数绑定失败: {}", message);
        return R.fail(400, message);
    }

    /**
     * 处理请求方式不支持异常
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public R<Void> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) {
        log.warn("请求方式不支持: {}", e.getMethod());
        return R.fail(405, "不支持的请求方式: " + e.getMethod());
    }

    /**
     * 处理认证异常(Sa-Token)
     */
    @ExceptionHandler(NotLoginException.class)
    public R<Void> handleNotLoginException(NotLoginException e) {
        log.warn("未登录访问: {}", e.getMessage());
        return R.fail(401, "未登录或Token已过期");
    }

    /**
     * 处理授权异常(Sa-Token)
     */
    @ExceptionHandler(NotPermissionException.class)
    public R<Void> handleNotPermissionException(NotPermissionException e) {
        log.warn("权限不足: {}", e.getPermission());
        return R.fail(403, "无权限访问: " + e.getPermission());
    }

    /**
     * 兜底异常处理
     * 所有未捕获的异常统一处理,记录完整堆栈
     */
    @ExceptionHandler(Exception.class)
    public R<Void> handleException(Exception e, HttpServletRequest request) {
        log.error("系统异常: uri={}, method={}",
                  request.getRequestURI(), request.getMethod(), e);

        // 记录系统异常日志(完整堆栈)
        errorLogService.recordSystemError(e, request);

        // 返回友好提示,不暴露系统内部信息
        return R.fail(500, "系统繁忙,请稍后重试");
    }
}

3.2 异常处理流程

flowchart TD
    A[Controller抛出异常] --> B{异常类型?}
    B -->|BaseException| C[业务异常处理]
    B -->|ValidException| D[参数校验处理]
    B -->|NotLoginException| E[认证异常处理]
    B -->|NotPermissionException| F[授权异常处理]
    B -->|其他Exception| G[兜底异常处理]

    C --> H[WARN日志 + 业务日志]
    D --> I[WARN日志]
    E --> J[WARN日志]
    F --> K[WARN日志]
    G --> L[ERROR日志 + 系统日志]

    H --> M[返回具体错误码和消息]
    I --> N[返回400 + 校验消息]
    J --> O[返回401 + 未登录提示]
    K --> P[返回403 + 无权限提示]
    L --> Q[返回500 + 友好提示]

    style C fill:#fff9c4,stroke:#f9a825
    style G fill:#ffcdd2,stroke:#c62828
    style M fill:#c8e6c9,stroke:#2e7d32
    style Q fill:#c8e6c9,stroke:#2e7d32

四、异常日志自动化采集

4.1 异常日志实体

/**
 * 异常日志实体
 */
public class ErrorLog {
    private Long id;

    /** 异常类型:1-业务异常 2-系统异常 */
    private Integer errorType;

    /** 异常码 */
    private Integer errorCode;

    /** 异常模块 */
    private String errorModule;

    /** 异常消息 */
    private String errorMessage;

    /** 堆栈信息(截取后) */
    private String stackTrace;

    /** 请求URI */
    private String requestUri;

    /** 请求方式 */
    private String requestMethod;

    /** 请求参数 */
    private String requestParams;

    /** 操作用户ID */
    private Long userId;

    /** 客户端IP */
    private String clientIp;

    /** 创建时间 */
    private LocalDateTime createTime;
}

4.2 异常日志服务

/**
 * 异常日志服务
 */
@Service
@Slf4j
public class ErrorLogService {

    private final ErrorLogMapper errorLogMapper;
    private final AsyncTaskExecutor asyncExecutor;

    /**
     * 记录业务异常日志
     * 异步执行,不影响接口响应速度
     */
    public void recordBusinessError(BaseException e, HttpServletRequest request) {
        asyncExecutor.execute(() -> {
            ErrorLog errorLog = new ErrorLog();
            errorLog.setErrorType(1);
            errorLog.setErrorCode(e.getCode());
            errorLog.setErrorModule(e.getModule());
            errorLog.setErrorMessage(e.getMessage());
            errorLog.setRequestUri(request.getRequestURI());
            errorLog.setRequestMethod(request.getMethod());
            errorLog.setClientIp(getClientIp(request));
            errorLog.setCreateTime(LocalDateTime.now());

            errorLogMapper.insert(errorLog);
        });
    }

    /**
     * 记录系统异常日志
     * 包含截取后的堆栈信息
     */
    public void recordSystemError(Exception e, HttpServletRequest request) {
        asyncExecutor.execute(() -> {
            ErrorLog errorLog = new ErrorLog();
            errorLog.setErrorType(2);
            errorLog.setErrorCode(500);
            errorLog.setErrorMessage(e.getClass().getName() + ": " + e.getMessage());
            errorLog.setStackTrace(truncateStackTrace(e, 10));
            errorLog.setRequestUri(request.getRequestURI());
            errorLog.setRequestMethod(request.getMethod());
            errorLog.setRequestParams(getRequestParams(request));
            errorLog.setClientIp(getClientIp(request));
            errorLog.setCreateTime(LocalDateTime.now());

            errorLogMapper.insert(errorLog);
        });
    }

    /**
     * 获取客户端IP
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty()) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty()) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    /**
     * 获取请求参数
     */
    private String getRequestParams(HttpServletRequest request) {
        Map<String, String[]> paramMap = request.getParameterMap();
        if (paramMap.isEmpty()) return null;

        // 过滤敏感参数
        Set<String> sensitiveKeys = Set.of("password", "token", "secret");

        return paramMap.entrySet().stream()
            .filter(e -> !sensitiveKeys.contains(e.getKey()))
            .map(e -> e.getKey() + "=" + String.join(",", e.getValue()))
            .collect(Collectors.joining("&"));
    }
}

五、堆栈信息截取

5.1 为什么需要截取

完整的异常堆栈可能非常长(数百行),直接存储到数据库会浪费存储空间,且不利于阅读。实际上,排查问题通常只需要关键的前几行堆栈。

5.2 截取工具方法

/**
 * 堆栈信息截取工具
 */
public class StackTraceTruncator {

    /** 默认保留的堆栈层数 */
    private static final int DEFAULT_MAX_DEPTH = 10;

    /** 堆栈信息最大长度 */
    private static final int MAX_LENGTH = 2000;

    /**
     * 截取异常堆栈信息
     * @param e 异常对象
     * @param maxDepth 保留的堆栈层数
     * @return 截取后的堆栈字符串
     */
    public static String truncateStackTrace(Throwable e, int maxDepth) {
        if (e == null) return null;

        StringBuilder sb = new StringBuilder();
        sb.append(e.getClass().getName());

        if (e.getMessage() != null) {
            sb.append(": ").append(e.getMessage());
        }
        sb.append("
");

        StackTraceElement[] elements = e.getStackTrace();
        int depth = Math.min(elements.length, maxDepth);

        for (int i = 0; i < depth; i++) {
            sb.append("	at ").append(elements[i]).append("
");
        }

        // 如果还有更多堆栈,添加省略标记
        if (elements.length > maxDepth) {
            sb.append("	... ").append(elements.length - maxDepth)
              .append(" more
");
        }

        // 处理Caused by
        Throwable cause = e.getCause();
        if (cause != null) {
            sb.append("Caused by: ");
            String causeTrace = truncateStackTrace(cause, maxDepth - 2);
            sb.append(causeTrace);
        }

        // 最终长度限制
        String result = sb.toString();
        if (result.length() > MAX_LENGTH) {
            result = result.substring(0, MAX_LENGTH) + "...(truncated)";
        }

        return result;
    }

    /**
     * 使用默认深度截取
     */
    public static String truncateStackTrace(Throwable e) {
        return truncateStackTrace(e, DEFAULT_MAX_DEPTH);
    }
}

5.3 截取效果对比

# 完整堆栈(可能50+行)
java.lang.NullPointerException: Cannot invoke method on null object
    at com.example.service.UserService.getUser(UserService.java:45)
    at com.example.service.UserService.getUserDetail(UserService.java:78)
    at com.example.controller.UserController.getUser(UserController.java:32)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.springframework.web.method.support.InvocableHandlerMethod.invoke(...)
    ... 40 more

# 截取后(10行,保留关键信息)
java.lang.NullPointerException: Cannot invoke method on null object
    at com.example.service.UserService.getUser(UserService.java:45)
    at com.example.service.UserService.getUserDetail(UserService.java:78)
    at com.example.controller.UserController.getUser(UserController.java:32)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.springframework.web.method.support.InvocableHandlerMethod.invoke(...)
    ... 40 more

六、异常日志查询与告警

6.1 异常日志查询接口

/**
 * 异常日志管理接口
 */
@RestController
@RequestMapping("/api/monitor/error-log")
public class ErrorLogController {

    private final ErrorLogService errorLogService;

    /**
     * 分页查询异常日志
     */
    @GetMapping("/page")
    public R<IPage<ErrorLog>> page(
            @RequestParam(defaultValue = "1") Integer pageNum,
            @RequestParam(defaultValue = "10") Integer pageSize,
            @RequestParam(required = false) Integer errorType,
            @RequestParam(required = false) String requestUri) {

        Page<ErrorLog> page = new Page<>(pageNum, pageSize);
        LambdaQueryWrapper<ErrorLog> wrapper = new LambdaQueryWrapper<ErrorLog>()
            .eq(errorType != null, ErrorLog::getErrorType, errorType)
            .like(requestUri != null, ErrorLog::getRequestUri, requestUri)
            .orderByDesc(ErrorLog::getCreateTime);

        return R.ok(errorLogService.page(page, wrapper));
    }
}

6.2 异常告警机制

/**
 * 异常告警服务
 * 当系统异常频率超过阈值时触发告警
 */
@Service
@Slf4j
public class ErrorAlertService {

    private final RedisTemplate<String, Object> redisTemplate;

    /** 告警阈值:5分钟内系统异常超过10次 */
    private static final int ALERT_THRESHOLD = 10;

    /** 统计窗口(分钟) */
    private static final int WINDOW_MINUTES = 5;

    /**
     * 检查是否需要告警
     */
    public void checkAlert() {
        String key = "alert:error:count:" +
            LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));

        Long count = redisTemplate.opsForValue().increment(key);

        // 首次设置过期时间
        if (count != null && count == 1) {
            redisTemplate.expire(key, WINDOW_MINUTES, TimeUnit.MINUTES);
        }

        // 超过阈值触发告警
        if (count != null && count == ALERT_THRESHOLD) {
            sendAlert("系统异常频率过高,最近" + WINDOW_MINUTES +
                      "分钟内已发生" + count + "次系统异常");
        }
    }

    /**
     * 发送告警(可对接钉钉/企业微信/邮件等)
     */
    private void sendAlert(String message) {
        log.error("【异常告警】{}", message);
        // 实际项目中对接告警通道
    }
}

七、异常处理全景流程

flowchart TD
    A[请求进入Controller] --> B{业务处理}
    B -->|正常| C[返回R.ok数据]
    B -->|抛出BaseException| D[GlobalExceptionHandler]
    B -->|抛出ValidException| D
    B -->|抛出其他Exception| D

    D --> E{异常分类}
    E -->|业务异常| F[WARN日志]
    E -->|参数异常| G[WARN日志]
    E -->|认证/授权异常| H[WARN日志]
    E -->|系统异常| I[ERROR日志]

    F --> J[异步记录业务日志]
    I --> K[异步记录系统日志<br/>含截取堆栈]

    J --> L[返回具体错误码+消息]
    G --> L
    H --> L
    K --> M[返回500+友好提示]

    K --> N[检查告警阈值]
    N -->|超阈值| O[发送告警通知]

    style I fill:#ffcdd2,stroke:#c62828
    style K fill:#ffcdd2,stroke:#c62828
    style L fill:#c8e6c9,stroke:#2e7d32
    style M fill:#fff9c4,stroke:#f9a825

结论与建议

核心设计原则

  1. 分类处理:业务异常返回具体信息,系统异常返回友好提示,绝不暴露堆栈给前端
  2. 异步记录:异常日志写入数据库使用异步线程,不影响接口响应速度
  3. 堆栈截取:只保留关键堆栈行,节省存储空间,提高可读性
  4. 告警机制:系统异常频率超阈值自动告警,及时发现线上问题

最佳实践

相关资源