美文网首页
统一异常处理介绍及实战——支持自定义错误信息

统一异常处理介绍及实战——支持自定义错误信息

作者: sprainkle | 来源:发表于2020-06-20 17:14 被阅读0次

    ps: 因为本文的内容比较简单,所以都是以测试用例来做实例,但逻辑与在 web 项目大同小异,具体代码详见 这里
    ps: 本文作为 统一异常处理介绍及实战 这篇文章的扩展,若还没阅读过,还请先移步过去了解一下,它会为你打开一扇神奇的门,看到不一样的统一异常处理方式。

    背景

    在前文 统一异常处理介绍及实战 中介绍如何优雅地抛出业务异常。举个例子,如果希望在创建订单的时候,检测到商品不存在,抛 “创建订单失败” 的异常,可以这么写:

    @Test
    public void assertNotNull() {
        Goods goods = getGoods("1001");
        ResponseEnum.ORDER_CREATION_FAILED.assertNotNull(goods);
    
        // others
    }
    
    @Getter
    @AllArgsConstructor
    public enum ResponseEnum implements BusinessExceptionAssert {
    
        ORDER_CREATION_FAILED(7001, "订单创建失败,请稍后重试");
    
        private int code;
        private String message;
    }
    
    public Goods getGoods(String id) {
        return null;
    }
    

    上面的代码最后打印如下日志:


    order creation failed

    但有没有发现,控制台打印的内容,对分析问题的帮助有限,因为导致订单失败的原因有很多,就比如上面举例的 商品不存在,也有可能是 计算订单金额时出现异常 ,亦或是 调用其他服务时发现服务不可用 等等。

    其实在开发中,这样的场景是很常见,可以简单归纳为:一个大类异常可以再细分出各种更具体的异常,并且用户并不关心具体异常,只关心此次操作成功与否

    虽说用户不关心真正的错误原因,但对于开发人员来说,还是有必要知道真正的问题出在哪里,不然运维看到这些日志然后,说:那啥,用户创建订单失败,你看是不是有bug。然后我瞬间就——

    我瞬间就

    如果可以在打印日志的时候顺便也把具体错误信息也打印出来,那定位问题就简单多了。比如:商品服务突然宕机不可用了,运维看到了直接紧急恢复下服务,用户就又能正常下单了。

    自定义错误信息

    具体的错误信息,肯定不是程序自己凭空构造出来的,而是需要开发人员在开发过程中,以某种形式去教程序怎么构造,构造出来后,跟最终返回给用户端的错误信息一起打印出来。

    所以打印出来的错误日志,必须包含2个错误信息,一个是给用户看的错误信息(订单创建失败),另一个是给运维/开发人员看的错误信息(获取商品详情失败)。

    分析到这里,接下来就是怎么实现的问题了。

    assert*WithMsg

    这里选择使用增加 assert*WithMsg 方法的方式,即每一种类型的断言方法,都增加2套 assert*WithMsg 方法,为什么是2套,下文会给出答案。

    这里以 断言非空 为例子,其他的都一样,代码如下:

    /**
     * <p>断言对象<code>obj</code>非空。如果对象<code>obj</code>为空,则抛出异常
     *
     * @param obj 待判断对象
     * @param errMsg 自定义的错误信息
     */
    default void assertNotNullWithMsg(Object obj, String errMsg) {
        if (obj == null) {
            WrapMessageException e = new WrapMessageException(errMsg);
            throw newException(e);
        }
    }
    
    /**
     * <p>断言对象<code>obj</code>非空。如果对象<code>obj</code>为空,则抛出异常
     * <p>异常信息<code>message</code>支持传递参数方式,避免在判断之前进行字符串拼接操作
     *
     * @param obj 待判断对象
     * @param errMsg 自定义的错误信息. 支持 {index} 形式的占位符, 比如: errMsg-用户[{0}]不存在, args-1001, 最后打印-用户[1001]不存在
     * @param args message占位符对应的参数列表
     */
    default void assertNotNullWithMsg(Object obj, String errMsg, Object... args) {
        if (obj == null) {
            if (ArrayUtil.isNotEmpty(args)) {
                errMsg = MessageFormat.format(errMsg, args);
            }
    
            WrapMessageException e = new WrapMessageException(errMsg);
            throw newException(e, args);
        }
    }
    

    其中涉及到一个异常类 WrapMessageException,其实就是一个继承了 RuntimeException 的普通异常类,这里可以先理解为就是 RuntimeException,至于为什么要定义这么一个异常,这里先卖个关子。

    当传入自定义错误信息 errMsg 后,使用该错误信息创建一个 WrapMessageException,然后把它传给 newException(Throwable t)。这么做有什么好处呢? 我们再来写个测试用例,看一下最终的打印效果。

    @Test
    public void assertNotNull2() {
        String goodsId = "1001";
        Goods goods = getGoods(goodsId);
        ResponseEnum.ORDER_CREATION_FAILED.assertNotNullWithMsg(goods, "商品[{0}]不存在", goodsId);
    
        // others
    }
    

    打印结果如下:


    商品不存在

    有没有看到那个 Caused by(相信各位大佬都是知道怎么看异常信息的),把我们刚刚传进去的具体错误信息也打印出来了。再从整体上看,从上到下分别是:订单创建失败,请稍后重试Caused by: 商品[1001]不存在,是不是很流畅,一下子就能定位具体异常。

    newExceptionWithMsg

    因为有很多断言方法,每个方法都需要写大致相同的逻辑,所以这里再封装两个 newExceptionWithMsg 默认方法,如下:

    /**
     * 创建异常.
     * 先使用 {@code errMsg} 创建一个 {@link WrapMessageException} 异常,
     * 再以入参的形式传给 {{{@link #newException(Throwable, Object...)}}}, 作为最后创建的异常的 cause 属性.
     *
     * @param errMsg 自定义的错误信息
     * @param args
     * @return
     */
    default BaseException newExceptionWithMsg(String errMsg, Object... args) {
        if (args != null && args.length > 0) {
            errMsg = MessageFormat.format(errMsg, args);
        }
    
        WrapMessageException e = new WrapMessageException(errMsg);
        throw newException(e, args);
    }
    
    /**
     * 创建异常.
     * 先使用 {@code errMsg} 和 {@code t} 创建一个 {@link WrapMessageException} 异常,
     * 再以入参的形式传给 {{{@link #newException(Throwable, Object...)}}}, 作为最后创建的异常的 cause 属性.
     *
     * @param errMsg 自定义的错误信息
     * @param args
     * @return
     */
    default BaseException newExceptionWithMsg(String errMsg, Throwable t, Object... args) {
        if (ArrayUtil.isNotEmpty(args)) {
            errMsg = MessageFormat.format(errMsg, args);
        }
    
        WrapMessageException e = new WrapMessageException(errMsg, t);
        throw newException(e, args);
    }
    

    最后的 assert*WithMsg 方法为:

    default void assertNotNullWithMsg(Object obj, String errMsg) {
        if (obj == null) {
            throw newExceptionWithMsg(errMsg);
        }
    }
    
    default void assertNotNullWithMsg(Object obj, String errMsg, Object... args) {
        if (obj == null) {
            throw newExceptionWithMsg(errMsg, args);
        }
    }
    

    复杂的错误信息

    考虑到自定义的错误信息有可能会比较复杂,所以又定义一套 assert*WithMsg 方法来处理这种场景。定义如下:

    default void assertNotNullWithMsg(Object obj, Supplier<String> errMsg) {
        if (obj == null) {
            throw newExceptionWithMsg(errMsg.get());
        }
    }
    
    default void assertNotNullWithMsg(Object obj, Supplier<String> errMsg, Object... args) {
        if (obj == null) {
            throw newExceptionWithMsg(errMsg.get(), args);
        }
    }
    

    唯一不同的是,errMsg 的类型变了,变成 Supplier<String>,该接口为 java8 提供的,在使用 java8lambda 表达式 新特性时经常会用到,如果对这一特性不是特别了解,可先略过,只需知道一点就是:可以通过 errMsg.get() 得到想要的自定义异常。

    这就是另一套 assert*WithMsg 方法了,哈哈。。。

    为什么定义 WrapMessageException 异常类

    首先来看下具体源码:

    /**
     * 只包装了 错误信息 的 {@link RuntimeException}.
     * 用于 {@link com.sprainkle.spring.cloud.advance.common.core.exception.assertion.Assert} 中用于包装自定义异常信息
     */
    public class WrapMessageException extends RuntimeException {
        public WrapMessageException(String message) {
            super(message);
        }
    
        public WrapMessageException(String message, Throwable cause) {
            super(message, cause);
        }
    }
    

    可以看到,源码很简单,就是继承了 RuntimeException,并且只提供2个构造方法。至于为什么,这个跟 WrapMessageException 这个类的职能有关。因为该类只用来包装 错误信息,也可以理解为 错误信息 的载体,所以不定义无参构造方法。另外,有时已经有一个具体异常,那么当然也需要支持传进来,所以又加多一个构造方法。

    至于为什么定义这样一个异常类,考虑到以后可能会对捕获到的异常进一步分析,如果检测到存在 WrapMessageException,则执行某种逻辑,所以必须定义一个具体异常类,且不能继承 BaseException,因为没有 code 属性。如果直接使用 RuntimeException 则很难解决上面的需求。

    总结

    当需要自定义详细错误信息时,可以使用如下代码:

    ResponseEnum.ORDER_CREATION_FAILED.assertNotNullWithMsg(goods, "商品[{0}]不存在", goodsId);
    

    如果错误信息比较复杂,需要依赖其他变量来构造,可以使用如下代码:

    int a = 1;
    String b = "2";
    Xxx c = ...;
    ResponseEnum.ORDER_CREATION_FAILED.assertNotNullWithMsg(goods, () -> "XXX" + a + b + c, goodsId);
    
    // 不要这么用,因为无论断言是否成功,都会拼接错误信息
    ResponseEnum.ORDER_CREATION_FAILED.assertNotNullWithMsg(goods, "XXX" + a + b + c, goodsId);
    

    谢谢观看,完!!!

    推荐阅读

    Spring Cloud 进阶玩法
    Spring Cloud Stream 进阶配置——使用延迟队列实现“定时关闭超时未支付订单”

    相关文章

      网友评论

          本文标题:统一异常处理介绍及实战——支持自定义错误信息

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