MyBatis-Plus 统一异常处理(结合 Spring MVC @ControllerAdvice)

适用场景:Web 项目中,捕获 MyBatis-Plus、数据库、业务等异常,统一返回格式化错误信息。
目标:避免异常堆栈暴露、提升系统健壮性、前后端交互标准化。


一、核心概念

1.1 为什么要统一异常处理?

  • 防止敏感信息(如 SQL 错误)暴露给前端。
  • 统一返回格式(如 {"code": 500, "msg": "服务器错误", "data": null})。
  • 提升用户体验和系统可维护性。

1.2 关键技术栈

技术 作用
@ControllerAdvice 全局异常处理器切面,拦截所有 Controller 异常
@ExceptionHandler 指定处理的异常类型
MyBatisException / PersistenceException MyBatis-Plus 抛出的持久层异常基类
自定义异常(如 ServiceException 业务逻辑异常封装

1.3 常见 MyBatis-Plus 相关异常

异常类 触发场景
org.apache.ibatis.exceptions.PersistenceException 所有 MyBatis 操作异常的顶层包装
org.springframework.dao.DuplicateKeyException 唯一索引冲突(如插入重复主键)
org.springframework.dao.DataIntegrityViolationException 数据完整性违反(外键、非空等)
org.apache.ibatis.binding.BindingException 参数绑定失败
com.baomidou.mybatisplus.core.exceptions.MybatisPlusException MP 自定义异常(较少见)

二、详细操作步骤(手把手教学)

步骤 1:添加依赖(Maven)

确保已包含 Spring Boot Web 和 MyBatis-Plus:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version>
    </dependency>

    <!-- Lombok(可选) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

步骤 2:定义统一返回结果类 Result<T>

import lombok.Data;

@Data
public class Result<T> {
    private int code;
    private String msg;
    private T data;

    public static <T> Result<T> success(T data) {
        Result<T> r = new Result<>();
        r.code = 200;
        r.msg = "操作成功";
        r.data = data;
        return r;
    }

    public static <T> Result<T> fail(int code, String msg) {
        Result<T> r = new Result<>();
        r.code = code;
        r.msg = msg;
        return r;
    }

    // 通用错误
    public static <T> Result<T> error(String msg) {
        return fail(500, msg);
    }
}

步骤 3:定义自定义业务异常(推荐)

import lombok.Getter;

@Getter
public class ServiceException extends RuntimeException {
    private final int code;

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

    public ServiceException(String message) {
        this(500, message);
    }
}

步骤 4:创建全局异常处理器 GlobalExceptionHandler

import org.apache.ibatis.exceptions.PersistenceException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice // 等价于 @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {

    /**
     * 处理 MyBatis 持久层异常(包括 MP)
     */
    @ExceptionHandler(PersistenceException.class)
    public Result<Void> handlePersistenceException(PersistenceException e) {
        // 日志记录(生产环境建议使用日志框架)
        e.printStackTrace();

        // 判断具体异常类型
        if (e.getCause() instanceof DuplicateKeyException) {
            return Result.fail(400, "数据重复,请检查唯一键(如手机号、用户名)");
        } else if (e.getCause() instanceof DataIntegrityViolationException) {
            return Result.fail(400, "数据完整性违反(如字段为空、外键不存在)");
        } else {
            return Result.error("数据库操作异常:" + e.getMessage());
        }
    }

    /**
     * 处理自定义业务异常
     */
    @ExceptionHandler(ServiceException.class)
    public Result<Void> handleServiceException(ServiceException e) {
        return Result.fail(e.getCode(), e.getMessage());
    }

    /**
     * 处理空指针异常(可选)
     */
    @ExceptionHandler(NullPointerException.class)
    public Result<Void> handleNullPointerException(NullPointerException e) {
        e.printStackTrace();
        return Result.error("系统内部错误:空指针异常");
    }

    /**
     * 处理所有未被捕获的异常(兜底)
     */
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        e.printStackTrace(); // 建议替换为 log.error(...)
        return Result.error("服务器内部错误,请联系管理员");
    }
}

步骤 5:创建实体类和 Mapper(测试用)

// User.java
@Data
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name; // 假设 name 有唯一索引
    private String email;
}
// UserMapper.java
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

步骤 6:Service 层模拟异常

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public void createUser(User user) {
        // 模拟插入重复 name 触发 DuplicateKeyException
        try {
            userMapper.insert(user);
        } catch (Exception e) {
            // 可以包装为 ServiceException 抛出,由全局处理器捕获
            throw new ServiceException("创建用户失败:" + e.getMessage());
        }
    }
}

步骤 7:Controller 层调用

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

    @Autowired
    private UserService userService;

    @PostMapping
    public Result<Void> create(@RequestBody User user) {
        userService.createUser(user);
        return Result.success(null);
    }
}

步骤 8:测试验证

  1. 启动 Spring Boot 项目。
  2. 使用 Postman 或 curl 发送请求,插入两条 name 相同的用户:
    POST http://localhost:8080/users
    {
      "name": "张三",
      "email": "zhangsan@example.com"
    }
    
  3. 第二次插入时,将收到统一格式的 JSON 响应:
    {
      "code": 400,
      "msg": "数据重复,请检查唯一键(如手机号、用户名)",
      "data": null
    }
    

三、常见错误与解决方案

错误现象 原因 解决方案
异常未被捕获,返回 HTML 错误页 未使用 @RestControllerAdvice 改用 @RestControllerAdvice 或手动加 @ResponseBody
返回 500 但无 JSON 处理器未返回 Result 确保每个 @ExceptionHandler 方法返回 Result<T>
DuplicateKeyException 未被捕获 未在 handlePersistenceException 中判断 getCause() 使用 e.getCause() instanceof DuplicateKeyException
日志未打印 未添加日志输出 添加 e.printStackTrace() 或集成 SLF4J/Logback

四、注意事项

  1. @RestControllerAdvice vs @ControllerAdvice

    • 前者自动序列化为 JSON,适合 REST API。
    • 后者需配合 @ResponseBody 使用。
  2. 异常捕获顺序

    • 子类异常应写在前面,避免被父类(如 Exception)提前捕获。
  3. 生产环境禁用堆栈打印

    • 使用 log.error("msg", e); 而非 e.printStackTrace()
  4. 不要吞掉异常

    • 即使处理了,也应记录日志。

五、使用技巧

5.1 按包扫描异常处理器

@RestControllerAdvice(basePackages = "com.example.controller")
public class GlobalExceptionHandler { ... }

5.2 使用枚举定义错误码

public enum ErrorCode {
    DUPLICATE_KEY(400, "数据重复"),
    DB_ERROR(500, "数据库错误");

    private final int code;
    private final String msg;

    // getter...
}

5.3 记录请求上下文日志

结合 @Before 切面记录请求参数,便于排查。


六、最佳实践

实践 说明
✅ 使用 Result<T> 统一返回格式 前后端约定好结构
✅ 自定义 ServiceException 业务异常清晰可控
✅ 全局捕获 PersistenceException 拦截所有数据库异常
✅ 区分异常级别 如 400(客户端错误)、500(服务端错误)
✅ 生产环境记录日志 使用 logbacklog4j2
✅ 不暴露敏感信息 如数据库表名、字段名、SQL 语句

七、性能优化建议

  1. 避免在异常处理中执行耗时操作:如远程调用、大量计算。
  2. 使用异步日志:如 Logback 配置 AsyncAppender
  3. 异常应尽量少发生:通过前端校验、参数验证减少异常触发。
  4. 缓存高频异常场景:如频繁重复提交,可结合 Redis 限流。

八、总结

要点 说明
核心组件 @ControllerAdvice + @ExceptionHandler
关键异常 PersistenceException, DuplicateKeyException
返回格式 推荐使用 Result<T> 统一封装
推荐做法 自定义异常 + 全局处理器 + 日志记录

结论:通过 @ControllerAdvice 实现 MyBatis-Plus 统一异常处理,是构建健壮 Spring Boot 应用的必备技能。既能保护系统安全,又能提升用户体验。