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:测试验证
- 启动 Spring Boot 项目。
- 使用 Postman 或 curl 发送请求,插入两条
name
相同的用户:POST http://localhost:8080/users { "name": "张三", "email": "zhangsan@example.com" }
- 第二次插入时,将收到统一格式的 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 |
四、注意事项
@RestControllerAdvice
vs@ControllerAdvice
- 前者自动序列化为 JSON,适合 REST API。
- 后者需配合
@ResponseBody
使用。
异常捕获顺序
- 子类异常应写在前面,避免被父类(如
Exception
)提前捕获。
- 子类异常应写在前面,避免被父类(如
生产环境禁用堆栈打印
- 使用
log.error("msg", e);
而非e.printStackTrace()
。
- 使用
不要吞掉异常
- 即使处理了,也应记录日志。
五、使用技巧
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(服务端错误) |
✅ 生产环境记录日志 | 使用 logback 或 log4j2 |
✅ 不暴露敏感信息 | 如数据库表名、字段名、SQL 语句 |
七、性能优化建议
- 避免在异常处理中执行耗时操作:如远程调用、大量计算。
- 使用异步日志:如 Logback 配置
AsyncAppender
。 - 异常应尽量少发生:通过前端校验、参数验证减少异常触发。
- 缓存高频异常场景:如频繁重复提交,可结合 Redis 限流。
八、总结
要点 | 说明 |
---|---|
核心组件 | @ControllerAdvice + @ExceptionHandler |
关键异常 | PersistenceException , DuplicateKeyException |
返回格式 | 推荐使用 Result<T> 统一封装 |
推荐做法 | 自定义异常 + 全局处理器 + 日志记录 |
✅ 结论:通过
@ControllerAdvice
实现 MyBatis-Plus 统一异常处理,是构建健壮 Spring Boot 应用的必备技能。既能保护系统安全,又能提升用户体验。