希望大家可以收獲:
1,背景分析是否貼合工作的實際場景,能否觸及痛點;
2,統一的技術方案,並演示最終的實現效果;
3,前端和后端相對完整的技術實現方案,系統的思考方式;
背景和需求
不同人群對錯誤處理的期望不同:這里基於業務系統簡單列表匯總;
人群 | 錯誤提示的期望 |
---|---|
業務系統產品經理 | 錯誤提示也是產品設計的一部分,標識正常業務的邊界,基於錯誤提示可以快速的進行業務功能的邊界條件,關鍵流程流向提示; |
業務系統測試人員 | 能定提示到是到底是前端還是后端的問題,快速的分類bug,指派給對應的開發人員;進行需求的二次確認,一些參數邊界的提示信息必須符合產品規約。 |
業務系統前端開發人員 | 聯調的時候,后端的錯誤可以提示哪里出錯了,如果是參數錯誤,讓我指引我哪個參數錯了,我好調整如果是后端邏輯或者內部錯誤,方便我提供截圖和traceId給到后端開發,讓后端去解決; |
業務系統運維人員 | 后端資源耗盡了,最好可以提示我哪塊資源不足,如何補充;中間件有問題了,告知我哪個中間件,建議的運維方法;如果實在無法在界面上告訴我,可以快速看到對應的應用日志,丟回給開發去進一步定位問題。 |
業務系統后端開發人員 | 開發和集成測試環境,最好在界面上或者控制台能看到堆棧信息,哪行代碼出錯了;最次也要能從界面或者控制台,或者抓包中找到traceId,方便我從日志中或者調用鏈跟蹤系統中快速的定位問題,方便快速解決問題; |
業務系統管理層 | 可服務性好,站在用戶的角度,希望有規范的提示和回到正確流程的提示;站在客戶方的二開或者集成工程師角度,希望錯誤碼能統一,並且對提示,方便我快速集成和二開;站在開發周期來說,希望錯誤提示可以加快前后端聯調,測試的工作效率; |
架構師 | 錯誤處理公共組件化,兼顧開發期的可擴展性,復用性,易用性,以及兼顧運行期的可服務性; |
二開用戶(業務系統B端-開發人員) | 我要錯誤編碼,還要指導提示,最好在本接口中返回給我,或者指引我一個文檔,我按照編碼去查;能加速我快速的集成或者二開; |
用戶:業務系統B-C端用戶 | 告訴我哪里出錯了,正確的使用方法,讓我可以回到正確的流程;最好還能顯示級別;提示不能為空,不能有英文,不能有堆棧信息,不能有我看不懂的信息 |
客戶:業務系統B端應用配置人員 | 同C端用戶,主要是告訴我哪里操作錯了,讓我可以回到正確的流程中; |
下面進行抽象和匯總。
一個合適的錯誤處理方案應該是怎樣的?
統一技術方案
位置 | 處理要點 | 說明 |
---|---|---|
前端 | 前端實現axios攔截器異常捕獲,封裝組件實現,展示邏輯&形式 | 原則:服務端能響應的、能返回錯誤的,提示語使用后端返回服務端不能響應的、不能返回錯誤的,提示語使用前端約定 |
后端 | 對rest接口進行統一異常的捕獲並轉換為錯誤碼,錯誤消息;對直接組裝的統一錯誤碼,錯誤消息,進行統一的管理,按照微服務進行錯誤碼進行封裝;封裝為組件形式,錯誤碼按照接口的規約進行限制,應用級別的錯誤碼和錯誤消息分散在微服務中; | 錯誤分兩種形式:1,通過異常輸出錯誤;2,通過組裝錯誤碼和錯誤消息拼裝錯誤返回信息;異常分為3類:1,參數校驗或者接口url資源定位不到,需要提示前端調整;2,內部的邏輯錯誤或者jvm異常,通過RuntimeException拋出;3,依賴的公共組件錯誤,給出環境問題或者調用問題的提示; |
后端
形式: 中間件的方式,定義暴露的配置屬性,對異常進行統一的處理封裝;
這里做一下調整,統一把分散在微服務里面錯誤碼枚舉放到團隊公共的SDK中;
后端錯誤的分類:
內部:主要是對前端,大部分錯誤通過異常的方式拋出,后端做統一的處理;
外部系統:主要對接外部系統,有些是直接拼接錯誤碼和錯誤消息的方式輸出的;
建立在服務可用,即httpStatus=200的基礎上,內部異常的分類:
錯誤描述 | 說明 |
---|---|
輸入參數非法 | 參數缺失,參數不符合規則要求,請求類型不支持 |
邏輯錯誤 | 不具備操作權限,jvm內部的異常,比如NPE等,方法超時,運行時異常(空指針等) |
內部環境錯誤 | 依賴的中間件不可用或者調用方法報錯,比如SQL寫錯了了 |
如果網關服務不可用: nginx需要有對應的40X , 友好json數據
如果網關后面的后端服務不可用: 后端服務需要返回 40X,50X的友好json數據;
用戶 | nginx 故障 | 后端網關 | 后端服務 |
---|---|---|---|
前端資源 | 404友好提示頁面 | 不經過 | 不經過 |
前端訪問后端資源 | url錯誤,瀏覽器默認404頁面 | 路由找不到,404轉換為json數據 | 40x轉換為json數據 50x自然轉換成了json數據 |
查看老業務系統的代碼,現在后端的錯誤處理方式分兩種:
錯誤處理方式 | 說明 | 目前的缺點 |
---|---|---|
統一異常處理 | 通過在web-api-service工程中 通過@RestControllerAdvice 標注一個統一的異常處理類 對每一種類別的異常進行處理統計如下表 | 異常的層級和分類不夠清晰有些異常 e.getmessage可能是英語,看不懂; |
直接拼裝錯誤碼和錯誤消息 | 分散在業務代碼中,見下面的截圖和部分代碼截取 | 無法統一管理錯誤碼和錯誤信息,並且錯誤信息中無正確操作指引信息 |
老業務系統統一異常處理分類
異常分類 | 父類 | 錯誤碼 | 說明 |
---|---|---|---|
RuntimeException | Exception | SERVER_ERROR(50000L, "服務異常") | 運行時異常 |
MissingServletRequestParameterException | ServletRequestBindingException-》NestedServletException-》ServletException-》Exception | ILLEGAL_PARAMETER_ERR(10005L, "非法的參數")缺少必傳參數+paramName | 接口參數綁定異常 |
HttpMessageNotReadableException | HttpMessageConversionException-》NestedRuntimeException-》RuntimeException-》Exception | CLASS_CAST_ERR(10009L, "類型轉換異常"), | JSON轉換異常,包含更多的消息轉換異常 |
HttpRequestMethodNotSupportedException | ServletException-》Exception | METHOD_NOT_SUPPORT(40007L, "請求方法不正確"), | ajax的http方法寫錯,或者簽名不對,如媒體類型等; |
ServiceException | |||
RuntimeException->Exception | 引擎層自定義的code,msg,異常數據 | 在引擎層進行了編碼和MSG的規范 | |
BusinessRuleException | RuntimeException->Exception | 引擎層自定義的code,msg,異常數據2 | 在引擎層進行了編碼和MSG的規范 |
PortalException | RuntimeException->Exception | Web Api異常 Portal 拋出的自定義異常 code msg 異常數據自定義 | webAPI異常范圍太廣泛 code msg 的定義不太規范,有隨意定義的代碼出現 |
RemotingException | Exception | 調用遠程服務失敗 REMOTING_ERR(10006L, "調用遠程服務失敗") | webAPI調用引擎的dubbo服務異常 |
RpcException | RuntimeException->Exception | 調用遠程服務失敗 REMOTING_ERR(10006L, "調用遠程服務失敗") | dubbo框架異常 |
UndeclaredThrowableException | RuntimeException->Exception | 一般用在在調用代理的方法調用的時候拋出的檢查異常__ _分別匹配_LicenseException ServiceException RuntimeException 如果類型匹配不上,code,msg __SERVER_ERROR(50000L, "服務異常"), | 未定義異常 未定義拋出異常 這里做了一個統一處理,理論上是不起作用的,會提前分流到對應的異常類型中去 |
InvocationTargetException | ReflectiveOperationException-》Exception | 用在調用代理的方法或者構造函數的時候拋出的檢查型異常____SERVER_ERROR(50000L, "服務異常"), | 進入托底異常 |
LicenseException | ServiceException-》RuntimeException->Exception | 校驗許可證拋出的異常code msg 異常數據自定義 | 許可證異常,引擎層的自定義異常 |
ConstraintViolationException | ValidationException->RuntimeException-》Exception | ILLEGAL_PARAMETER_ERR(10005L, "非法的參數"), | 參數校驗異常 |
MethodArgumentNotValidException | MethodArgumentNotValidException->Exception | ||
MaxUploadSizeExceededException | MultipartException-》NestedRuntimeException-》RuntimeException-》Exception | OSS_UPLOAD_SIZE_LIMIT_EXCEEDED(10025L,"文件上傳超出大小限制") | oss上傳文件超出大小限制異常 |
直接拼接錯誤碼返回
if (oauth2Authentication.isAuthenticated()) {
UserModel user = dubboConfigService.getSystemSecurityFacade().getUserByUsername(oauth2Authentication.getName());
return ResponseResult.builder().errcode(0L).data(user).errmsg("授權用戶信息加載成功").build();
} else {
return ResponseResult.builder().errcode(10403L).errmsg("未授權").build();
}
異常的知識補充:
Exception: 可以預見到的異常情況,應該被捕獲或者處理,在java中,分為檢查異常(編譯期)和不檢查異常(運行期)。
Error: 出現了錯誤系統不能正常運行或者恢復,一般情況不容易發生;
共同點:都繼承自Throwable,在java中只有Throwable的子類可以被catch或者throw;
ERROR一般是后端服務掛了,一般無法恢復,提示501 服務不可用或者404 ;
Throwable 異常的基類,一般不直接處理;
重點處理的Exception和RuntimeException
異常分類 | 說明 |
---|---|
CheckedException 檢查型異常, | 一般直接繼承Exception,(RuntimeException除外),需要顯示的try-catch 否則編譯報錯 |
RuntimeException 運行時異常 | 程序運行過程中發生的異常,編譯器無法提前發現,一般的業務異常都是運行時異常; |
在處理異常的時候,有4個基本規則需要注意:
- 不要catch 最普遍的Exception ,而應該優先捕獲具體的異常,可以留下足夠的診斷信息;
- 不要生吞異常,應該嘗試拋出或者寫到日志,否則無法判斷異常發生的位置;
- 不要使用e.printStackTrace(),在分布式系統中,無法確定輸出到了什么位置,應該輸出到日志中;
- 提早拋出,晚點捕獲;提高效率
自定義異常的時候需要注意兩點:
1,盡量不要定義檢查異常
2,異常需要保留足夠的診斷信息,但是也需要脫敏;
新業務系統錯誤碼統一管理
- 按照微服務統一的規范枚舉統一管理錯誤碼,錯誤信息,並填充建議操作信息,可通過共同接口進行規范;
比如design微服務定義微服務級別的錯誤碼枚舉 需要實現 ErrorCodeI接口,填充服務名稱,錯誤碼,錯誤提示信息,正確操作指引信息;
public interface ErrorCodeI {
/**
* 錯誤碼
* @return
*/
String getErrCode();
/**
* 錯誤描述
* @return
*/
String getErrDesc();
/**
* 獲取微服務的名稱
* @return
*/
default String getServiceName(){
return null;
}
/**
* 獲取恢復錯誤的正確指導
* @return
*/
default String getCorrectGuid(){
return null;
}
}
-
收縮自定義的errcode,errmessage到對應的枚舉中,進行統一的編碼和錯誤信息配置;
-
網關提供接口和前端頁面,展示所有的錯誤碼和錯誤信息,建議處理方法; 作為一個補充的查找建議操作的方案;(可在網關匯聚所有的微服務中的錯誤碼信息,並展示出來)
新業務系統異常體系分級分類
1,輸入參數異常(提示給到前端)
2,邏輯業務異常,JVM運行時異常(各微服務按照類別自行擴展)
3,內部異常(中間件的連接錯誤和異常,進行統一封裝)
4, 托底的異常捕獲(如果不是以上的異常,直接提示服務器內部錯誤,並提供可以查錯的地方;)
分別配置好對應的errcode, errmsg需要考慮到英文的情況,可對已經發現的中間件異常進行定義前置名稱,英文異常信息通過翻譯接口解決,最差要做托底中文信息替換;
前端
**原則:
服務端能響應的、能返回錯誤的,提示語使用后端返回
服務端不能響應的、不能返回錯誤的,提示語使用前端約定
**1. 狀態碼一覽表
Http Status | Code | Error Code | 等級 | 提示語 | 備注 |
---|---|---|---|---|---|
200 | 200 | B | |||
201 | B | ||||
300 | 300 | - | - | - | 通常不需要提示 |
... | - | - | - | 同上 | |
400 | 400 | B | |||
B | |||||
B | |||||
B | |||||
B | |||||
B | |||||
B | |||||
B | |||||
B | |||||
B | |||||
B | |||||
B | |||||
401 | B | 權限相關,提示登陸或無權限 | |||
402 | |||||
403 | F/B | ||||
404 | F | NOT FOUND | |||
... | |||||
500 | 500 | F | 服務端異常 | ||
501 | F | ||||
502 | F |
-
后端返回數據格式
| 字段 | 說明 |
| --- | --- |
| errCode | 錯誤碼 |
| errMessage | 提示消息 |
| data | 返回結果 |
| traceId | skywalking的跟蹤ID | -
前端實現axios攔截器異常捕獲,封裝組件實現,展示邏輯&形式
組件文檔: 部署到服務器上再統一放開
更多的需求點
崗位角度 | 需求補充 |
---|---|
后端 | 1,提供一個統一操作異常的工具類,替代throw new Exception(),規范異常的拋出 2,錯誤碼的規則:8位 1-2服務 3-4異常分類 5-8 序號,防止多微服錯誤碼重疊 3,錯誤提示信息和正確引導分成兩個字段返回到前端; |
前端 | 1,提示的風格具體應該是什么樣的,可能是UED來定義,但是我們的框架要支持靈活的去擴展實現這種信息提示的展示, 2,再提供一套風格的UI,手動關閉toast提示;3,前端出錯了的錯誤堆棧或者位置信息應該有地方可查; |
測試 | |
管理 | 1,后期可考慮加入客戶主動反饋錯誤功能,通過業務人員間接傳遞特別影響技術團隊的口碑;(福春) |
運維實施 | 1,錯誤碼的提示可以直接鏈接到統一的錯誤碼說明頁面,加快實施人員的效率;2. 如果提示不夠,通過traceID能到對應的分布式日志系統查到調用鏈的信息; |
日常開發中如何使用
錯誤碼: 如果是系統內部的,即不對外部的第三方系統開發,錯誤碼使用字符串,可讀性更好;
如果是對外部的第三方系統的,可使用統一的數字編碼,也可使用字符串,根據需要來;
加入你負責一個服務的開發,下面兩種場景是你必須要考慮的。
團隊公用SDK中定義微服務對應的錯誤枚舉
寫法如下:在client包中;
package com.xxx.app.paas.client.exception;
import com.alibaba.cola.dto.ErrorCodeI;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author carter
* create_date 2020/7/7 13:55
* description 應用級別的錯誤碼統一定義
*/
@AllArgsConstructor
public enum ConsoleErrorCodeEnum implements ErrorCodeI {
PARAM_ERR("11010000","參數檢查出錯","請按照輸入提示輸入或者選擇"),
BIZ_CREATE_GATEWAY_ERR("11020001","創建網關出錯","請檢查應用的編碼最好只包含字母和數字"),
BIZ_CREATE_CONFIG_ERR("11020002","創建配置文件出錯","請聯系運維人員檢查應用的配置文件模板在nacos中是否存在"),
SYS_DATABASE_LINK_ERR("11030001","數據庫出錯","請聯系運維人員檢查數據庫的連接信息"),
SYS_NPE_ERR("11030002","空指針錯誤","請聯系開發人員解決問題"),
//已知異常轉換
DIVISOR_CAN_NOT_BE_ZERO("DIVISOR_CAN_NOT_BE_ZERO","除數不能為0","請聯系開發人員檢查你的除數是不是0"),
//安裝部署
INSTALL_ERR_START_PATH("11019000","啟動失敗,無法獲取正確的pid","請輸入正確的包文件路徑或啟動命令有誤"),
INSTALL_ERR_START_FILE("11019001","文件不存在","請確保包文件存在"),
INSTALL_ERR_SAVE_NS("11019002","相同的命名空間不能安裝第二套雲樞","請正確選擇注冊中心的namespace"),
;
private String errorCode;
private String errorDesc;
@Getter
private String correctGuid;
@Override
public String getErrCode() {
return errorCode;
}
@Override
public String getErrDesc() {
return errorDesc;
}
@Override
public String getServiceName() {
return "app-paas";
}
}
如何拋出業務或者系統異常?
統一通過類Exceptions來拋出異常;
com.alibaba.cola.exception.Exceptions
使用實例如下:
package com.xxx.app.paas.controller;
import com.alibaba.cola.exception.Exceptions;
import com.xxx.app.paas.domain.exception.ConsoleErrorCodeEnum;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author carter
* create_date 2020/7/8 11:19
* description 錯誤測試接口
*/
@RestController
public class ErrorCodeControllerI {
//校驗異常
@GetMapping("/error/check_param")
public void checkParamException() {
Assert.isTrue(1 == 2, "xxx參數校驗錯誤");
}
//業務異常
@GetMapping("/error/biz")
public void bizException() {
Exceptions.throwBizException(ConsoleErrorCodeEnum.BIZ_CREATE_CONFIG_ERR);
}
//系統異常
@GetMapping("/error/sys")
public void sysException() {
String a = null;
a.getBytes();
}
//已經識別的系統異常,可以給特定的錯誤提示和錯誤碼
@GetMapping("/error/sys2")
public void sys2Exception() {
try {
int i = 3 / 0;
} catch (Exception exception) {
Exceptions.throwSysException(ConsoleErrorCodeEnum.DIVISOR_CAN_NOT_BE_ZERO, exception);
}
}
}
前端進行預設樣式的提示。
已有的錯誤處理融合
如果已經有自己的錯誤處理了,跟統一異常處理進行融合,融合方式具體情況具體分析。提供統一的擴展方式。
可單獨找 @李福春(lifuchun) 一起看整改方式 。
工程分工和任務跟進
完成標志:在測試環境中提示規范,有價值。
明確指出是哪個服務的什么問題,建議提示一定要准確到位。 測試做驗證。
小結
作為一名程序員,需要站在更高的角度,用產品思維,系統思維,終局思維,商業運營思維去看待出現的痛點問題。
通過從前到后的約定錯誤和異常提示,解決各個崗位對軟件系統排錯性的建設。
下期我直接輸出一個統一的java異常處理的后端SDK樣例。
原創不易,關注誠可貴,轉發價更高!轉載請注明出處,讓我們互通有無,共同進步,歡迎溝通交流。