Spring Boot 的全局异常处理机制复习

一、基础原理:@ControllerAdvice + @ExceptionHandler

Spring Boot 基于 Spring MVC 提供了标准的全局异常处理机制:

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ErrorResponse.of(e.getCode(), e.getMessage()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleUnexpectedException(Exception e) {
        String traceId = MDC.get("traceId"); // 关联链路追踪
        log.error("系统异常 [traceId={}]: {}", traceId, e.getMessage(), e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ErrorResponse.of("SYS_ERROR", "系统繁忙,请稍后再试"));
    }
}

✅ 核心注解:

  • @ControllerAdvice:对所有 @Controller 生效
  • @RestControllerAdvice:返回 JSON(等价于 @ControllerAdvice + @ResponseBody
  • @ExceptionHandler:捕获指定异常类型

二、实战中的关键实践(亮点)

1. 分层异常体系设计(核心亮点)

普遍采用 自定义异常分层模型 ,避免直接抛出 RuntimeExceptionException

java 复制代码
// 基础异常类
public abstract class BaseException extends RuntimeException {
    private final String code;
    public BaseException(String code, String message) { super(message); this.code = code; }
    public String getCode() { return code; }
}

// 业务异常(4xx)
public class BusinessException extends BaseException {
    public BusinessException(String code, String message) {
        super(code, message);
    }
}

// 系统异常(5xx)
public class SystemException extends BaseException {
    public SystemException(String message, Throwable cause) {
        super("SYS_ERROR", message);
        this.initCause(cause);
    }
}

优势

  • 前端可根据 code 做精准提示(如 USER_NOT_FOUND vs PERMISSION_DENIED
  • 监控系统可按异常码聚合告警
  • 避免暴露内部堆栈给用户

2. 统一响应体(Standardized Response)

所有接口返回结构一致,便于前端处理:

java 复制代码
public class ErrorResponse {
    private String code;      // 业务码,如 "ORDER_001"
    private String message;   // 用户友好提示
    private String traceId;   // 链路ID,用于日志追踪
    private long timestamp;

    public static ErrorResponse of(String code, String message) {
        ErrorResponse resp = new ErrorResponse();
        resp.code = code;
        resp.message = message;
        resp.traceId = MDC.get("traceId");
        resp.timestamp = System.currentTimeMillis();
        return resp;
    }
}

🌟 大厂要求:即使 5xx 错误,也不能返回裸露的 Tomcat 错误页或 JSON 格式不一致!

3. 日志与监控深度集成

(1)关联全链路追踪 ID
  • 利用 MDC(Mapped Diagnostic Context)注入 traceId
  • 异常日志中包含 traceId,可在 ELK / Sentry / ARMS 中快速定位请求上下文
(2)关键异常自动上报监控
java 复制代码
@ExceptionHandler(BusinessException.class)
public ResponseEntity<?> handle(...) {
    if (isCritical(e.getCode())) {
        metrics.counter("exception.business", "code", e.getCode()).increment();
        alarmService.sendAlert("业务异常激增: " + e.getCode()); // 接入企业微信/钉钉告警
    }
    // ...
}

💡 大厂通常会对接 Prometheus + Grafana内部监控平台,对异常进行量化分析。

4. 安全与信息脱敏

  • 绝不返回技术细节给前端(如数据库错误、文件路径)
  • 生产环境屏蔽 e.printStackTrace() 的原始信息
  • 敏感字段(如用户手机号、身份证)在日志中脱敏
java 复制代码
// 错误示例 ❌
return "数据库连接失败: " + e.getMessage(); // 可能泄露 IP、表名等

// 正确做法 ✅
return "服务暂时不可用,请联系客服"; // 用户友好 + 安全

5. 异步与非 Web 场景的异常处理

大厂系统大量使用异步任务、MQ 消费、定时任务,这些场景 不在 @ControllerAdvice 覆盖范围内

解决方案:

  • MQ 消费者:try-catch + 死信队列 + 告警

  • @Async 异步方法 :自定义 AsyncConfigurer 设置异常处理器

    java 复制代码
    @Override
    public AsyncTaskExecutor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadFactory(r -> {
            Thread t = new Thread(r);
            t.setUncaughtExceptionHandler((thread, ex) -> 
                log.error("Async task failed", ex));
            return t;
        });
        return executor;
    }

⚠️ 面试高频考点:@ControllerAdvice 只对 Controller 层生效,对 Service、Async、MQ 无效!

三、高级设计亮点(面试加分项)

✅ 1. 异常码标准化管理

  • 使用枚举或配置中心管理异常码(如 Apollo/Nacos)
  • 支持多语言国际化(i18n)
java 复制代码
public enum ErrorCode {
    USER_NOT_FOUND("USER_001", "用户不存在"),
    ORDER_EXPIRED("ORDER_002", "订单已过期");

    private final String code;
    private final String msg;
}

✅ 2. 熔断与降级联动

  • 当某类异常频率突增,触发 Hystrix/Sentinel 熔断
  • 返回缓存数据或默认兜底结果,而非直接报错

✅ 3. 自动化测试覆盖

  • 单元测试验证异常路径(MockMvc 测试 4xx/5xx 响应)
  • Chaos Engineering(混沌工程)主动注入异常,验证系统韧性

四、常见面试题 & 回答思路

Q1:@ControllerAdvice 的原理是什么?

A:基于 Spring AOP 和 DispatcherServlet 的 HandlerExceptionResolver 机制。当 Controller 抛出异常时,DispatcherServlet 会遍历所有 HandlerExceptionResolver,其中 ExceptionHandlerExceptionResolver 会查找匹配的 @ExceptionHandler 方法并执行。

Q2:全局异常处理能捕获哪些异常?

A:只能捕获 Controller 层抛出的异常。对于 Filter、Interceptor、Async、MQ、定时任务中的异常,需要单独处理。

Q3:如何避免异常信息泄露?

A:生产环境关闭 Spring Boot 的 server.error.include-message=never,并在全局异常处理器中统一返回用户友好消息,原始堆栈仅记录到日志。

Q4:如何做异常监控?

A:结合 MDC 注入 traceId,异常日志接入 ELK;关键异常通过 Micrometer 上报指标到 Prometheus;高频异常触发企业告警(如钉钉机器人)。


总结:异常处理的核心思想

维度 做法
分层 自定义异常体系(业务/系统)
统一 标准响应格式 + 异常码
可观测 traceId + 日志 + 监控 + 告警
安全 不泄露技术细节,日志脱敏
健壮 覆盖异步/MQ/定时任务等非 Web 场景
相关推荐
陈随易32 分钟前
VSCode的Copilot扩展支持接入DeepSeek,Kimi了!
前端·后端·程序员
我不是外星人2 小时前
有了 Harness Engineering ,真的还需要研发工程师吗?
前端·后端·ai编程
candyTong2 小时前
RTK 技术原理:一次典型会话里,80% 上下文是怎么省下来的
javascript·后端·架构
Rust研习社4 小时前
组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
后端·rust·编程语言
IT_陈寒5 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro5 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax6 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH6 小时前
Koa和Express的区别
后端
MariaH6 小时前
Koa框架的使用
后端
luckdewei7 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端