SpringBoot 全局异常处理最佳实践

一、为什么需要全局异常处理?

在Spring Boot项目开发中,如果没有统一的异常处理机制,会遇到以下问题:

  1. 代码冗余:每个Controller中都需要重复编写try-catch代码块
  2. 响应格式混乱:不同接口返回的错误信息格式不一致,前端难以统一处理
  3. 安全隐患:系统内部异常(如SQLException、NullPointerException)直接暴露给用户
  4. 维护困难:异常处理逻辑分散在各个Controller中,难以统一管理和扩展

二、核心实现方案

方案一:@RestControllerAdvice + @ExceptionHandler(主流推荐)

这是目前最主流、最优雅的方式,通过集中处理所有控制器抛出的异常,实现统一管理。

1. 定义统一响应体

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
    private Integer code;    // 业务状态码,如200-成功,4001-用户不存在
    private String msg;     // 提示信息
    private T data;         // 响应数据
    
    // 成功响应
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "success", data);
    }
    
    // 失败响应
    public static <T> Result<T> error(Integer code, String msg) {
        return new Result<>(code, msg, null);
    }
}

2. 自定义业务异常类

public class BusinessException extends RuntimeException {
    private Integer code;
    
    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
    }
    
    public Integer getCode() {
        return code;
    }
}

3. 全局异常处理器

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    // 处理业务异常
    @ExceptionHandler(BusinessException.class)
    public Result<?> handleBusinessException(BusinessException ex) {
        log.warn("业务异常:code={}, msg={}", ex.getCode(), ex.getMessage());
        return Result.error(ex.getCode(), ex.getMessage());
    }
    
    // 处理参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidException(MethodArgumentNotValidException ex) {
        String msg = ex.getBindingResult().getFieldError().getDefaultMessage();
        log.warn("参数校验失败:{}", msg);
        return Result.error(400, msg);
    }
    
    // 处理空指针异常
    @ExceptionHandler(NullPointerException.class)
    public Result<?> handleNullPointerException(NullPointerException ex) {
        log.error("空指针异常:", ex);
        return Result.error(500, "系统内部错误");
    }
    
    // 兜底异常处理
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception ex) {
        log.error("系统异常:", ex);
        return Result.error(500, "系统繁忙,请稍后再试");
    }
}

方案二:实现HandlerExceptionResolver接口

这种方式提供了完全的控制流程,但侵入性较强,适用于特殊定制需求。

@Component
public class CustomExceptionResolver implements HandlerExceptionResolver {
    
    @Override
    public ModelAndView resolveException(HttpServletRequest request, 
                                       HttpServletResponse response, 
                                       Object handler, 
                                       Exception ex) {
        // 自定义异常处理逻辑
        return new ModelAndView();
    }
}

方案三:使用AOP拦截异常

虽然灵活且可跨层,但无法直接访问HTTP上下文,且对异步方法支持较差,不推荐用于Web层异常处理。

三、最佳实践要点

1. 异常分类与状态码设计

误区:所有异常都返回500状态码。

正确做法

  • 业务异常:返回200状态码,通过code字段区分具体错误(如1001=用户不存在)
  • 客户端错误:返回400状态码(参数错误、权限不足等)
  • 服务器错误:返回500状态码(数据库连接失败、系统内部错误等)

2. 日志记录策略

在全局异常处理器中必须记录详细的日志信息:

@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception ex, HttpServletRequest request) {
    log.error("请求异常:uri={}, method={}, msg={}", 
              request.getRequestURI(), 
              request.getMethod(), 
              ex.getMessage(), 
              ex);
    return Result.error(500, "系统繁忙,请稍后再试");
}

日志分级建议

  • 业务异常:WARN级别
  • 系统异常:ERROR级别
  • 参数校验异常:WARN级别

3. 请求链路追踪

使用MDC(Mapped Diagnostic Context)实现请求链路追踪:

@Component
public class RequestIdFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain filterChain) throws IOException, ServletException {
        String requestId = UUID.randomUUID().toString();
        MDC.put("requestId", requestId);
        try {
            filterChain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }
}

在logback-spring.xml中配置日志格式:

<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{requestId}] %-5level %logger{36} - %msg%n</pattern>

4. 敏感信息脱敏

在异常消息中不要包含密码、身份证号等敏感信息:

@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception ex) {
    String message = ex.getMessage();
    // 脱敏处理
    if (message != null && message.contains("password")) {
        message = message.replaceAll("password=[^&]*", "password=***");
    }
    return Result.error(500, message);
}

5. 多环境适配

区分开发环境和生产环境的错误信息展示:

@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception ex) {
    if ("dev".equals(env)) {
        return Result.error(500, ex.getMessage());
    } else {
        return Result.error(500, "系统繁忙,请稍后再试");
    }
}

四、进阶优化

1. 异常落库存储

将异常信息持久化到数据库,方便后续查询和统计:

@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception ex, HttpServletRequest request) {
    ErrorLog log = new ErrorLog();
    log.setUri(request.getRequestURI());
    log.setMethod(request.getMethod());
    log.setMessage(ex.getMessage());
    log.setStackTrace(Arrays.toString(ex.getStackTrace()));
    log.setCreateTime(LocalDateTime.now());
    errorLogRepository.save(log);
    
    return Result.error(500, "系统繁忙,请稍后再试");
}

2. 集成监控系统

接入Sentry、SkyWalking等监控平台:

@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception ex) {
    Sentry.captureException(ex);
    return Result.error(500, "系统繁忙,请稍后再试");
}

3. 国际化支持

通过MessageSource获取本地化的错误消息:

@Autowired
private MessageSource messageSource;

@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception ex, Locale locale) {
    String errorMessage = messageSource.getMessage("error.message", null, locale);
    return Result.error(500, errorMessage);
}

4. 异步任务异常处理

对于异步任务中的异常,通过自定义线程池的uncaughtExceptionHandler进行兜底:

@Bean
public ThreadPoolTaskExecutor taskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setThreadNamePrefix("async-");
    executor.setCorePoolSize(5);
    executor.setMaxPoolSize(10);
    executor.setQueueCapacity(100);
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.setTaskDecorator(new MDCTaskDecorator());
    executor.setWaitForTasksToCompleteOnShutdown(true);
    executor.setAwaitTerminationSeconds(60);
    executor.setThreadFactory(new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setUncaughtExceptionHandler((t, e) -> {
                log.error("异步任务异常:", e);
            });
            return thread;
        }
    });
    return executor;
}

五、常见问题与解决方案

1. 异常处理不生效?

原因:自定义异常不是RuntimeException的子类。

解决方案:确保自定义异常继承RuntimeException,Spring默认只捕获RuntimeException和Error。

2. Filter中抛出的异常无法捕获?

原因:@RestControllerAdvice只能捕获Controller层之后的异常,Filter中的异常无法被捕获。

解决方案:在Filter中捕获异常后,手动调用全局异常处理器的方法。

3. 事务回滚问题

原因:Spring默认只对RuntimeException及其子类回滚事务。

解决方案:自定义业务异常必须继承RuntimeException,否则需要手动配置事务回滚规则。

六、总结

通过全局异常处理,我们实现了以下目标:

  1. 统一返回结构:所有接口返回格式一致,前端解析简单
  2. 集中管理异常:减少冗余代码,提升代码可维护性
  3. 区分异常类型:业务异常与系统异常分离,便于问题定位
  4. 安全保障:避免系统内部信息泄露
  5. 可扩展性强:支持日志记录、监控告警、国际化等扩展功能

建议在实际项目中,将全局异常处理作为基础框架的一部分,避免每个Controller重复造轮子,真正实现”异常即业务逻辑的一部分”的设计理念。


作 者:南烛
链 接:https://www.itnotes.top/archives/1094
来 源:IT笔记
文章版权归作者所有,转载请注明出处!


上一篇
下一篇