作者:不想打工的码农
标签:#SpringBoot #Java #异常处理 #后端开发 #RESTful

大家好,我是“不想打工的码农”。

上周上线一个新功能,测试时一切正常,结果第二天客户反馈:“点保存按钮没反应!”
我一查日志,发现是数据库字段长度超限,抛了个 DataIntegrityViolationException,但前端只收到一个空白的 500 响应。

这让我意识到:很多开发者只关注“正常流程”,却忽略了“异常流程”的用户体验。

今天,我就用一个真实项目案例,教你怎么用 Spring Boot 的 @ControllerAdvice 做一套专业、统一、可维护的全局异常处理机制。


一、先看错误示范:到处 try-catch

很多初学者会这样写:

@PostMapping("/student")
public ResponseEntity<?> saveStudent(@RequestBody Student student) {
    try {
        studentService.save(student);
        return ResponseEntity.ok("保存成功");
    } catch (Exception e) {
        log.error("保存学生失败", e);
        return ResponseEntity.status(500).body("系统错误");
    }
}

问题很明显

  • 每个接口都要写 try-catch,代码重复
  • 错误信息太笼统(“系统错误”),前端无法提示用户
  • 无法区分不同异常类型(比如参数校验失败 vs 数据库异常)

二、正确做法:用 @ControllerAdvice 统一处理

第一步:定义统一返回格式

先约定前后端的数据结构,避免各写各的:

public class Result<T> {
    private int code;      // 200=成功,400=参数错误,500=系统错误
    private String msg;    // 错误描述
    private T data;        // 成功时返回的数据

    // 构造方法
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "success", data);
    }

    public static <T> Result<T> error(int code, String msg) {
        return new Result<>(code, msg, null);
    }

    // getter/setter 省略(实际项目必须有)
}

第二步:创建全局异常处理器

新建 GlobalExceptionHandler.java

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.validation.BindException;
import javax.validation.ConstraintViolationException;

@RestControllerAdvice // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    // 处理参数校验失败(@Valid 注解触发的)
    @ExceptionHandler(BindException.class)
    public Result<?> handleBindException(BindException e) {
        String msg = e.getBindingResult().getFieldError().getDefaultMessage();
        return Result.error(400, "参数错误:" + msg);
    }

    // 处理 JSR-303 校验失败(@Validated + 方法参数)
    @ExceptionHandler(ConstraintViolationException.class)
    public Result<?> handleConstraintViolation(ConstraintViolationException e) {
        String msg = e.getConstraintViolations().iterator().next().getMessage();
        return Result.error(400, "参数错误:" + msg);
    }

    // 处理数据库约束异常(如唯一索引冲突、字段超长)
    @ExceptionHandler(DataIntegrityViolationException.class)
    public Result<?> handleDbException(DataIntegrityViolationException e) {
        log.error("数据库操作异常", e);
        if (e.getMessage().contains("Duplicate entry")) {
            return Result.error(400, "该学号已存在,请修改后重试");
        }
        if (e.getMessage().contains("Data too long")) {
            return Result.error(400, "输入内容过长,请检查后提交");
        }
        return Result.error(500, "数据保存失败");
    }

    // 处理所有未捕获的异常(兜底)
    @ExceptionHandler(Exception.class)
    public Result<?> handleGenericException(Exception e) {
        log.error("系统未知异常", e);
        return Result.error(500, "系统繁忙,请稍后再试");
    }
}

✅ 关键点说明

  • @RestControllerAdvice 会自动把返回对象转成 JSON
  • 按异常类型分层处理,越具体的异常放前面
  • 所有异常都记录日志,便于排查
  • 返回给前端的信息要具体但不暴露敏感信息(比如不说“主键冲突”,而说“学号已存在”)

第三步:在 Controller 中只写正常逻辑

现在你的接口可以清爽多了:

@PostMapping("/student")
public Result<String> saveStudent(@Valid @RequestBody Student student) {
    studentService.save(student);
    return Result.success("保存成功");
}

注意:这里加了 @Valid,当参数不符合校验规则时,会自动抛出 BindException,被全局处理器捕获。


三、验证效果

  1. 正常请求:json
    { "code": 200, "msg": "success", "data": "保存成功" }
  2. 参数校验失败(比如姓名为空):json
    { "code": 400, "msg": "参数错误:姓名不能为空", "data": null }
  3. 数据库唯一索引冲突:json
    { "code": 400, "msg": "该学号已存在,请修改后重试", "data": null }
  4. 其他未知异常:json
    { "code": 500, "msg": "系统繁忙,请稍后再试", "data": null }

前端拿到这样的响应,就能友好地提示用户,而不是显示“网络错误”或白屏。


四、常见问题

Q1:为什么我的 BindException 没被捕获?

A:检查 Controller 方法参数是否加了 @Valid,且实体类字段有 @NotBlank 等注解。

Q2:自定义业务异常怎么处理?

A:可以定义自己的异常类,比如:

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

然后在 GlobalExceptionHandler 中加一个 @ExceptionHandler(BusinessException.class)

Q3:需要对每个模块返回不同的错误码吗?

A:建议统一用 HTTP 状态码语义(4xx=客户端错,5xx=服务端错),业务错误码可通过 msg 或额外字段传递,避免过度设计。


五、总结

一套好的异常处理机制,应该做到:

  • ✅ 对用户友好:错误信息清晰可操作
  • ✅ 对开发者友好:日志完整,便于定位
  • ✅ 对代码友好:无需到处写 try-catch

记住:用户不关心你用了什么框架,只关心“为什么不能用”。

把异常处理做好,你的接口才算真正“上线可用”。


我是“不想打工的码农”,一个坚持写能跑通、能上线代码的普通开发者。
如果你觉得有用,欢迎点赞、收藏、关注!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐