統(tǒng)一異常處理介紹及實戰(zhàn)——支持自定義錯誤信息

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;
}

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


order creation failed

但有沒有發(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 提供的拂酣,在使用 java8lambda 表達式 新特性時經(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)“定時關閉超時未支付訂單”

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末蓝撇,一起剝皮案震驚了整個濱河市果复,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌渤昌,老刑警劉巖虽抄,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異独柑,居然都是意外死亡迈窟,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進店門忌栅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來车酣,“玉大人,你說我怎么就攤上這事『Ь叮” “怎么了?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵者春,是天一觀的道長破衔。 經(jīng)常有香客問我,道長钱烟,這世上最難降的妖魔是什么晰筛? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮拴袭,結果婚禮上读第,老公的妹妹穿的比我還像新娘。我一直安慰自己拥刻,他們只是感情好怜瞒,可當我...
    茶點故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著般哼,像睡著了一般吴汪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蒸眠,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天漾橙,我揣著相機與錄音,去河邊找鬼楞卡。 笑死霜运,一個胖子當著我的面吹牛,可吹牛的內容都是我干的蒋腮。 我是一名探鬼主播淘捡,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼池摧!你這毒婦竟也來了案淋?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤险绘,失蹤者是張志新(化名)和其女友劉穎踢京,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宦棺,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡瓣距,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了代咸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蹈丸。...
    茶點故事閱讀 40,852評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖奶是,靈堂內的尸體忽然破棺而出商乎,到底是詐尸還是另有隱情梭灿,我是刑警寧澤候衍,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布房铭,位于F島的核電站竟坛,受9級特大地震影響丈甸,放射性物質發(fā)生泄漏线得。R本人自食惡果不足惜够话,卻給世界環(huán)境...
    茶點故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一蓝翰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧女嘲,春花似錦畜份、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至愕鼓,卻和暖如春顶别,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拒啰。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工驯绎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谋旦。 一個月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓剩失,卻偏偏與公主長得像,于是被迫代替她去往敵國和親册着。 傳聞我的和親對象是個殘疾皇子拴孤,可洞房花燭夜當晚...
    茶點故事閱讀 45,851評論 2 361