新聞中心
一、什么是好的錯(cuò)誤信息(Error Message)?
- Context: 什么導(dǎo)致了錯(cuò)誤?發(fā)生錯(cuò)誤的時(shí)候代碼想做什么?
- The error itself: 到底是什么導(dǎo)致了失敗?具體的原因和當(dāng)時(shí)的數(shù)據(jù)是什么?
- Mitigation: 有什么解決方案來克服這個(gè)錯(cuò)誤,也可以理解為 Solutions。
聽起來還是有點(diǎn)抽象,能否給點(diǎn)代碼? 剛好有一個(gè) jdoctor 的項(xiàng)目,作者來自O(shè)racle Labs[1] 樣例代碼如下:

創(chuàng)新互聯(lián)公司是一家成都網(wǎng)站設(shè)計(jì)、成都網(wǎng)站制作,提供網(wǎng)頁設(shè)計(jì),網(wǎng)站設(shè)計(jì),網(wǎng)站制作,建網(wǎng)站,專業(yè)公司,網(wǎng)站開發(fā)公司,于2013年成立是互聯(lián)行業(yè)建設(shè)者,服務(wù)者。以提升客戶品牌價(jià)值為核心業(yè)務(wù),全程參與項(xiàng)目的網(wǎng)站策劃設(shè)計(jì)制作,前端開發(fā),后臺(tái)程序制作以及后期項(xiàng)目運(yùn)營并提出專業(yè)建議和思路。
ProblemBuilder.newBuilder(TestProblemId.ERROR1, StandardSeverity.ERROR, "Hawaiian pizza")
.withLongDescription("Pineapple on pizza would put your relationship with folks you respect at risk.")
.withShortDescription("pineapple on pizza isn't allowed")
.because("the Italian cuisine should be respected")
.documentedAt("https://www.bbc.co.uk/bitesize/articles/z2vftrd")
.addSolution(s -> s.withShortDescription("eat pineapple for desert"))
.addSolution(s -> s.withShortDescription("stop adding pineapple to pizza"));
這里的Problem理解為Error沒有問題,核心主要包括以下幾個(gè)字段:
- context: such as app name, component, status code,使用一個(gè)字符串描述當(dāng)時(shí)的上下文,如應(yīng)用名稱 + 組件名稱 +具體的錯(cuò)誤狀態(tài)碼等,這個(gè)由你自己決定,當(dāng)然JSON字符串也可以,如 {"app":"uic", "component": "login", "code":"111"}。
- description: Long(Short) to describe error 錯(cuò)誤描述,有Long和Short兩者。
- because/reason: explain the reason with data 詳細(xì)解釋錯(cuò)誤的原因,當(dāng)然必須包含相應(yīng)的數(shù)據(jù)。
- documentedAt: error link 錯(cuò)誤對(duì)應(yīng)的HTTP連接,更詳細(xì)地介紹該錯(cuò)誤。
- solutions: possible solutions 可能的解決方案,如提示訪問者檢查email拼寫是否正確,短信的Pass Code是否輸入正確等。
有了這些具體的字段后,我們理解起來就方便多啦。
二、錯(cuò)誤碼(Error Code)的設(shè)計(jì)
各種錯(cuò)誤處理上都建議使用錯(cuò)誤碼,錯(cuò)誤碼有非常多的優(yōu)勢(shì):唯一性、搜索/統(tǒng)計(jì)更方便等,所以我們還是要討論一下錯(cuò)誤碼的設(shè)計(jì)。網(wǎng)上也有不少錯(cuò)誤碼的設(shè)計(jì)規(guī)范,當(dāng)然這篇文章也少不了重復(fù)造輪子,該設(shè)計(jì)提供給大家參考,大家自行判斷啊,當(dāng)然也非常歡迎留言指正。
一個(gè)錯(cuò)誤碼通常包含三個(gè)部分:
- System/App short name: 系統(tǒng)或者應(yīng)用的名稱,如 RST, OSS等。如果你熟悉Jira的話,基本也是這個(gè)規(guī)范,Java程序員應(yīng)該都知道HHH和SPR代表什么吧?
- Component short name or code: 系統(tǒng)內(nèi)部的組件名稱或者編碼,如LOGIN, AUDIT,001 這些都可以,方便更快地定位錯(cuò)誤。
- Status code: 錯(cuò)誤的狀態(tài)碼,這個(gè)是一個(gè)三位數(shù)字的狀態(tài)碼,如200,404,500,主要是借鑒自 HTTP Status Code,畢竟絕大多數(shù)開發(fā)者都了解HTTP狀態(tài)碼,我們沒有必要再重新設(shè)計(jì)。
有了上述的規(guī)范后,讓我們看一下典型的錯(cuò)誤編碼長(zhǎng)什么樣子:
- OSS-001-404: 你應(yīng)該知道是OSS的某一組件報(bào)告資源沒有找到吧;
- RST-002-500:這個(gè)是一個(gè)組件的內(nèi)部錯(cuò)誤;
- UIC-LOGIN-404:這個(gè)應(yīng)該是會(huì)員登錄時(shí)查找不到指定的賬號(hào)。
我們采用應(yīng)用名縮寫, 組件名或者編碼, 狀態(tài)值,然后以中劃線連接起來。中劃線比較方便閱讀,下劃線有時(shí)候在顯示的時(shí)候理解為空格。同時(shí)有了標(biāo)準(zhǔn)的HTTP Status Code支持,不用參考文檔,你都能猜一個(gè)八九不離十。 錯(cuò)誤碼設(shè)計(jì)千萬不要太復(fù)雜,試圖將所有的信息都添加進(jìn)去,當(dāng)然信息非常全,但是也增加了開發(fā)者理解和使用成本,這個(gè)可能要做一個(gè)取舍,當(dāng)然我也不是說目前這種一鍵三連(打賞、點(diǎn)贊加轉(zhuǎn)發(fā))的結(jié)構(gòu)就最合理,你也可以自行調(diào)整。有沒有做心里研究的同學(xué)來說一下,這種三部分組成的方式,是不是最符合人們的認(rèn)知習(xí)慣?如果超過三部分,如4和5,人們能記住和使用的概率是不是就下降的非常多?
還記得前面說的error的context嗎?這里error code其實(shí)就是啟動(dòng)context的作用,如 UIC-LOGIN-404,錯(cuò)誤發(fā)生在哪里?錯(cuò)誤碼幫你定位啦。當(dāng)時(shí)代碼想干什么?錯(cuò)誤碼也說明啦。雖然說錯(cuò)誤碼不能完全代表錯(cuò)誤的上下文,但是其承載的信息已經(jīng)足夠我們幫我們了解當(dāng)時(shí)的上下文啦,所以這里error code就是起著context的作用。目前看來至少error code要比ProblemBuilder.newBuilder(TestProblemId.ERROR1, StandardSeverity.ERROR, "Hawaiian pizza") 中的Hawaiian pizza 作為context更具有說服力,也規(guī)范一些。
三、錯(cuò)誤消息的編寫格式
錯(cuò)誤碼設(shè)計(jì)完畢后,我們還不能用錯(cuò)誤碼+簡(jiǎn)短消息方式輸出錯(cuò)誤,不然就出現(xiàn)類似 ORA-00942: table or view does not exist這種情況,你一定會(huì)吐槽:"你為何不告訴哪個(gè)表或者view?"。所以我們還需要設(shè)計(jì)一個(gè)message格式,能夠?qū)㈠e(cuò)誤的context, description, reason, document link, solutions全部包含進(jìn)來,這樣對(duì)開發(fā)者會(huì)比較友好。這里我擬定了一個(gè)Message的規(guī)范,當(dāng)然大家可以發(fā)表自己的意見啊,如下:
long description(short desc): because/reason --- document link -- solutions
解釋一下:
- 錯(cuò)誤的長(zhǎng)描述直接書寫,短描述使用括弧進(jìn)行包含。這種寫法在合同中非常常見,如阿里云計(jì)算有限公司(阿里云) ,你簽署勞動(dòng)合同時(shí),公司的稱謂基本也是全名(代稱) 這種方式。好多同學(xué)會(huì)在錯(cuò)誤日志中書寫登錄失敗,但是登錄系統(tǒng)中有多種登錄方式,所以遠(yuǎn)不如Failed to log in with email and password(Login Failed), Failed to log in with phone and passcode(Login Failed), Failed to log in with oauth2(Login Failed) 更清晰。
- 錯(cuò)誤具體原因: 接下來是冒號(hào),然后書寫詳細(xì)的原因,如 email [email protected] not found ,gender field is not allowed in package.json 一定要包含具體的數(shù)據(jù)信息,包括輸入的,還是和勞動(dòng)合同一樣,抬頭之后就是你的具體崗位和薪水,雖然合同是格式化的,但是每一個(gè)人具體的崗位和薪水是不同的,這些參數(shù)都是從外部獲取的。此處有安全同學(xué)發(fā)問,如何數(shù)據(jù)脫敏?這個(gè)是另外的問題,大多數(shù)開發(fā)者應(yīng)該了解如何進(jìn)行mask,這里我們就跳過。當(dāng)出現(xiàn)勞動(dòng)糾紛這個(gè)錯(cuò)誤時(shí),具體原因中的數(shù)據(jù),如崗位和薪水等,這樣勞動(dòng)仲裁局就可以快速定位并解決該"錯(cuò)誤"。
- document link: 接下來我們使用三種劃線---進(jìn)行分隔,輸入對(duì)應(yīng)的error link。三劃線作為分隔符在很多的場(chǎng)景中多有使用,如mdx, yaml等,大家不會(huì)太陌生。 如果沒有l(wèi)ink那就忽略就可以。
- solutions:自然的文本表述即可,能說明清楚就可以,也是放在三中劃線后。
看一個(gè)具體的消息格式例子:
APP-100-400=Failed to log in system with email and password(Email login failed): can not find account with email {} --- please refer https://example.com/login/byemail --- Solutions: 1. check your email 2. check your password
上述的APP-100-400的錯(cuò)誤碼對(duì)應(yīng)的描述基本覆蓋到j(luò)doctor中需要的信息,可以說對(duì)一個(gè)錯(cuò)誤的描述應(yīng)該非常全啦,而且有一定的格式,也方便后續(xù)的日志分析。
四、組裝和保存錯(cuò)誤碼 + Message
有了錯(cuò)誤碼和message的規(guī)范,接下來我們應(yīng)該如何保存這些信息呢?如果是Java,是不是要?jiǎng)?chuàng)建對(duì)應(yīng)的ErrorEnum,然后是一些POJO?這里個(gè)人建議使用properties文件來保存錯(cuò)誤碼和message的信息。文件名可以直接為ErrorMessages.properties,當(dāng)然是在某一package下,文件樣例如下:
### error messages for your App
APP-100-400=Failed to log in system with email and password(Email login failed): can not find account with email {0} --- please refer https://example.com/login/byemail --- Solutions: 1. check your email 2. check your password
APP-100-401=Failed to log in system with phone and pass(Phone login failed): can not find account with phone {0} --- please refer https://example.com/login/byphone --- Solutions: 1. check your phone 2. check your pass code in SMS
為何要選擇properties文件來保存error code和message信息,主要有以下幾個(gè)原因:
- 國際化支持:Java的同學(xué)都知道,如果你的錯(cuò)誤消息想調(diào)整為中文,創(chuàng)建一個(gè)ErrorMessages-zh_CN.properties 即可。原文中的建議是Don’t localize error messages,但是考慮到國內(nèi)大多數(shù)程序員未必能用英文表達(dá)清楚,所以中文也是可以的。題外話:如果中國的程序員都能用英文清晰地閱讀文章和表達(dá)自己的思想和觀點(diǎn),我們?cè)谟?jì)算機(jī)方面的水平可能會(huì)提升到更高的臺(tái)階。
- 各種語言對(duì)properties的文件解析都有支持,不只是Java,其他語言也有,而且properties文件本身也不復(fù)雜,所以該properties文件可以給Node.js, Rust等其他語言使用,如果是Java enum和POJO基本就不可能啦。
- properties文件格式豐富:支持注釋,換行符,多行轉(zhuǎn)義等也都沒有問題。
最后最關(guān)鍵的是IDE支持非常友好 , 以Java開發(fā)者使用的IntelliJ IDEA來說,對(duì)Properties文件的支持可以說是到了極致,如下:
- error code的自動(dòng)提示
快速查看:鼠標(biāo)移上去就可以,按下CMD鼠標(biāo)移上去也可以, Alt+Space也可以,當(dāng)然點(diǎn)擊直接定位就更不用說啦。
- 重構(gòu)和查找支持:雖然Error Code是字符串,但是也是properties的key,所以rename這個(gè)error code,所有引用的地方都會(huì)rename。還支持find usage,那些地方引用了該error code等,都非常方便。當(dāng)然如果Error Code在系統(tǒng)中沒有被使用,也會(huì)灰色標(biāo)識(shí)。
- 折疊自動(dòng)顯示功能:當(dāng)你的代碼處于折疊狀態(tài)時(shí),IDEA直接將message拿過來進(jìn)行顯示,你在code review的時(shí)候方便多啦,也便于你理解代碼。
直接修改message的值:
總之IntellIJ IDEA對(duì)properties文件的支持到了極致,我們也沒有理由不考慮開發(fā)者體驗(yàn)的問題,到處跳來跳去地找錯(cuò)誤碼,這種傷害程序員開發(fā)體驗(yàn)的事情不能做。 當(dāng)然JetBrains的其他IDE,WebStorm等都有對(duì)proproperties文件編輯支持。
五、代碼實(shí)現(xiàn)
看起來功能挺酷炫的,是不是這種方式錯(cuò)誤管理要介入一個(gè)開發(fā)包啊?不需要,你只需要10行代碼就搞定,如下:
import org.slf4j.helpers.MessageFormatter;
public class AppErrorMessages {
private static final String BUNDLE_FQN = "app.ErrorMessages";
private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_FQN, new Locale("en", "US"));
public static String message(@PropertyKey(resourceBundle = BUNDLE_FQN) String key, Object... params) {
if (RESOURCE_BUNDLE.containsKey(key)) {
String value = RESOURCE_BUNDLE.getString(key);
final FormattingTuple tuple = MessageFormatter.arrayFormat(value, params);
return key + " - " + tuple.getMessage();
} else {
return MessageFormatter.arrayFormat(key, params).getMessage();
}
}
}
這樣在任何地方如果你要打印錯(cuò)誤消息的時(shí)候,這樣log.info(AppErrorMessages.message("APP-100-400","xxx"));就可以。如果你還有想法和log進(jìn)行一下Wrapper,如 log.info("APP-100-400","xxx"); ,也沒有問題,樣例代碼如下:
public class ErrorCodeLogger implements Logger {
private Logger delegate;
private static final String BUNDLE_FQN = "app.ErrorMessages";
private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_FQN, new Locale("en", "US"));
public ErrorCodeLogger(Logger delegate) {
this.delegate = delegate;
}
@Override
public void trace(@PropertyKey(resourceBundle = BUNDLE_FQN) String msg) {
delegate.trace(RESOURCE_BUNDLE.getString(msg));
}
}
接下來你就可以在log中直接整合error code,非常便捷。上述代碼我已經(jīng)寫好,你參考文章末尾的項(xiàng)目地址即可。
最終的日志輸出如下:
提醒:這里我們使用了slf4j的MessageFormatter,主要是方便后續(xù)的Slf4j的整合,而且slf4j的MessageFormatter比Java的MessageFormat容錯(cuò)和性能上更好一些。
六、FAQ
1. 為何選擇3位的HTTP Status Code作為Error的Status Code?
大多數(shù)開發(fā)者對(duì)HTTP Status Code都比較熟悉,所以看到這些code就大致明白什么意思,當(dāng)然對(duì)應(yīng)用開發(fā)者也有嚴(yán)格的要求,你千萬別將404解釋為內(nèi)部錯(cuò)誤,如數(shù)據(jù)庫連接失敗這樣的,逆正常思維的事情不要做。HTTP status code歸類如下,當(dāng)然你也可以參考一下 HTTP Status Codes Cheat Sheet[2]。
- Informational responses (100–199)
- Successful responses (200–299)
- Redirection messages (300–399)
- Client error responses (400–499)
- Server error responses (500–599)
但是Error Status Code不局限在HTTP Status Code,你也可以參考SMTP, POP3等Status Code,此外你也自行可以選擇諸如007,777這樣的編碼,只要能解釋的合理就可以啦。
在日常的生活中,我們會(huì)使用一些特殊意義的數(shù)字或者和數(shù)字諧音,以下是一些友情提醒:
- UIC-LOGIN-666: 太順利啦,完美登錄。但是你團(tuán)隊(duì)中有歐美老外的話,他可能理解為理解為惡意登錄,登錄失??;
- APP-LOGIN-062: 如果你團(tuán)隊(duì)有杭州土著的話,不要使用62這個(gè)數(shù)字;
- APP-001-013: 如果該error code要透?jìng)鹘o最終用戶,請(qǐng)不要使用13這個(gè)數(shù)字,會(huì)引發(fā)不適。
這種有特殊意義的數(shù)字或者數(shù)字諧音,如520,886,999,95等,如果能使用的恰當(dāng)非常方便理解或更友好,如透?jìng)鹘o用戶UIC-REG-200(注冊(cè)成功),如果調(diào)整為UIC-REG-520可能更溫馨一些??偟膩碚f使用這些數(shù)字要注意場(chǎng)景,當(dāng)然比較保險(xiǎn)的做法就是參考HTTP,SMTP等設(shè)計(jì)的status code。
2. properties文件存儲(chǔ)error code和message,真的比enum和POJO好嗎?
就Java和IntelliJ IDEA的支持來看,目前的配合還是比較好的,如i18n,維護(hù)成本等,而且這些ErrorMessages.properties也可以提交到中心倉庫進(jìn)行Error Code集中管理,如果是Java Enum+POJO對(duì)i18n和集中管理都比較麻煩,而且代碼量也比較大,你從上述的jdoctor的problem builder的就可以看出。當(dāng)然在不同的語言中也未必是絕對(duì)的,如在Rust中,由于enum的特性比較豐富,所以在Rust下使用enum來實(shí)現(xiàn)error code可能是比較好的選擇。
#[derive(Debug)]
enum ErrorMessages {
AppLogin404 {
email: String,
},
AppLogin405(String),
}
impl fmt::Display for ErrorMessages {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// extract enum name parameter
// output message from java-properties
write!(f, "{:?}", self)
}
}
3. 為何不在Error Code中提供錯(cuò)誤級(jí)別
不少錯(cuò)誤碼設(shè)計(jì)中會(huì)添加錯(cuò)誤級(jí)別,如 RS-001-404-9 這樣,最后一位表示錯(cuò)誤的嚴(yán)重級(jí)別。這樣做沒有問題,但是也要考慮現(xiàn)實(shí)因素,如下:
- 錯(cuò)誤的級(jí)別會(huì)動(dòng)態(tài)調(diào)整的:如隨著時(shí)空的變化,之前非常嚴(yán)重的錯(cuò)誤級(jí)別,現(xiàn)在并不那么嚴(yán)重啦。如果資源找不到可能之前非常嚴(yán)重,但是現(xiàn)在添加了備份方案,可以從備份服務(wù)器中再查找一次,所以這個(gè)錯(cuò)誤出現(xiàn)在主服務(wù)上可能現(xiàn)在就不是那么嚴(yán)重啦。
- 不同團(tuán)隊(duì)對(duì)錯(cuò)誤級(jí)別的認(rèn)知不一樣:如OSS-404在OSS團(tuán)隊(duì)的data server上找不到,元信息都是有的,結(jié)果在data server上沒有找到對(duì)應(yīng)的數(shù)據(jù),這個(gè)是非常嚴(yán)重的錯(cuò)誤。雷卷在業(yè)務(wù)團(tuán)隊(duì),如負(fù)責(zé)Serverless Jamstack,其中的一個(gè)文件缺失,如html, css, image,可能并不是一個(gè)大問題,等一會(huì)重試下,不行就再上傳一下。我想表達(dá)的是同樣的錯(cuò)誤,在不同團(tuán)隊(duì)中的重要性并不一樣。
如果將錯(cuò)誤的基本固化到error code中,這個(gè)后續(xù)你就沒法調(diào)整啦,你如果調(diào)整了錯(cuò)誤級(jí)別,那就是可能就是另外一個(gè)錯(cuò)誤碼,給統(tǒng)計(jì)和理解都會(huì)造成問題。我個(gè)人是建議錯(cuò)誤碼中不要包括嚴(yán)重級(jí)別這些信息,而是通過外圍的文檔和描述進(jìn)行說明,當(dāng)然你也可以通過諸如 log.info , log.error來確定錯(cuò)誤的級(jí)別。
4. 能否提供共享庫?
由于IntelliJ IDEA并不支持動(dòng)態(tài)的properties文件名稱,如果你用動(dòng)態(tài)的properties文件名稱,就不能進(jìn)行代碼提示,查找等功能也都不能使用,所以必須是這種 @PropertyKey(resourceBundle = BUNDLE_FQN) 靜態(tài)的properties文件名方式。就一個(gè)Java類,你就受累Copy一下這個(gè)Java類,畢竟是一次性的工作,當(dāng)然你想個(gè)性化調(diào)整代碼也更方便,如和Log4j 2.x或自定也的logging框架整合也簡(jiǎn)單些。 日志是項(xiàng)目最基本的需求,所以你創(chuàng)建的項(xiàng)目的時(shí)候,就把Error Code對(duì)應(yīng)的代碼添加到項(xiàng)目模板中,這樣項(xiàng)目創(chuàng)建后就自動(dòng)包含logging和error code的功能。
5. 其他的考量
原文和Reddit上相關(guān)的討論也進(jìn)行了一些整理和說明:
- 內(nèi)外有別:如內(nèi)部開發(fā)者的錯(cuò)誤中可能會(huì)包括服務(wù)器的具體信息,當(dāng)然給最終消費(fèi)者,如平臺(tái)的FaaS開發(fā)者,可能就不能輸出這樣的信息,有一定的安全風(fēng)險(xiǎn)。
- 小心在錯(cuò)誤中暴露敏感數(shù)據(jù):輸出到錯(cuò)誤日志的數(shù)據(jù)一定要進(jìn)行mask,當(dāng)然也不要影響你定位錯(cuò)誤,這個(gè)要看具體的場(chǎng)景。
- 不要將錯(cuò)誤消息作為 API 契約:在API的場(chǎng)景中,響應(yīng)錯(cuò)誤有兩種方式:根據(jù)錯(cuò)誤碼做響應(yīng),如REST API;另外一種是根據(jù)消息做出響應(yīng),如GraphQL,所以這個(gè)你自行選擇。
- Error Code的一致性:錯(cuò)誤消息會(huì)輸出給不同的消費(fèi)者,如REST API,界面等,可能錯(cuò)誤的提示消息有所不同,如國際化、脫敏等,但是最好都是相同的error code,也就是front end + backend 共享相同的error code,方便定位錯(cuò)誤和統(tǒng)計(jì)。
七、總結(jié)
采用error code + 基于properties文件存儲(chǔ)error message,這個(gè)設(shè)計(jì)其實(shí)就是一個(gè)綜合的取舍。如果IDEA不能很好地支持properties文件,你看到一個(gè)Error Code,不能直接定位到錯(cuò)誤的消息,相反還需要跳轉(zhuǎn)來跳轉(zhuǎn)去找對(duì)應(yīng)的消息,那么Enum + POJO可能就是好的選擇。此外error code的設(shè)計(jì)也非常偏向http status code方案,這個(gè)也是主要基于大家對(duì)HTTP都非常熟悉,基本上就能猜出大概的意思,相反隨機(jī)編碼的數(shù)字就沒有這方法的優(yōu)勢(shì),要去error code中心再去查找一下,無形中也是浪費(fèi)開發(fā)人員的時(shí)間。
最后項(xiàng)目的Demo地址:http://gitlab.alibaba-inc.com/leijuan/java-error-messages-wizard
[1]https://github.com/melix/jdoctor
[2]https://cheatography.com/kstep/cheat-sheets/http-status-codes/
[3]https://www.morling.dev/blog/whats-in-a-good-error-message/
當(dāng)前名稱:什么是好的錯(cuò)誤消息? 討論一下Java系統(tǒng)中的錯(cuò)誤碼設(shè)計(jì)
瀏覽路徑:http://m.fisionsoft.com.cn/article/dpgsjee.html


咨詢
建站咨詢
