希望大家可以收穫:
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樣例。
原創不易,關注誠可貴,轉發價更高!轉載請註明出處,讓我們互通有無,共同進步,歡迎溝通交流。