演示代码地址:
GitHub:https://github.com/zhangzhixi0305/exception-handling
码云:https://gitee.com/zhang-zhixi/exception-handling.git
参考链接:
https://www.yuque.com/books/share/2b434c74-ed3a-470e-b148-b4c94ba14535/db60lb
一、SpringBoot默认异常处理机制
在SpringBoot中,无论是请求不存在的路径、@Valid校验,还是业务代码(Controller、Service、Dao)抛出异常,SpringBoot对错误的默认处理机制是:
BasicErrorController会判断当前请求来自哪里,如果来自浏览器则响应错误页面,如果来自APP则响应JSON。
那么,SpringBoot是如何判断一个请求到底来自浏览器还是APP的呢?其实,主要是看HTTP的一个请求头:Accept。
SpringBoot默认的异常处理机制有什么不好呢?主要还是两点:
- 样式或数据格式不统一
- 对外暴露的信息不可控
以JSON格式为例,通常我们希望不论接口请求是否正常,都返回以下格式:
{ "data": {} "success": true, "massage": "" }
如果请求失败,希望把错误信息转化为指定内容(比如“系统正在繁忙”)放在message中返回,给前端/客户端一个友好提示。
这样一来,不论请求成功还是失败,响应格式都是统一的,对外暴露的信息也可控。
自定义异常处理可以大致分为两类:
- 自定义错误页面
- 自定义异常JSON
二、自定义错误页面
在resources/error下存放404.html和500.html,当本次请求状态码为404或500时,SpringBoot就会读取我们自定义的html返回,否则返回默认的错误页面。
现在一般都是前后端分离,所以关于自定义错误页面就略过了。
三、**自定义异常JSON**
{ "timestamp": "2021-01-31T01:36:12.187+00:00", "status": 500, "error": "Internal Server Error", "message": "", "path": "/insertUser" }
而我们希望响应格式是这样的:
{ "data": {} "success": true, "massage": "" }
一般有两种方式,并且通常会组合使用:
- 在代码中使用工具类封装(ApiResultTO/Result)
- 用全局异常处理兜底
为了方便模拟异常情况,下面案例中我们会直接抛出自定义异常,然后考虑如何处理它。
在此之前,我们先准备通用枚举类和自定义的业务异常:
1、先定义通用枚举类和自定义的业务异常
ExceptionCodeEnum.java
import lombok.Getter; import java.util.HashMap; import java.util.Map; import java.util.Optional; /** * @ClassName ExceptionCodeEnum * @Author zhangzhixi * @Description 通用错误枚举(不同类型的错误也可以拆成不同的Enum细分) * @Date 2022-4-7 18:19 * @Version 1.0 */ @Getter public enum ExceptionCodeEnum { /** * 通用结果 */ ERROR(-1, "网络错误"), SUCCESS(200, "成功"), /** * 用户登录 */ NEED_LOGIN(900, "用户未登录"), /** * 参数校验 */ ERROR_PARAM(10000, "参数错误"), EMPTY_PARAM(10001, "参数为空"), ERROR_PARAM_LENGTH(10002, "参数长度错误"); private final Integer code; private final String desc; ExceptionCodeEnum(Integer code, String desc) { this.code = code; this.desc = desc; } private static final Map<Integer, ExceptionCodeEnum> ENUM_CACHE = new HashMap<>(); static { for (ExceptionCodeEnum exceptionCodeEnum : ExceptionCodeEnum.values()) { ENUM_CACHE.put(exceptionCodeEnum.code, exceptionCodeEnum); } } public static String getDesc(Integer code) { return Optional.ofNullable(ENUM_CACHE.get(code)) .map(ExceptionCodeEnum::getDesc) .orElseThrow(() -> new IllegalArgumentException("invalid exception code!")); } }
BizException.java
import lombok.Getter; /** * @ClassName BizException * @Author zhangzhixi * @Description 业务异常 biz是business的缩写 * @Date 2022-4-7 18:21 * @Version 1.0 */ @Getter public class BizException extends RuntimeException { private static final long serialVersionUID = -3229475403587709519L; private ExceptionCodeEnum error; /** * 构造器,有时我们需要将第三方异常转为自定义异常抛出,但又不想丢失原来的异常信息,此时可以传入cause * * @param error * @param cause */ public BizException(ExceptionCodeEnum error, Throwable cause) { super(cause); this.error = error; } /** * 构造器,只传入错误枚举 * * @param error */ public BizException(ExceptionCodeEnum error) { this.error = error; } }
下面演示两种处理异常的方式。
2、第一种:Result手动封装
先封装一个Result,用来统一返回格式:
/** * @ClassName Result * @Author zhangzhixi * @Description 一般返回实体 * @Date 2022-4-7 18:24 * @Version 1.0 */ @Data @NoArgsConstructor public class Result<T> implements Serializable { private static final long serialVersionUID = -687690141206758604L; private Integer code; private String message; private T data; private Result(Integer code, String message, T data) { this.code = code; this.message = message; this.data = data; } private Result(Integer code, String message) { this.code = code; this.message = message; this.data = null; } /** * 带数据成功返回 * * @param data * @param <T> * @return */ public static <T> Result<T> success(T data) { return new Result<>(ExceptionCodeEnum.SUCCESS.getCode(), ExceptionCodeEnum.SUCCESS.getDesc(), data); } /** * 不带数据成功返回 * * @return */ public static <T> Result<T> success() { return success(null); } /** * 通用错误返回,传入指定的错误枚举 * * @param exceptionCodeEnum * @return */ public static <T> Result<T> error(ExceptionCodeEnum exceptionCodeEnum) { return new Result<>(exceptionCodeEnum.getCode(), exceptionCodeEnum.getDesc()); } /** * 通用错误返回,传入指定的错误枚举,但支持覆盖message * * @param exceptionCodeEnum * @param msg * @return */ public static <T> Result<T> error(ExceptionCodeEnum exceptionCodeEnum, String msg) { return new Result<>(exceptionCodeEnum.getCode(), msg); } /** * 通用错误返回,只传入message * * @param msg * @param <T> * @return */ public static <T> Result<T> error(String msg) { return new Result<>(ExceptionCodeEnum.ERROR.getCode(), msg); } }
使用自定义异常后,Controller层写法的区别
原本的Controller层插入用户的写法:
/** * 插入用户 * * @param userPojo 用户信息 * @return 是否成功 */ @PostMapping("insertUser") public boolean insertUser(@RequestBody User userPojo) { return userService.save(userPojo); }
现在的写法:
/** * 插入用户 * * @param userPojo 用户信息 * @return 是否成功 */ @PostMapping("insertUser") public Result<Boolean> insertUser(@RequestBody User userPojo) { if (userPojo == null) { // 只传入定义好的错误 return Result.error(ExceptionCodeEnum.EMPTY_PARAM); } if (userPojo.getUserName().contains("死")) { // 抛出自定义的错误信息 return Result.error(ExceptionCodeEnum.ERROR_PARAM, "用户名不能包含特殊字符"); } if (userPojo.getUserAge() < 18) { // 抛出自定义的错误信息 return Result.error("年龄不能小于18"); } if (!"男".equals(userPojo.getUserSex()) && !"女".equals(userPojo.getUserSex())) { // 抛出自定义的错误信息 可以自定义错误码 return Result.error("性别只能是男或女"); } return Result.success(userService.save(userPojo)); }
测试:
失败案例:
POST localhost/user/insertUser
请求体JSON:
{ "userName": "张三", "userAge": "23", "userSex": "男的" }
3、第二种:@RestControllerAdvice全局异常处理兜底
异常还有一种处理方式,就是利用Spring/SpringBoot提供的@RestControllerAdvice进行兜底处理,
/** * @ClassName GlobalExceptionHandler * @Author zhangzhixi * @Description 全局异常处理 * @Date 2022-4-8 9:49 * @Version 1.0 */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { /** * 业务异常 * * @param * @return */ @ExceptionHandler(BizException.class) public Result<ExceptionCodeEnum> handleBizException(BizException bizException) { log.error("业务异常:{}", bizException.getMessage(), bizException); return Result.error(bizException.getError()); } /** * 运行时异常 * * @param e * @return */ @ExceptionHandler(RuntimeException.class) public Result<ExceptionCodeEnum> handleRunTimeException(RuntimeException e) { log.error("运行时异常: {}", e.getMessage(), e); return Result.error(ExceptionCodeEnum.ERROR); } }
测试
总结
一般来说,全局异常处理只是一种兜底的异常处理策略,也就是说提倡自己处理异常。但现在其实很多人都喜欢直接在代码中抛异常,全部交给@RestControllerAdvice处理:
这个异常抛到@RestControllerAdvice后,其实还是被封装成Result返回了。
所以Result和@ResultControllerAdvice两种方式归根结底是一样的: