统一异常处理介绍及实战——支持自定义错误信息
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
提供的,在使用 java8
的 lambda 表达式
新特性时经常会用到,如果对这一特性不是特别了解,可先略过,只需知道一点就是:可以通过 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 进阶配置——使用延迟队列实现“定时关闭超时未支付订单”