美文网首页实践实战
Spring Boot 响应式 WebFlux(三、全局异常处理

Spring Boot 响应式 WebFlux(三、全局异常处理

作者: 梅西爱骑车 | 来源:发表于2020-08-21 00:26 被阅读0次

    在上一篇 [global response]即全局统一返回中,我们已经定义了使用 CommonResult 全局统一返回,并且看到了成功返回的示例与代码。这一小节,我们主要是来全局异常处理,最终能也是通过 CommonResult 返回。

    3.1 ServiceExceptionEnum

    创建 [ServiceExceptionEnum]枚举类,枚举项目中的错误码。代码如下

    package com.erbadagang.springboot.springwebflux.globalresponse.constants;
    
    /**
     * 业务异常枚举
     */
    public enum ServiceExceptionEnum {
    
        // ========== 系统级别 ==========
        SUCCESS(0, "成功"),
        SYS_ERROR(2001001000, "服务端发生异常"),
        MISSING_REQUEST_PARAM_ERROR(2001001001, "参数缺失"),
    
        // ========== 用户模块 ==========
        USER_NOT_FOUND(1001002000, "用户不存在"),
    
        // ========== 订单模块 ==========
    
        // ========== 商品模块 ==========
        ;
    
        /**
         * 错误码
         */
        private final int code;
        /**
         * 错误提示
         */
        private final String message;
    
        ServiceExceptionEnum(int code, String message) {
            this.code = code;
            this.message = message;
        }
    
        public int getCode() {
            return code;
        }
    
        public String getMessage() {
            return message;
        }
    
    }
    

    3.2 ServiceException

    我们在一起讨论下 Service 逻辑异常的时候,如何进行返回。这里的逻辑异常,我们指的是,例如说用户名已经存在,商品库存不足等。一般来说,常用的方案选择,有两种:

    • 封装统一的业务异常类 ServiceException ,里面有错误码和错误提示,然后进行 throws 抛出。
    • 封装通用的返回类 CommonResult ,里面有错误码和错误提示,然后进行 return 返回。

    一开始,我们选择了 CommonResult ,结果发现如下情况:

    • 因为 Spring @Transactional 声明式事务,是基于异常进行回滚的,如果使用 CommonResult 返回,则事务回滚会非常麻烦。
    • 当调用别的方法时,如果别人返回的是 CommonResult 对象,还需要不断的进行判断,写起来挺麻烦的。

    所以,后来我们采用了抛出业务异常 ServiceException 的方式。

    创建 [ServiceException]异常类,继承 RuntimeException 异常类,用于定义业务异常。代码如下:

    package com.erbadagang.springboot.springwebflux.globalresponse.core.exception;
    
    import com.erbadagang.springboot.springwebflux.globalresponse.constants.ServiceExceptionEnum;
    
    /**
     * 服务异常
     *
     * 参考 https://www.kancloud.cn/onebase/ob/484204 文章
     *
     * 一共 10 位,分成四段
     *
     * 第一段,1 位,类型
     *      1 - 业务级别异常
     *      2 - 系统级别异常
     * 第二段,3 位,系统类型
     *      001 - 用户系统
     *      002 - 商品系统
     *      003 - 订单系统
     *      004 - 支付系统
     *      005 - 优惠劵系统
     *      ... - ...
     * 第三段,3 位,模块
     *      不限制规则。
     *      一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子:
     *          001 - OAuth2 模块
     *          002 - User 模块
     *          003 - MobileCode 模块
     * 第四段,3 位,错误码
     *       不限制规则。
     *       一般建议,每个模块自增。
     */
    public final class ServiceException extends RuntimeException {
    
        /**
         * 错误码
         */
        private final Integer code;
    
        public ServiceException(ServiceExceptionEnum serviceExceptionEnum) {
            // 使用父类的 message 字段
            super(serviceExceptionEnum.getMessage());
            // 设置错误码
            this.code = serviceExceptionEnum.getCode();
        }
    
        public Integer getCode() {
            return code;
        }
    
    }
    

    提供传入 serviceExceptionEnum 参数的构造方法。具体的处理,看下代码和注释。

    3.3 GlobalExceptionHandler

    创建 [GlobalExceptionHandler]类,全局统一返回的处理器。代码如下:

    package com.erbadagang.springboot.springwebflux.globalresponse.core.web;
    
    import com.erbadagang.springboot.springwebflux.globalresponse.constants.ServiceExceptionEnum;
    import com.erbadagang.springboot.springwebflux.globalresponse.core.exception.ServiceException;
    import com.erbadagang.springboot.springwebflux.globalresponse.core.vo.CommonResult;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.server.ServerWebInputException;
    
    @ControllerAdvice(basePackages = "com.erbadagang.springboot.springwebflux.globalresponse.controller")
    public class GlobalExceptionHandler {
    
        private Logger logger = LoggerFactory.getLogger(getClass());
    
        /**
         * 处理 ServiceException 异常
         */
        @ResponseBody
        @ExceptionHandler(value = ServiceException.class)
        public CommonResult serviceExceptionHandler(ServiceException ex) {
            logger.debug("[serviceExceptionHandler]", ex);
            // 包装 CommonResult 结果
            return CommonResult.error(ex.getCode(), ex.getMessage());
        }
    
        /**
         * 处理 ServerWebInputException 异常
         *
         * WebFlux 参数不正确
         */
        @ResponseBody
        @ExceptionHandler(value = ServerWebInputException.class)
        public CommonResult serverWebInputExceptionHandler(ServerWebInputException ex) {
            logger.debug("[ServerWebInputExceptionHandler]", ex);
            // 包装 CommonResult 结果
            return CommonResult.error(ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getCode(),
                    ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getMessage());
        }
    
        /**
         * 处理其它 Exception 异常
         */
        @ResponseBody
        @ExceptionHandler(value = Exception.class)
        public CommonResult exceptionHandler(Exception e) {
            // 记录异常日志
            logger.error("[exceptionHandler]", e);
            // 返回 ERROR CommonResult
            return CommonResult.error(ServiceExceptionEnum.SYS_ERROR.getCode(),
                    ServiceExceptionEnum.SYS_ERROR.getMessage());
        }
    
    }
    
    • 在 WebFlux 中,可以使用通过实现 ResponseBodyAdvice 接口,并添加 @ControllerAdvice 接口,拦截 Controller 的返回结果。注意,我们这里 @ControllerAdvice 注解,设置了 basePackages 属性,只拦截"com.erbadagang.springboot.springwebflux.globalresponse.controller" 包,也就是我们定义的 Controller 。为什么呢?因为在项目中,我们可能会引入 Swagger 等库,也使用 Controller 提供 API 接口,那么我们显然不应该让 GlobalResponseBodyHandler 去拦截这些接口,毕竟它们并不需要我们去替它们做全局统一的返回
    • 我们定义了三个方法,通过添加 @ExceptionHandler 注解,定义每个方法对应处理的异常。并且,也添加了 @ResponseBody 注解,标记直接使用返回结果作为 API 的响应。
    • #serviceExceptionHandler(...) 方法,拦截处理 ServiceException 业务异常,直接使用该异常的 code + message 属性,构建出 CommonResult 对象返回。
    • #serverWebInputExceptionHandler(...) 方法,拦截处理 ServerWebInputException 请求参数异常,构建出错误码为 ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR 的 CommonResult 对象返回。
    • #exceptionHandler(...) 方法,拦截处理 Exception 异常,构建出错误码为 ServiceExceptionEnum.SYS_ERROR 的 CommonResult 对象返回。这是一个兜底的异常处理,避免有一些其它异常,我们没有在 GlobalExceptionHandler 中,提供自定义的处理方式。

    注意,在 #exceptionHandler(...) 方法中,我们还多使用 logger 打印了错误日志,方便我们接入 ELK 等日志服务,发起告警,通知我们去排查解决。如果胖友的系统里暂时没有日志服务,可以记录错误日志到数据库中,也是不错的选择。而其它两个方法,因为是更偏业务的,相对正常的异常,所以无需记录错误日志。

    3.4 UserController

    在 [UserController]类中,我们添加两个 API 接口,抛出异常,方便我们测试全局异常处理的效果。代码如下:

    // UserController.java
    
    /**
     * 测试抛出 NullPointerException 异常
     */
    @GetMapping("/exception-01")
    public UserVO exception01() {
        throw new NullPointerException("没有粗面鱼丸");
    }
    
    /**
     * 测试抛出 ServiceException 异常
     */
    @GetMapping("/exception-02")
    public UserVO exception02() {
        throw new ServiceException(ServiceExceptionEnum.USER_NOT_FOUND);
    }
    

    #exception01()方法,抛出NullPointerException异常。这样,异常会被 GlobalExceptionHandler#exceptionHandler(...)方法来拦截,包装成CommonResult类型返回。请求结果如下:

    {
        "code": 2001001000,
        "message": "服务端发生异常",
        "data": null
    }
    

    #exception02()方法,抛出ServiceException异常。这样,异常会被 GlobalExceptionHandler#serviceExceptionHandler(...)方法来拦截,包装成 CommonResult 类型返回。请求结果如下:

    {
        "code": 1001002000,
        "message": "用户不存在",
        "data": null
    }
    

    3.5 简单小结

    采用 ControllerAdvice + @ExceptionHandler 注解的方式,可以很方便的实现 WebFlux 的全局异常处理。不过这种方案存在一个弊端,不支持 WebFlux 的基于函数式编程方式。不过考虑到,绝大多数情况下,我们并不会采用基于函数式编程方式,所以这种方案还是没问题的。

    底线


    本文源代码使用 Apache License 2.0开源许可协议,这里是本文源码Gitee地址,可通过命令git clone+地址下载代码到本地,也可直接点击链接通过浏览器方式查看源代码。

    相关文章

      网友评论

        本文标题:Spring Boot 响应式 WebFlux(三、全局异常处理

        本文链接:https://www.haomeiwen.com/subject/pxrcdktx.html