全局异常处理器
以 根据id查找用户 举例,返回结果可能是存在/不存在,为了前后端数据传递方便,之前规定了统一返回体Result。来看看运行结果:


很明显,当id不存在的时候,后端不会返回由code、data、message组成的响应体,其实这是由异常返回的默认结果。为了解决这个问题,不优雅的方法是在每个抛异常的地方包裹 try-catch 语句里面写 return Result.error(),代码量就冗余了。
这个时候就需要在出现异常的时候进行集中管理、统一处理。
4.1 定义全局异常处理器
位置:
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com.itstudent.springbootdemo/
│ │ │ ├── handler/ # 处理器文件夹
│ │ │ │ └── GlobalExceptionHandler.java # 全局异常处理器
程序:
@Slf4j
@RestControllerAdvice // A
public class GlobalExceptionHandler {
@ExceptionHandler // B
public Result<?> exceptionHandler(Exception e) {
log.error("异常", e);
return Result.error(ResultCodeEnum.SYSTEM_ERROR, e.getMessage());
}
}
A:@RestControllerAdvice = @ControllerAdvice + @ResponseBody,@ControllerAdvice可拦截所有 @Controller或@RestController 抛出的异常,@ResponseBody自动将方法返回值转为 JSON
B:@ExceptionHandler 标记处理异常的方法,可以传入异常的字节码文件(Exception.class)来指定它会处理什么异常,不配置会根据参数依次寻找。
注意:目前方法中配置了接收Exception的异常,所有Exception和它的子异常都会被该方法拦截和处理,这也太过于统一了,程序还希望对于不同的业务逻辑产生的异常,统一返回体的message字段能够进行不同的提示,比如:用户不存在、请求次数过频繁等等。
4.2 自定义异常
本项目使用 BaseException 继承了RuntimeException,再定义了许多业务异常继承BaseException。全局异常处理器只要拦截到BaseException说明拦截了业务产生的异常(这些业务异常一般是我们自己抛出的)。对于这些进行统一的处理。
位置:
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com.itstudent.springbootdemo/
│ │ │ ├── exception/ # 自定义异常类
│ │ │ │ ├── BaseException.java # 本项目业务异常基类(所有自定义异常的父类)
│ │ │ │ └── SelectNotFoundException.java # 具体异常:查询结果为空
BaseException:
public class BaseException extends RuntimeException {
public BaseException() {
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
}
SelectNotFoundException:
public class SelectNotFoundException extends BaseException {
public SelectNotFoundException() {
}
public SelectNotFoundException(String message) {
super(message);
}
public SelectNotFoundException(Throwable cause) {
super(cause);
}
public SelectNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
在抛出这些异常的时候,可以传入字符串作为 message ,捕获异常时可以通过 e.getMessage() 拿到。也可以传入异常触发的原因(传入原始异常构造异常链)保留完整的异常上下文信息。例如数据库操作抛出 SQLException,业务层将其包装为 SelectNotFoundException 时,SQLException 就是 cause。
4.3 自定义Message常量
三个核心价值:
- 消除魔法字符串 在抛出异常时如果每次都直接硬编码,修改时容易遗漏。定义为常量后只需改一处。
- 语义统一 确保整个项目中同一种错误的提示消息完全一致,前端可以据此做统一的用户提示。
- 方便扩展 如果日后需要支持多语言,只需修改常量文件,将所有中文替换为国际化资源的 Key。
位置:
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com.itstudent.springbootdemo/
│ │ │ ├── constant # 常量
│ │ │ │ └── MessageConstant.java # Message常量
程序(暂时就写这几个,以后有新业务了再加):
public class MessageConstant {
public static final String ENTRY_NOT_FOUND = "不存在该记录";
public static final String ENTRY_ALREADY_EXISTS = "该记录已经存在";
public static final String CONTACT_THE_ADMINISTRATOR = "出现异常,请联系管理员";
}
4.4 优化全局异常处理器
通过自定义异常、自定义message常量,目前可以将全局异常处理器优化地更精确,面对不同的异常产生不同的处理逻辑,返回给前端也保证了是统一的响应体。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/*
拦截所有的业务异常
业务异常往往是设置message常量,因此可以直接返回前端
*/
@ExceptionHandler
public Result<?> exceptionHandler(BaseException e) {
log.error("业务异常", e);
return Result.error(ResultCodeEnum.SYSTEM_ERROR, e.getMessage());
}
/*
MyBatis 会将底层的 SQL 异常包装成
org.apache.ibatis.exceptions.PersistenceException
或 org.springframework.dao.DuplicateKeyException
攻击场景:攻击者可通过注册接口试探手机号是否已被注册(暴力枚举),实现用户信息探测
为了避免隐私泄露,直接采用模糊的“记录已存在”返回
*/
@ExceptionHandler
public Result<?> exceptionHandler(DuplicateKeyException e) {
log.error("重复键异常", e);
return Result.error(ResultCodeEnum.SYSTEM_ERROR, MessageConstant.ENTRY_ALREADY_EXISTS);
}
/*
MyBatis 包装前的异常,作为上一个异常的兜底
*/
@ExceptionHandler
public Result<?> exceptionHandler(SQLIntegrityConstraintViolationException e) {
log.error("SQL违反完整性约束异常", e);
return Result.error(ResultCodeEnum.SYSTEM_ERROR, MessageConstant.ENTRY_ALREADY_EXISTS);
}
/*
如果能拦到这一层,说明出现了非业务的、非SQL的异常,直接返回“请联系管理员”,避免隐私泄露
*/
@ExceptionHandler
public Result<?> exceptionHandler(Exception e) {
log.error("异常", e);
return Result.error(ResultCodeEnum.SYSTEM_ERROR, MessageConstant.CONTACT_THE_ADMINISTRATOR);
}
}
4.5 效果


测试了传入不存在的id和设置1/0,这个时候再产生异常返回的结果就很固定了'
异常处理流程:
1. GET /user/999 (用户不存在)
│
2. UserController.findById(999)
│ 调用 userService.findById(999)
│
3. UserServiceImpl.findById(999)
│ userMapper.findById(999) 返回 null
│ throw new SelectNotFoundException(MessageConstant.ENTRY_NOT_FOUND)
│
4. GlobalExceptionHandler.exceptionHandler(BaseException e)
│ log.error("业务异常", e)
│ return Result.error(ResultCodeEnum.SYSTEM_ERROR, "不存在该记录")
│
5. 前端收到响应: {"code":500, "message":"不存在该记录", "data":null, "timestamp":...}
posted on 2026-06-10 21:48 HarryTruman 阅读(3) 评论(0) 收藏 举报
浙公网安备 33010602011771号