一、什么是全局异常处理?
在实际开发中,程序运行时难免会遇到各种意外。这些意外在 Java 中就表现为异常(Exception)。如果不做统一处理,用户可能会看到一堆错误信息。
常见场景:
-
NullPointerException(空指针异常):前端传了一个用户 ID,但后端没做判空,直接调用user.getName(),结果 user 是 null 导致程序崩溃! -
SQLException(数据库连接失败):数据库宕机或网络中断,导致查询失败,直接抛出 SQL 异常。 -
NumberFormatException(参数格式错误):前端传了个字符串"abc",后端试图用Integer.parseInt()转成数字失败!
如果不做处理,会发生什么?
SpringBoot 默认会把异常的完整堆栈信息原样返回给前端,比如:
json
{
"timestamp": "2025-11-26T12:00:00",
"status": 500,
"error": "Internal Server Error",
"message": "/ by zero",
"path": "/api/calculate"
}
甚至在开发环境下,还可能返回几十行 Java 堆栈,这些会导致以下以下问题:
- 用户看不懂,
- 前端无法统一处理,
- 更严重的是:可能泄露类名、方法名、服务器路径等敏感信息!
有了全局异常处理,就能:
- 拦截所有异常,统一返回结构化错误信息
- 区分不同异常类型,返回不同的状态码和提示语
- 自动记录日志,方便排查问题
- 避免敏感信息外泄,提升系统安全性
最终返回给前端的,可能是这样一段干净、友好的 JSON:
json
{
"code": 400,
"message": "用户ID不能为空",
"data": null
}
或者:
json
{
"code": 200,
"message": "订单创建成功",
"data": true
}
是不是清爽又专业多了😊
二、如何做异常处理
传统做法(不推荐)
每个 Controller 方法里都写 try-catch:
java
@GetMapping("/user/{id}")
public ResponseEntity getUser(@PathVariable Long id) {
try {
User user = userService.findById(id);
return ResponseEntity.ok(user);
} catch (UserNotFoundException e) {
return ResponseEntity.status(404).body("用户不存在");
} catch (Exception e) {
return ResponseEntity.status(500).body("系统错误");
}
}
问题很明显:
- 代码重复
- 难维护
- 容易漏掉
全局异常处理(推荐)
只写一次,全局生效!
下面主要写 SpringBoot 的处理。
三、SpringBoot 中如何实现全局异常处理?
核心就靠一个注解:@ControllerAdvice
第一步:定义统一的错误响应格式
java
// 统一返回结构,前端好解析
public class ErrorResponse {
private int code;
private String message;
private Object data; // 一般为 null,但保留扩展性
public ErrorResponse(int code, String message) {
this.code = code;
this.message = message;
}
// getter / setter 省略(实际项目中建议用 Lombok)
}
第二步:创建全局异常处理器类
java
// 1. 创建一个类,加上 @ControllerAdvice 注解
@ControllerAdvice
public class GlobalExceptionHandler {
// 2. 用 @ExceptionHandler 捕获特定异常
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity handleUserNotFound(UserNotFoundException e) {
ErrorResponse error = new ErrorResponse(404, e.getMessage());
return ResponseEntity.status(404).body(error);
}
// 3. 捕获所有其他异常(兜底)
@ExceptionHandler(Exception.class)
public ResponseEntity handleGenericException(Exception e) {
ErrorResponse error = new ErrorResponse(500, "服务器内部错误,请联系管理员");
return ResponseEntity.status(500).body(error);
}
}
第三步:自定义业务异常(可选但推荐)
java
// 自定义业务异常
public class BusinessException extends RuntimeException {
private Integer code;
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
// getter省略
}
然后在 Service 层这样用:
java
public User findById(Long id) {
if (id == null || id <= 0) {
throw new BusinessException("用户ID无效");
}
// 查询数据库...
}
可以说配置全局异常是比较简单的,可能因为简单,或者是项目里本来就配置的有,所以很多朋友直接忽略掉了这些问题。
四、更智能的异常处理
1. 区分 Controller 和 REST 接口
如果你既有页面(返回 HTML),又有 API(返回 JSON),可以用:
java
@ControllerAdvice(annotations = RestController.class)
public class RestGlobalExceptionHandler {
// 只处理 @RestController 的异常
}
或者指定包路径:
java
@ControllerAdvice(basePackages = "com.example.api")
public class ApiExceptionHandler { ... }
2. 处理参数校验异常(如 @Valid)
SpringBoot 自带校验框架,校验失败会抛 MethodArgumentNotValidException:
java
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldError().getDefaultMessage();
return ResponseEntity.badRequest().body(new ErrorResponse(400, msg));
}
3. 记录日志(非常重要!)
在 handleGenericException 中加入日志:
java
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(Exception.class)
public ResponseEntity handleGenericException(Exception e) {
log.error("系统发生未预期异常", e); // 记录完整堆栈
return ResponseEntity.status(500).body(new ErrorResponse(500, "服务器开小差啦~"));
}
五、深入理解@ControllerAdvice
很多同学可能会好奇:为什么加了@ControllerAdvice后,异常就能被自动捕获呢?
其实原理很简单:
Spring MVC在处理请求时,如果 Controller 方法抛出异常Spring会查找有没有对应的异常处理器(@ExceptionHandler)- 查找顺序是:先找当前 Controller 中的
@ExceptionHandler方法 - 如果当前 Controller 没有,就找
@ControllerAdvice标注的类中的@ExceptionHandler方法 - 找到匹配的异常处理器后,调用对应的方法处理异常
最佳实践建议
1. 定义清晰的异常体系
java
// 基础异常类
public class BaseException extends RuntimeException {
private Integer code;
public BaseException(Integer code, String message) {
super(message);
this.code = code;
}
}
// 各种具体异常
public class BusinessException extends BaseException {
public BusinessException(Integer code, String message) {
super(code, message);
}
}
public class SystemException extends BaseException {
public SystemException(Integer code, String message) {
super(code, message);
}
}
2. 使用枚举管理错误码
java
public enum ErrorCode {
SUCCESS(200, "成功"),
PARAM_ERROR(400, "参数错误"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "资源不存在"),
SYSTEM_ERROR(500, "系统错误");
private final Integer code;
private final String message;
ErrorCode(Integer code, String message) {
this.code = code;
this.message = message;
}
// getter方法
}
3. 日志记录要恰当
- 业务异常:一般用warn级别,不需要记录堆栈
- 系统异常:用error级别,需要记录堆栈信息
完整示例代码
java
// 统一返回结果
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {
private Integer code;
private String message;
private T data;
public static Result success(T data) {
return new Result<>(200, "成功", data);
}
public static Result error(Integer code, String message) {
return new Result<>(code, message, null);
}
}
// 全局异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 处理业务异常
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
logger.warn("业务异常:code={}, message={}", e.getCode(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.error(400, message);
}
// 处理所有其他异常
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
logger.error("系统异常:", e);
return Result.error(500, "系统繁忙,请稍后重试");
}
}
总结
统一格式 :前后端约定好错误结构,沟通更顺畅
减少重复代码 :不用每个方法写 try-catch
提升健壮性:兜底处理防止系统崩溃,还能记录日志
本文首发于公众号:程序员大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《这 5 个冷门 HTML 标签,让我直接删了100 行 JS 代码》