Spring Boot 全局异常处理

✅ 引言

在开发 Spring Boot 项目时,你是否遇到过这些问题?

  • ❌ 不同 Controller 重复写 try-catch
  • ❌ 异常信息格式不统一,前端难以解析
  • ❌ 系统内部错误直接暴露给用户(如堆栈信息)
  • ❌ 404、500 等状态码处理混乱

这些问题不仅影响用户体验,还可能带来安全风险。

全局异常处理就是 Spring Boot 为我们提供的"统一调度中心",它能在异常发生时自动拦截、处理,并返回友好的响应。

本文将带你从零开始,构建一套生产级的全局异常处理机制,并结合实际代码,让你彻底掌握这一核心技能。


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

想象一个电商系统:

  • 用户下单时库存不足 → 返回"库存不足,请稍后再试"
  • 订单ID格式错误 → 返回"订单不存在"
  • 数据库连接失败 → 记录日志,返回"系统繁忙,请稍后重试"

如果没有统一处理,每个接口都要写类似的 try-catch,代码重复且难以维护。

而有了全局异常处理,就像设立了一个"客户服务中心",所有异常都由它统一接待、分类处理、礼貌回应。


📌 二、核心技术:@ControllerAdvice@ExceptionHandler

Spring Boot 提供了两个核心注解来实现全局异常处理:

注解 作用
@ControllerAdvice 定义全局异常处理器(可作用于所有 Controller)
@ExceptionHandler 指定处理某类异常的方法

📌 三、实战:构建统一异常处理机制

3.1 定义统一返回格式

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;

    // 成功响应
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(200, "success", data);
    }

    // 失败响应
    public static <T> ApiResponse<T> error(int code, String message) {
        return new ApiResponse<>(code, message, null);
    }
}

3.2 自定义业务异常

java 复制代码
// 业务异常基类
public class BusinessException extends RuntimeException {
    private int code;

    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }

    public BusinessException(String message) {
        super(message);
        this.code = 500;
    }

    // getter
    public int getCode() {
        return code;
    }
}

使用示例:

java 复制代码
@Service
public class OrderService {
    public void createOrder(Long productId, Integer quantity) {
        if (quantity <= 0) {
            throw new BusinessException(400, "购买数量必须大于0");
        }
        // ... 其他逻辑
    }
}

3.3 全局异常处理器(核心)

java 复制代码
@RestControllerAdvice  // 等价于 @ControllerAdvice + @ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 处理自定义业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ApiResponse<String> handleBusinessException(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage());
        return ApiResponse.error(e.getCode(), e.getMessage());
    }

    /**
     * 处理参数校验异常(@Valid)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<String> handleValidationException(MethodArgumentNotValidException e) {
        // 获取第一个错误信息
        String errorMessage = e.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(FieldError::getDefaultMessage)
                .findFirst()
                .orElse("参数校验失败");
        
        log.warn("参数校验异常: {}", errorMessage);
        return ApiResponse.error(400, errorMessage);
    }

    /**
     * 处理空指针异常
     */
    @ExceptionHandler(NullPointerException.class)
    public ApiResponse<String> handleNullPointerException(NullPointerException e) {
        log.error("空指针异常", e);
        return ApiResponse.error(500, "系统内部错误,请联系管理员");
    }

    /**
     * 处理所有未被捕获的异常(兜底)
     */
    @ExceptionHandler(Exception.class)
    public ApiResponse<String> handleException(Exception e) {
        log.error("未处理异常", e);
        return ApiResponse.error(500, "系统繁忙,请稍后重试");
    }

    /**
     * 处理 404 Not Found
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiResponse<String> handle404(NoHandlerFoundException e) {
        log.warn("请求路径不存在: {}", e.getRequestURL());
        return ApiResponse.error(404, "请求的资源不存在");
    }
}

3.4 启用 404 异常捕获(重要!)

默认情况下,404 异常不会进入 @ExceptionHandler,需在配置文件中开启:

yaml 复制代码
# application.yml
spring:
  mvc:
    throw-exception-if-no-handler-found: true  # 找不到处理器时抛出异常

  web:
    resources:
      add-mappings: false  # 关闭默认静态资源映射(可选,更严格)

📌 四、测试验证

4.1 创建测试 Controller

java 复制代码
@RestController
@RequestMapping("/api")
public class TestController {

    @GetMapping("/business")
    public ApiResponse<String> businessError() {
        throw new BusinessException(400, "用户名已存在");
    }

    @PostMapping("/validate")
    public ApiResponse<String> validate(@Valid @RequestBody UserForm form) {
        return ApiResponse.success("验证通过");
    }

    @GetMapping("/null")
    public ApiResponse<String> nullPointer() {
        String str = null;
        str.length(); // 触发 NullPointerException
        return ApiResponse.success("success");
    }

    @GetMapping("/unknown")
    public ApiResponse<String> unknown() {
        throw new RuntimeException("未知错误");
    }
}
java 复制代码
class UserForm {
    @NotBlank(message = "用户名不能为空")
    private String username;

    @Min(value = 18, message = "年龄不能小于18岁")
    private Integer age;

    // getter & setter
}

4.2 测试结果

请求 响应
GET /api/business {"code":400,"message":"用户名已存在","data":null}
POST /api/validate(无参数) {"code":400,"message":"用户名不能为空","data":null}
GET /api/null {"code":500,"message":"系统内部错误,请联系管理员","data":null}
GET /api/unknown {"code":500,"message":"系统繁忙,请稍后重试","data":null}
GET /api/not-exist {"code":404,"message":"请求的资源不存在","data":null}

📌 五、高级技巧与生产实践

5.1 异常分类管理

你可以为不同模块定义不同的异常处理器:

java 复制代码
// 用户模块异常处理器
@ControllerAdvice("com.example.controller.user")
public class UserExceptionHandler { ... }

// 订单模块异常处理器
@ControllerAdvice("com.example.controller.order")
public class OrderExceptionHandler { ... }

5.2 结合 AOP 记录异常日志

java 复制代码
@Aspect
@Component
public class ExceptionLogAspect {

    @AfterThrowing(pointcut = "@within(org.springframework.web.bind.annotation.RestController)", 
                   throwing = "ex")
    public void logException(JoinPoint joinPoint, Exception ex) {
        String methodName = joinPoint.getSignature().getName();
        log.error("方法 {} 发生异常: {}", methodName, ex.getMessage());
    }
}

5.3 返回错误码枚举(推荐)

java 复制代码
public enum ErrorCode {
    SUCCESS(200, "成功"),
    BAD_REQUEST(400, "请求参数错误"),
    UNAUTHORIZED(401, "未授权"),
    FORBIDDEN(403, "禁止访问"),
    NOT_FOUND(404, "资源不存在"),
    SERVER_ERROR(500, "系统内部错误");

    private final int code;
    private final String message;

    ErrorCode(int code, String message) {
        this.code = code;
        this.message = message;
    }

    // getter
}

使用:

java 复制代码
throw new BusinessException(ErrorCode.BAD_REQUEST);

✅ 总结:全局异常处理的最佳实践

实践 说明
✅ 使用 @RestControllerAdvice 统一返回 JSON 格式
✅ 自定义 BusinessException 区分业务异常与系统异常
✅ 记录日志 @Slf4j + log.error/warn
✅ 敏感信息脱敏 不要将数据库错误、堆栈信息暴露给前端
✅ 合理分类异常 优先处理具体异常,最后是 Exception
✅ 启用 404 捕获 配置 throw-exception-if-no-handler-found: true
相关推荐
码事漫谈8 分钟前
C++继承中的虚函数机制:从单继承到多继承的深度解析
后端
阿冲Runner9 分钟前
创建一个生产可用的线程池
java·后端
写bug写bug18 分钟前
你真的会用枚举吗
java·后端·设计模式
喵手1 小时前
如何利用Java的Stream API提高代码的简洁度和效率?
java·后端·java ee
-Xie-1 小时前
Maven(二)
java·开发语言·maven
掘金码甲哥1 小时前
全网最全的跨域资源共享CORS方案分析
后端
m0_480502641 小时前
Rust 入门 生命周期-next2 (十九)
开发语言·后端·rust
IT利刃出鞘1 小时前
Java线程的6种状态和JVM状态打印
java·开发语言·jvm
张醒言1 小时前
Protocol Buffers 中 optional 关键字的发展史
后端·rpc·protobuf
鹿鹿的布丁2 小时前
通过Lua脚本多个网关循环外呼
后端