• SpringBoot异常处理


    演示代码地址:

      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**

    SpringBoot默认的异常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两种方式归根结底是一样的:

     

  • 相关阅读:
    华为服务器内存插法
    关于公司内部域名称是否要和外部真实域名称对应的问题
    配置Office 365单点登录摘要
    配置Office 365单点登录过程中的一些注意事项
    AADC安装指南
    使用非Web方式从CA申请证书
    爬取某招聘网站的信息
    通过PowerShell启用AADC的密码同步功能
    Azure Active Directory Connect密码同步问题
    Python脚本配合Linux计划任务工作
  • 原文地址:https://www.cnblogs.com/zhangzhixi/p/16114857.html
Copyright © 2020-2023  润新知