Spring Boot自定义全局异常处理:从痛点到优雅实现
在 Spring Boot 项目开发中,异常处理是保障系统稳定性的关键环节。传统的try-catch
分散在各个 Controller、Service 中,不仅导致代码冗余,还会出现同一异常不同响应格式的问题 —— 比如有的接口返回{"code":500,"msg":"错误"}
,有的直接返回 Tomcat 默认的 404 页面,前端处理时需反复适配。
而自定义全局异常处理能统一接管项目中所有异常,实现 “一处定义、全局生效”,让异常响应格式标准化、代码逻辑更简洁。本文将从核心原理到实战落地,带你掌握 Spring Boot 全局异常处理的完整方案。
一、为什么需要全局异常处理?先看传统方案的痛点
在没有全局异常处理时,我们通常这样处理异常:@RestController
@RequestMapping("/user")
public class UserController {@GetMapping("/{id}")public String getUserById(@PathVariable Long id) {try {if (id == null || id <= 0) {throw new IllegalArgumentException("用户ID非法");}User user = userService.getById(id);if (user == null) {return "{\"code\":404,\"msg\":\"用户不存在\",\"data\":null}";}return "{\"code\":200,\"msg\":\"成功\",\"data\":" + JSON.toJSONString(user) + "}";} catch (IllegalArgumentException e) {return "{\"code\":400,\"msg\":\"" + e.getMessage() + "\",\"data\":null}";} catch (Exception e) {// 系统异常,隐藏具体信息return "{\"code\":500,\"msg\":\"服务器内部错误\",\"data\":null}";}}
}
这种方式存在 3 个明显问题:
- 代码冗余:每个接口都要写重复的
try-catch
和响应格式拼接; - 格式不统一:若多个开发者定义不同的
code
或msg
字段,前端需反复适配; - 异常遗漏:若忘记捕获某个异常(如
NullPointerException
),会返回默认 500 错误页,体验极差。
而全局异常处理能一次性解决这些问题 —— 只需定义一套规则,所有异常都会按统一格式返回,且无需在业务代码中写try-catch
。
二、核心技术:Spring Boot 的异常处理注解
Spring Boot 通过 3 个核心注解实现全局异常处理,需先理解其作用:注解 | 作用说明 |
---|---|
@RestControllerAdvice | 全局异常处理的 “入口”,是@ControllerAdvice +@ResponseBody 的组合,自动返回 JSON 格式响应(适合前后端分离);若用@ControllerAdvice ,需配合@ResponseBody 使用。 |
@ExceptionHandler | 定义 “异常处理器方法”,指定该方法处理哪类异常(如@ExceptionHandler(NullPointerException.class) 处理空指针异常)。 |
@ResponseStatus | 可选,为异常响应设置 HTTP 状态码(如 400、404、500),默认返回 200 OK。 |
此外,还需用到统一响应类—— 封装所有接口(正常响应 + 异常响应)的返回格式,确保前后端交互字段一致。
三、实战:从零实现全局异常处理
下面按 “统一响应→自定义异常→全局处理器→测试验证” 的步骤,搭建完整的全局异常处理方案。
步骤 1:定义统一响应类(核心前提)
首先创建`Result`类,统一所有接口的返回格式,包含 3 个核心字段:code
:业务状态码(如 200 = 成功,400 = 参数错误,500 = 系统错误);msg
:响应消息(成功 / 错误描述);data
:响应数据(正常响应时返回业务数据,异常时为null
)。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** 统一响应类*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {// 业务状态码private Integer code;// 响应消息private String msg;// 响应数据private T data;// -------------- 静态工厂方法:简化调用 --------------// 成功(无数据)public static <T> Result<T> success() {return new Result<>(200, "操作成功", null);}// 成功(有数据)public static <T> Result<T> success(T data) {return new Result<>(200, "操作成功", data);}// 失败(自定义状态码和消息)public static <T> Result<T> fail(Integer code, String msg) {return new Result<>(code, msg, null);}// 失败(默认系统错误)public static <T> Result<T> error() {return new Result<>(500, "服务器内部错误", null);}
}
有了这个类,正常接口的返回会更简洁,例如:
// 正常响应示例
@GetMapping("/{id}")
public Result<User> getUserById(@PathVariable Long id) {User user = userService.getById(id);if (user == null) {// 直接返回失败响应,无需拼接JSONreturn Result.fail(404, "用户不存在");}return Result.success(user);
}
步骤 2:定义自定义业务异常
实际开发中,很多异常是业务相关的(如 “用户余额不足”“订单已取消”),需要携带业务状态码。此时需定义**自定义异常类**,继承`RuntimeException`(无需强制捕获),并包含`code`字段。import lombok.Getter;/*** 自定义业务异常(如用户不存在、参数非法等)*/
@Getter // 提供code和message的getter方法,供全局处理器获取
public class BusinessException extends RuntimeException {// 业务状态码(如404=用户不存在,400=参数错误)private final Integer code;// 构造方法1:自定义code和messagepublic BusinessException(Integer code, String message) {super(message); // 父类RuntimeException的message字段this.code = code;}// 构造方法2:默认code=400(参数错误)public BusinessException(String message) {this(400, message);}
}
后续业务中抛出异常时,直接用自定义异常:
// 业务层抛出自定义异常示例
public void deductBalance(Long userId, BigDecimal amount) {User user = userMapper.selectById(userId);if (user == null) {// 抛出“用户不存在”异常,携带code=404throw new BusinessException(404, "用户不存在");}if (user.getBalance().compareTo(amount) < 0) {// 抛出“余额不足”异常,默认code=400throw new BusinessException("用户余额不足");}// 后续扣减余额逻辑...
}
步骤 3:编写全局异常处理器(核心实现)
创建GlobalExceptionHandler
类,用@RestControllerAdvice
标注,内部定义多个@ExceptionHandler
方法,分别处理不同类型的异常(自定义异常、系统异常、参数校验异常等)。
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;/*** 全局异常处理器:统一处理所有异常*/
@RestControllerAdvice(basePackages = "cn.varin.demo.controller") // 只处理指定包下的Controller异常
@Slf4j // 记录异常日志
public class GlobalExceptionHandler {// ---------------- 1. 处理自定义业务异常 ----------------@ExceptionHandler(BusinessException.class)public Result<Void> handleBusinessException(BusinessException e) {// 记录异常日志(级别为WARN,因为是业务已知异常)log.warn("业务异常:{},状态码:{}", e.getMessage(), e.getCode());// 返回自定义的业务状态码和消息return Result.fail(e.getCode(), e.getMessage());}// ---------------- 2. 处理参数校验异常(如@NotNull、@Size) ----------------// 注:需配合Spring Validation依赖(如spring-boot-starter-validation)@ExceptionHandler(MethodArgumentNotValidException.class)@ResponseStatus(HttpStatus.BAD_REQUEST) // 设置HTTP状态码为400public Result<Void> handleValidException(MethodArgumentNotValidException e) {// 获取参数校验失败的第一个错误信息String errMsg = e.getBindingResult().getFieldError().getDefaultMessage();log.warn("参数校验异常:{}", errMsg);// 返回code=400和错误信息return Result.fail(400, errMsg);}// ---------------- 3. 处理系统通用异常(如NullPointerException、IllegalArgumentException) ----------------@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 设置HTTP状态码为500public Result<Void> handleGeneralException(Exception e) {// 记录异常堆栈信息(级别为ERROR,因为是未知系统异常,需排查)log.error("系统异常:", e); // 注意这里要传e,才能打印堆栈// 隐藏具体异常信息,返回通用提示(避免泄露系统细节)return Result.error();}
}
关键说明:
basePackages
属性:限定处理器的作用范围,只处理cn.varin.demo.controller
包下的 Controller 抛出的异常,避免处理其他第三方组件的异常;- 日志记录分级:业务异常(
BusinessException
)用warn
级别(已知问题,无需紧急处理),系统异常用error
级别(未知问题,需排查); @ResponseStatus
:为异常响应设置 HTTP 状态码(如参数校验失败返回 400,系统错误返回 500),前端可通过状态码快速判断异常类型;- 参数校验异常:需引入
spring-boot-starter-validation
依赖,配合@Valid
注解使用(后续会演示)。
步骤 4:引入参数校验依赖(可选但推荐)
若要处理参数校验异常(如@NotNull
、@Min
),需在pom.xml
中引入依赖:
<!-- Spring Validation:参数校验 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>
步骤 5:测试验证
创建测试 Controller,模拟不同异常场景,验证全局异常处理是否生效。
测试 1:自定义业务异常
@RestController
@RequestMapping("/user")
public class UserController {@PostMapping("/deduct")public Result<Void> deductBalance(@RequestParam Long userId, @RequestParam BigDecimal amount) {// 调用业务层方法,内部会抛出BusinessExceptionuserService.deductBalance(userId, amount);return Result.success();}
}
请求示例:POST /user/deduct?userId=999&amount=100
(用户 ID=999 不存在)
响应结果(统一格式):
{"code": 404,"msg": "用户不存在","data": null}
控制台日志:WARN cn.varin.demo.exception.GlobalExceptionHandler - 业务异常:用户不存在,状态码:404
测试 2:参数校验异常
// 用@Valid注解开启参数校验,@RequestBody接收对象参数
@PostMapping("/add")
public Result<User> addUser(@Valid @RequestBody User user) {userService.save(user);return Result.success(user);
}// User类(添加参数校验注解)
@Data
public class User {@NotNull(message = "用户名不能为空") // 校验username不能为null@Size(min = 2, max = 10, message = "用户名长度必须在2-10之间") // 校验长度private String username;@Min(value = 18, message = "年龄不能小于18岁") // 校验age最小值private Integer age;
}
请求示例:提交{"username":"a","age":17}
(用户名长度 1,年龄 17)
响应结果:
{"code": 400,"msg": "用户名长度必须在2-10之间","data": null}
控制台日志:WARN cn.varin.demo.exception.GlobalExceptionHandler - 参数校验异常:用户名长度必须在2-10之间
测试 3:系统异常(空指针)
@GetMapping("/test/error")
public Result<Void> testSystemError() {// 模拟空指针异常(未处理)String str = null;str.length(); // 此处会抛出NullPointerExceptionreturn Result.success();
}
请求示例:GET /user/test/error
响应结果(隐藏具体异常信息):
{"code": 500,"msg": "服务器内部错误","data": null}
控制台日志(打印完整堆栈,方便排查):
ERROR cn.varin.demo.exception.GlobalExceptionHandler - 系统异常:
java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null
at cn.varin.demo.controller.UserController.testSystemError(UserController.java:45)
...(后续堆栈省略)
四、进阶场景:处理特殊异常
除了上述常见场景,还有一些特殊异常需要单独处理,确保覆盖全面。1. 处理 404(资源未找到)异常
Spring Boot 默认的 404 响应是 HTML 页面,需自定义 404 异常处理器,返回 JSON 格式:
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) // HTTP状态码404
public Result<Void> handle404Exception(NoHandlerFoundException e) {
log.warn("资源未找到:{}", e.getRequestURL());
return Result.fail(404, "请求的接口不存在");
}
注意:需在application.yml
中开启 404 异常抛出配置,否则无法被全局处理器捕获:
spring:mvc:throw-exception-if-no-handler-found: true # 开启404异常抛出web:resources:add-mappings: false # 禁用默认的静态资源映射(避免静态资源404被捕获)
2. 处理异步方法中的异常
若方法用@Async
标注(异步执行),其抛出的异常无法被默认的全局处理器捕获,需自定义AsyncUncaughtExceptionHandler
:
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;@Configuration
@EnableAsync // 开启异步支持
public class AsyncConfig implements AsyncConfigurer {// 自定义异步异常处理器@Overridepublic AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {return (ex, method, params) -> {// 记录异步异常日志log.error("异步方法[{}]执行异常,参数:{}", method.getName(), params, ex);// 若需要通知(如邮件、钉钉),可在此处添加逻辑};}
}
3. 区分开发 / 生产环境的异常响应
开发环境需要显示异常堆栈(方便调试),生产环境需隐藏细节。可通过@Profile
注解实现环境区分:
// 开发环境:返回详细异常信息
@ExceptionHandler(Exception.class)
@Profile("dev") // 仅dev环境生效
public Result<Void> handleDevGeneralException(Exception e) {
log.error("系统异常:", e);
// 返回异常堆栈的字符串形式(简化)
String stackTrace = Arrays.stream(e.getStackTrace())
.map(StackTraceElement::toString)
.limit(5) // 只显示前5行堆栈
.collect(Collectors.joining("\n"));
return Result.fail(500, "开发环境异常:" + e.getMessage() + "\n" + stackTrace);
}// 生产环境:隐藏细节
@ExceptionHandler(Exception.class)
@Profile("prod") // 仅prod环境生效
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleProdGeneralException(Exception e) {
log.error("系统异常:", e);
return Result.fail(500, "服务器繁忙,请稍后再试");
}
五、常见问题与解决方案
- 全局异常处理器不生效?
- 检查
@RestControllerAdvice
的basePackages
是否包含 Controller 所在包; - 检查异常是否被业务代码中的
try-catch
捕获(若捕获后未重新抛出,处理器无法感知); - 检查是否引入了正确的依赖(如参数校验需
spring-boot-starter-validation
)。
- 404 异常无法被捕获?
- 必须在配置文件中开启
spring.mvc.throw-exception-if-no-handler-found=true
和spring.web.resources.add-mappings=false
。
- 异步方法异常无法被捕获?
- 需实现
AsyncConfigurer
,自定义AsyncUncaughtExceptionHandler
,不能依赖默认的全局处理器。
六、最佳实践总结
- 统一响应格式:所有接口(正常 / 异常)都通过
Result
类返回,避免格式混乱; - 分类处理异常:自定义业务异常(已知)、系统异常(未知)、特殊异常(404、异步)分开处理,日志分级记录;
- 隐藏敏感信息:生产环境不返回异常堆栈,避免泄露系统架构;
- 配合参数校验:用
Spring Validation
+ 全局处理器自动处理参数错误,减少重复代码; - 环境差异化:开发环境返回详细异常信息,生产环境返回友好提示。
通过自定义全局异常处理,不仅能提升代码的简洁性和可维护性,还能让前后端交互更高效。你在项目中遇到过哪些特殊的异常处理场景?欢迎在评论区交流!