Spring异常处理(@ControllerAdvice)

Spring 异常处理终极版(2025 最新生产级写法)

99% 的项目都只用这一套就完全够了,统一、优雅、可维护、可监控,直接复制到项目里开箱即用!

一、2025 年真实项目推荐层级(从外到内)

层级处理方式作用范围推荐指数
1Filter(最外层)所有请求(包括静态资源)1 star
2Spring Security ExceptionTranslationFilter认证/授权异常5 stars
3@ControllerAdvice + @ExceptionHandlerSpring MVC 所有 Controller5 stars
4HandlerExceptionResolver(底层)兜底内部使用

结论:99.9% 的项目只需要 @ControllerAdvice 就完美解决所有异常处理!

二、生产级统一异常处理完整代码(直接复制)

@RestControllerAdvice   // = @ControllerAdvice + @ResponseBody
@Order(Ordered.HIGHEST_PRECEDENCE)  // 优先级最高(可选)
@Slf4j
public class GlobalExceptionHandler {

    /** 1. 校验异常(@Valid、@Validated) */
    @ExceptionHandler(BindException.class)
    public R<String> handleBindException(BindException e) {
        String msg = e.getAllErrors().get(0).getDefaultMessage();
        log.warn("参数校验失败: {}", msg);
        return R.error(400, "参数错误:" + msg);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R<String> handleValidException(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
        log.warn("JSON 参数校验失败: {}", msg);
        return R.error(400, msg);
    }

    /** 2. 自定义业务异常(最常用!) */
    @ExceptionHandler(BusinessException.class)
    public R<String> handleBusiness(BusinessException e) {
        log.warn("业务异常: code={}, msg={}", e.getCode(), e.getMessage());
        return R.error(e.getCode(), e.getMessage());
    }

    /** 3. 权限相关 */
    @ExceptionHandler(AccessDeniedException.class)
    public R<String> handleAccessDenied(AccessDeniedException e) {
        return R.error(403, "无权限访问");
    }

    /** 4. 404 - 资源不存在 */
    @ExceptionHandler(NoHandlerFoundException.class)
    public R<String> handleNotFound(NoHandlerFoundException e) {
        return R.error(404, "接口不存在:" + e.getRequestURL());
    }

    /** 5. 文件上传大小超限 */
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public R<String> handleMaxUploadSize(MaxUploadSizeExceededException e) {
        return R.error(413, "文件太大,最大支持 100MB");
    }

    /** 6. 通用运行时异常(兜底) */
    @ExceptionHandler(Exception.class)
    public R<String> handleException(Exception e) {
        log.error("未捕获异常", e);
        String traceId = IdUtil.fastSimpleUUID();  // hutool
        return R.error(500, "服务器异常,请稍后重试(traceId: " + traceId + ")");
    }
}

三、自定义业务异常体系(强烈推荐!)

// 基础业务异常
@Getter
public class BusinessException extends RuntimeException {
    private final int code;

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

    public BusinessException(ResultCode resultCode) {
        super(resultCode.getMsg());
        this.code = resultCode.getCode();
    }
}

// 常用枚举码(放在 constants 包)
@Getter
public enum ResultCode {
    SUCCESS(200, "操作成功"),
    FAIL(500, "操作失败"),

    PARAM_ERROR(1001, "参数错误"),
    USER_NOT_FOUND(1002, "用户不存在"),
    LOGIN_EXPIRED(1003, "登录已过期"),
    STOCK_NOT_ENOUGH(1004, "库存不足"),
    ORDER_NOT_FOUND(1005, "订单不存在");

    private final int code;
    private String msg;

    ResultCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

使用方式(Controller/Service 里直接扔):

if (user == null) {
    throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
if (stock < num) {
    throw new BusinessException(ResultCode.STOCK_NOT_ENOUGH);
}

四、统一响应体 R(所有接口都返回这个)

@Getter
@Setter
@NoArgsConstructor
public class R<T> implements Serializable {
    private int code = 200;
    private String msg = "success";
    private T data;

    public static <T> R<T> ok() {
        return new R<>();
    }

    public static <T> R<T> ok(T data) {
        R<T> r = new R<>();
        r.setData(data);
        return r;
    }

    public static <T> R<T> error(int code, String msg) {
        R<T> r = new R<>();
        r.setCode(code);
        r.setMsg(msg);
        return r;
    }

    public static <T> R<T> error(ResultCode resultCode) {
        return error(resultCode.getCode(), resultCode.getMsg());
    }
}

五、Spring Boot 3 额外配置(让 404 也能被 @ControllerAdvice 捕获)

# application.yml
spring:
  mvc:
    throw-exception-if-no-handler-found: true   # 关键!默认 false
  web:
    resources:
      add-mappings: false   # 防止静态资源 404 被吞掉(可选)

六、2025 年最优雅写法(支持返回指定状态码)

@ExceptionHandler(DuplicateKeyException.class)
@ResponseStatus(HttpStatus.CONFLICT)  // 返回 409
public R<String> handleDuplicateKey(DuplicateKeyException e) {
    return R.error(409, "数据已存在");
}

或者使用 ResponseEntity:

@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<R<String>> handleAccessDenied() {
    return ResponseEntity.status(403).body(R.error(403, "无权限"));
}

七、真实项目最终效果(前后端都爽)

// 正常返回
{
  "code": 200,
  "msg": "success",
  "data": {...}
}

// 业务异常
{
  "code": 1002,
  "msg": "用户不存在",
  "data": null
}

// 参数错误
{
  "code": 400,
  "msg": "参数错误:用户名不能为空",
  "data": null
}

// 服务器异常
{
  "code": 500,
  "msg": "服务器异常,请稍后重试(traceId: a1b2c3d4e5)",
  "data": null
}

八、避坑指南

坑点解决方案
@Valid 校验异常没生效方法参数要加 @Valid + BindingResult,或直接用 @ControllerAdvice 捕获 MethodArgumentNotValidException
404 接口没被捕获配置 throw-exception-if-no-handler-found: true
想返回自定义状态码加 @ResponseStatus 或返回 ResponseEntity
多个 @ControllerAdvice 冲突加 @Order(Ordered.HIGHEST_PRECEDENCE) 控制顺序
想区分开发/生产环境返回堆栈加 profile 判断

现在你已经拥有了企业级最优雅、最完整的异常处理体系!

下一步你想看哪个实战?

  • 结合 Spring Validation 做分组校验 + 自定义校验器
  • 集成 Sentry/ELK 异常监控 + 钉钉报警
  • 结合 Spring Security 的权限异常处理
  • 401/403 统一处理
  • 异步请求(@Async)异常如何捕获
    直接告诉我,我继续给你写 10 万字级干货!
文章已创建 3070

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部