源本科技 | 码上会

Spring Boot 自定义异常

2026/03/31
6
0

引言

在企业级 Spring Boot 开发中,全局异常处理是系统健壮性的核心保障。 传统 try-catch 硬编码存在代码冗余、业务与异常逻辑耦合、响应格式混乱、堆栈信息泄露等致命问题。Spring Boot 提供的注解式全局异常处理方案,可以完美解决这些问题,实现:

  • 逻辑解耦:业务代码只关注核心逻辑,异常统一集中处理

  • 响应标准化:前后端约定统一的错误格式,降低对接成本

  • 安全隔离:屏蔽底层异常堆栈,向用户返回友好提示

  • 日志规范化:统一记录异常日志,方便问题排查


核心注解

Spring 全局异常处理依赖两个核心注解,是实现功能的基础:

  1. @RestControllerAdvice

    • 组合注解:@ControllerAdvice + @ResponseBody

    • 作用:全局捕获所有 Controller 层抛出的异常,并直接返回 JSON 格式响应

    • 范围:默认拦截所有包下的控制器,可通过 basePackages 指定扫描范围

  2. @ExceptionHandler

    • 作用:标注在方法上,指定需要捕获的异常类型

    • 优先级:精确匹配异常 > 父类异常(如先捕获BusinessException,再捕获Exception


统一错误码枚举

企业级规范

禁止硬编码错误码(如 4001、500),企业开发必须使用枚举类统一管理所有错误码,便于维护和前端对接:

package com.example.demo.common;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 全局错误码枚举
 * 约定:
 * 200 = 成功
 * 4xxx = 客户端异常(参数错误、资源不存在)
 * 5xxx = 服务端异常(系统错误、未知异常)
 */
@Getter
@AllArgsConstructor
public enum ErrorCode {

    // 成功
    SUCCESS(200, "操作成功"),
    // 客户端异常
    PARAM_ERROR(4000, "参数格式错误"),
    RESOURCE_NOT_FOUND(4001, "资源不存在"),
    // 服务端异常
    SERVER_ERROR(5000, "服务器繁忙,请稍后再试"),
    UNKNOWN_ERROR(5001, "未知异常");

    private final Integer code;
    private final String msg;
}

统一响应结果类

基于枚举封装标准化响应体,同时支持成功 / 失败两种场景,前后端通用:

package com.example.demo.common;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class Result<T> {
    private Integer code;    // 响应码
    private String message; // 响应信息
    private T data;         // 响应数据

    // 1. 成功响应(带数据)
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(ErrorCode.SUCCESS.getCode());
        result.setMessage(ErrorCode.SUCCESS.getMsg());
        result.setData(data);
        return result;
    }

    // 2. 成功响应(无数据)
    public static <T> Result<T> success() {
        return success(null);
    }

    // 3. 失败响应(自定义错误码+提示语)
    public static <T> Result<T> error(Integer code, String message) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        return result;
    }

    // 4. 失败响应(使用枚举)
    public static <T> Result<T> error(ErrorCode errorCode) {
        return error(errorCode.getCode(), errorCode.getMsg());
    }
}

自定义业务异常

继承 RuntimeException非受检异常,无需手动 try-catch),绑定错误码枚举,适配业务场景:

package com.example.demo.exception;

import com.example.demo.common.ErrorCode;
import lombok.Getter;

/**
 * 自定义业务异常(所有业务错误都抛出此异常)
 */
@Getter
public class BusinessException extends RuntimeException {

    private final Integer code;
    private final String message;

    // 自定义错误码+信息
    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }

    // 使用枚举
    public BusinessException(ErrorCode errorCode) {
        super(errorCode.getMsg());
        this.code = errorCode.getCode();
        this.message = errorCode.getMsg();
    }
}

全局异常处理器

捕获所有常见异常(业务异常、参数校验、系统异常、请求异常等),统一处理并记录日志,是企业开发标准配置:

package com.example.demo.handler;

import com.example.demo.common.ErrorCode;
import com.example.demo.common.Result;
import com.example.demo.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import java.util.Objects;

/**
 * 全局异常处理器
 */
@Slf4j
@RestControllerAdvice(basePackages = "com.example.demo") // 指定扫描包,缩小范围
public class GlobalExceptionHandler {

    // ====================== 1. 自定义业务异常(优先处理) ======================
    @ExceptionHandler(BusinessException.class)
    public Result<?> handleBusinessException(BusinessException e) {
        log.warn("【业务异常】code:{},message:{}", e.getCode(), e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    // ====================== 2. 参数校验异常(@Valid 校验失败) ======================
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidException(MethodArgumentNotValidException e) {
        // 获取校验失败的提示信息
        String message = Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage();
        log.warn("【参数校验异常】message:{}", message);
        return Result.error(ErrorCode.PARAM_ERROR.getCode(), message);
    }

    // ====================== 3. 参数类型转换异常 ======================
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public Result<?> handleTypeMismatchException(MethodArgumentTypeMismatchException e) {
        String message = "参数类型错误:" + e.getName();
        log.warn("【参数类型异常】message:{}", message);
        return Result.error(ErrorCode.PARAM_ERROR.getCode(), message);
    }

    // ====================== 4. 请求方法不支持异常 ======================
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public Result<?> handleMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        String message = "请求方法错误,支持:" + e.getSupportedHttpMethods();
        log.warn("【请求方法异常】message:{}", message);
        return Result.error(ErrorCode.PARAM_ERROR.getCode(), message);
    }

    // ====================== 5. 空指针异常 ======================
    @ExceptionHandler(NullPointerException.class)
    public Result<?> handleNullPointerException(NullPointerException e) {
        log.error("【空指针异常】", e);
        return Result.error(ErrorCode.SERVER_ERROR);
    }

    // ====================== 6. 兜底:所有未知异常(最后处理) ======================
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
        log.error("【系统未知异常】", e);
        return Result.error(ErrorCode.SERVER_ERROR);
    }
}

实战使用

Controller

package com.example.demo.controller;

import com.example.demo.common.Result;
import com.example.demo.exception.BusinessException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public Result<String> getUser(@PathVariable Long id) {
        // 模拟业务:ID=100 抛出资源不存在异常
        if (id == 100) {
            throw new BusinessException(4001, "用户ID=" + id + "不存在");
        }
        return Result.success("用户信息:张三");
    }
}

Service

package com.example.demo.service;

import com.example.demo.exception.BusinessException;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    public void checkUser(Long id) {
        if (id == null) {
            throw new BusinessException(4000, "用户ID不能为空");
        }
    }
}

测试验证

启动项目,测试不同异常场景,验证全局异常处理效果:

请求地址

异常类型

响应结果 JSON

GET /users/1

无异常(成功)

{"code":200,"message":"操作成功","data":"用户信息:张三"}

GET /users/100

自定义业务异常

{"code":4001,"message":"用户ID=100不存在","data":null}

GET /users/abc

参数类型转换异常

{"code":4000,"message":"参数类型错误:id","data":null}

POST /users/1

请求方法不支持异常

{"code":4000,"message":"请求方法错误...","data":null}


其他补充

1. 异常处理优先级

精确匹配的异常 > 父类异常 例:先捕获 BusinessException,再捕获 Exception,避免兜底异常覆盖业务异常。

2. 受检异常 vs 非受检异常

  • 自定义异常必须继承 RuntimeException(非受检异常),无需在方法上声明 throws

  • 禁止继承 Exception(受检异常),会强制代码写 try-catch,违背解耦初衷

3. 全局异常生效范围

  • @RestControllerAdvice 默认拦截所有包的控制器

  • 生产环境建议指定 basePackages,缩小扫描范围,提升性能

4. 日志规范

  • 业务异常:log.warn()(警告级别,非严重错误)

  • 系统异常:log.error()(错误级别,必须打印堆栈)

  • 禁止向客户端暴露堆栈信息,仅在控制台打印


最佳实践

  1. 统一错误码:所有错误码使用枚举管理,杜绝魔法值

  2. 分层抛异常:Service 层抛业务异常,Controller 层不处理异常

  3. 全覆盖异常:捕获参数校验、类型转换、请求方法、空指针等所有常见异常

  4. 安全提示:客户端只返回友好提示,堆栈信息仅记录在日志

  5. 参数校验:配合 @Valid 实现自动参数校验,异常统一处理

  6. 范围控制:指定异常处理器扫描包,避免不必要的性能开销


总结

  1. 核心注解@RestControllerAdvice(全局拦截)+ @ExceptionHandler(捕获指定异常)

  2. 三层架构:统一响应类 + 错误码枚举 + 自定义异常 + 全局处理器

  3. 核心价值:解耦业务与异常逻辑、标准化响应、安全隔离、日志规范化

  4. 企业标准:这是 Spring Boot 开发必写的基础配置,所有项目通用