HarryTruman

 

全局异常处理器

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

a4

a5

很明显,当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常量

三个核心价值:

  1. 消除魔法字符串 在抛出异常时如果每次都直接硬编码,修改时容易遗漏。定义为常量后只需改一处。
  2. 语义统一 确保整个项目中同一种错误的提示消息完全一致,前端可以据此做统一的用户提示。
  3. 方便扩展 如果日后需要支持多语言,只需修改常量文件,将所有中文替换为国际化资源的 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 效果

a6

a7

测试了传入不存在的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)    收藏  举报

导航