一、为什么需要全局异常处理?
在Spring Boot项目开发中,如果没有统一的异常处理机制,会遇到以下问题:
- 代码冗余:每个Controller中都需要重复编写try-catch代码块
- 响应格式混乱:不同接口返回的错误信息格式不一致,前端难以统一处理
- 安全隐患:系统内部异常(如SQLException、NullPointerException)直接暴露给用户
- 维护困难:异常处理逻辑分散在各个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,否则需要手动配置事务回滚规则。
六、总结
通过全局异常处理,我们实现了以下目标:
- 统一返回结构:所有接口返回格式一致,前端解析简单
- 集中管理异常:减少冗余代码,提升代码可维护性
- 区分异常类型:业务异常与系统异常分离,便于问题定位
- 安全保障:避免系统内部信息泄露
- 可扩展性强:支持日志记录、监控告警、国际化等扩展功能
建议在实际项目中,将全局异常处理作为基础框架的一部分,避免每个Controller重复造轮子,真正实现”异常即业务逻辑的一部分”的设计理念。