ps: 因為本文的內容比較簡單慎式,所以都是以測試用例來做實例伶氢,但邏輯與在 web 項目大同小異,具體代碼詳見 這里瘪吏。
ps: 本文作為 統(tǒng)一異常處理介紹及實戰(zhàn) 這篇文章的擴展癣防,若還沒閱讀過,還請先移步過去了解一下掌眠,它會為你打開一扇神奇的門蕾盯,看到不一樣的統(tǒng)一異常處理方式。
背景
在前文 統(tǒng)一異常處理介紹及實戰(zhàn) 中介紹如何優(yōu)雅地拋出業(yè)務異常蓝丙。舉個例子级遭,如果希望在創(chuàng)建訂單的時候,檢測到商品不存在渺尘,拋 “創(chuàng)建訂單失敗” 的異常挫鸽,可以這么寫:
@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, "訂單創(chuàng)建失敗,請稍后重試");
private int code;
private String message;
}
public Goods getGoods(String id) {
return null;
}
上面的代碼最后打印如下日志:
但有沒有發(fā)現(xiàn)鸥跟,控制臺打印的內容丢郊,對分析問題的幫助有限,因為導致訂單失敗的原因有很多医咨,就比如上面舉例的 商品不存在
枫匾,也有可能是 計算訂單金額時出現(xiàn)異常
,亦或是 調用其他服務時發(fā)現(xiàn)服務不可用
等等拟淮。
其實在開發(fā)中干茉,這樣的場景是很常見,可以簡單歸納為:一個大類異澈懿矗可以再細分出各種更具體的異常角虫,并且用戶并不關心具體異常沾谓,只關心此次操作成功與否。
雖說用戶不關心真正的錯誤原因上遥,但對于開發(fā)人員來說搏屑,還是有必要知道真正的問題出在哪里,不然運維看到這些日志然后粉楚,說:那啥辣恋,用戶創(chuàng)建訂單失敗,你看是不是有bug模软。然后我瞬間就——
如果可以在打印日志的時候順便也把具體錯誤信息也打印出來伟骨,那定位問題就簡單多了。比如:商品服務突然宕機不可用了燃异,運維看到了直接緊急恢復下服務携狭,用戶就又能正常下單了。
自定義錯誤信息
具體的錯誤信息回俐,肯定不是程序自己憑空構造出來的逛腿,而是需要開發(fā)人員在開發(fā)過程中,以某種形式去教程序怎么構造仅颇,構造出來后单默,跟最終返回給用戶端的錯誤信息一起打印出來。
所以打印出來的錯誤日志忘瓦,必須包含2個錯誤信息搁廓,一個是給用戶看的錯誤信息(訂單創(chuàng)建失敗)耕皮,另一個是給運維/開發(fā)人員看的錯誤信息(獲取商品詳情失斁惩伞)。
分析到這里凌停,接下來就是怎么實現(xiàn)的問題了粱年。
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>支持傳遞參數(shù)方式皆尔,避免在判斷之前進行字符串拼接操作
*
* @param obj 待判斷對象
* @param errMsg 自定義的錯誤信息. 支持 {index} 形式的占位符, 比如: errMsg-用戶[{0}]不存在, args-1001, 最后打印-用戶[1001]不存在
* @param args message占位符對應的參數(shù)列表
*/
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
后流炕,使用該錯誤信息創(chuàng)建一個 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
(相信各位大佬都是知道怎么看異常信息的)渠欺,把我們剛剛傳進去的具體錯誤信息也打印出來了。再從整體上看椎眯,從上到下分別是:訂單創(chuàng)建失敗挠将,請稍后重試
→ Caused by: 商品[1001]不存在
,是不是很流暢编整,一下子就能定位具體異常舔稀。
newExceptionWithMsg
因為有很多斷言方法,每個方法都需要寫大致相同的邏輯闹击,所以這里再封裝兩個 newExceptionWithMsg
默認方法镶蹋,如下:
/**
* 創(chuàng)建異常.
* 先使用 {@code errMsg} 創(chuàng)建一個 {@link WrapMessageException} 異常,
* 再以入?yún)⒌男问絺鹘o {{{@link #newException(Throwable, Object...)}}}, 作為最后創(chuàng)建的異常的 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);
}
/**
* 創(chuàng)建異常.
* 先使用 {@code errMsg} 和 {@code t} 創(chuàng)建一個 {@link WrapMessageException} 異常,
* 再以入?yún)⒌男问絺鹘o {{{@link #newException(Throwable, Object...)}}}, 作為最后創(chuàng)建的異常的 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 表達式
新特性時經(jīng)常會用到,如果對這一特性不是特別了解仲义,可先略過婶熬,只需知道一點就是:可以通過 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
這個類的職能有關。因為該類只用來包裝 錯誤信息拔鹰,也可以理解為 錯誤信息 的載體仪缸,所以不定義無參構造方法。另外列肢,有時已經(jīng)有一個具體異常恰画,那么當然也需要支持傳進來,所以又加多一個構造方法瓷马。
至于為什么定義這樣一個異常類锣尉,考慮到以后可能會對捕獲到的異常進一步分析,如果檢測到存在 WrapMessageException
决采,則執(zhí)行某種邏輯自沧,所以必須定義一個具體異常類,且不能繼承 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);
謝謝觀看,完RァJ凭觥!
推薦閱讀
Spring Cloud 進階玩法
Spring Cloud Stream 進階配置——使用延遲隊列實現(xiàn)“定時關閉超時未支付訂單”