程式設計師你是如何解決軟體系統的易排錯性?

李福春發表於2021-11-30

希望大家可以收穫:

1,背景分析是否貼合工作的實際場景,能否觸及痛點;

2,統一的技術方案,並演示最終的實現效果;

3,前端和後端相對完整的技術實現方案,系統的思考方式;

背景和需求

不同人群對錯誤處理的期望不同:這裡基於業務系統簡單列表彙總;

人群 錯誤提示的期望
業務系統產品經理 錯誤提示也是產品設計的一部分,標識正常業務的邊界,基於錯誤提示可以快速的進行業務功能的邊界條件,關鍵流程流向提示;
業務系統測試人員 能定提示到是到底是前端還是後端的問題,快速的分類bug,指派給對應的開發人員;進行需求的二次確認,一些引數邊界的提示資訊必須符合產品規約。
業務系統前端開發人員 聯調的時候,後端的錯誤可以提示哪裡出錯了,如果是引數錯誤,讓我指引我哪個引數錯了,我好調整如果是後端邏輯或者內部錯誤,方便我提供截圖和traceId給到後端開發,讓後端去解決;
業務系統運維人員 後端資源耗盡了,最好可以提示我哪塊資源不足,如何補充;中介軟體有問題了,告知我哪個中介軟體,建議的運維方法;如果實在無法在介面上告訴我,可以快速看到對應的應用日誌,丟回給開發去進一步定位問題。
業務系統後端開發人員 開發和整合測試環境,最好在介面上或者控制檯能看到堆疊資訊,哪行程式碼出錯了;最次也要能從介面或者控制檯,或者抓包中找到traceId,方便我從日誌中或者呼叫鏈跟蹤系統中快速的定位問題,方便快速解決問題;
業務系統管理層 可服務性好,站在使用者的角度,希望有規範的提示和回到正確流程的提示;站在客戶方的二開或者整合工程師角度,希望錯誤碼能統一,並且對提示,方便我快速整合和二開;站在開發週期來說,希望錯誤提示可以加快前後端聯調,測試的工作效率;
架構師 錯誤處理公共元件化,兼顧開發期的可擴充套件性,複用性,易用性,以及兼顧執行期的可服務性;
二開使用者(業務系統B端-開發人員) 我要錯誤編碼,還要指導提示,最好在本介面中返回給我,或者指引我一個文件,我按照編碼去查;能加速我快速的整合或者二開;
使用者:業務系統B-C端使用者 告訴我哪裡出錯了,正確的使用方法,讓我可以回到正確的流程;最好還能顯示級別;提示不能為空,不能有英文,不能有堆疊資訊,不能有我看不懂的資訊
客戶:業務系統B端應用配置人員 同C端使用者,主要是告訴我哪裡操作錯了,讓我可以回到正確的流程中;

下面進行抽象和彙總。

一個合適的錯誤處理方案應該是怎樣的?

file

統一技術方案

位置 處理要點 說明
前端 前端實現axios攔截器異常捕獲,封裝元件實現,展示邏輯&形式 原則:服務端能響應的、能返回錯誤的,提示語使用後端返回服務端不能響應的、不能返回錯誤的,提示語使用前端約定
後端 對rest介面進行統一異常的捕獲並轉換為錯誤碼,錯誤訊息;對直接組裝的統一錯誤碼,錯誤訊息,進行統一的管理,按照微服務進行錯誤碼進行封裝;封裝為元件形式,錯誤碼按照介面的規約進行限制,應用級別的錯誤碼和錯誤訊息分散在微服務中; 錯誤分兩種形式:1,通過異常輸出錯誤;2,通過組裝錯誤碼和錯誤訊息拼裝錯誤返回資訊;異常分為3類:1,引數校驗或者介面url資源定位不到,需要提示前端調整;2,內部的邏輯錯誤或者jvm異常,通過RuntimeException丟擲;3,依賴的公共元件錯誤,給出環境問題或者呼叫問題的提示;

後端

形式: 中介軟體的方式,定義暴露的配置屬性,對異常進行統一的處理封裝;

file

這裡做一下調整,統一把分散在微服務裡面錯誤碼列舉放到團隊公共的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上傳檔案超出大小限制異常

直接拼接錯誤碼返回

file

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

異常的知識補充:

file

Exception: 可以預見到的異常情況,應該被捕獲或者處理,在java中,分為檢查異常(編譯期)和不檢查異常(執行期)。

Error: 出現了錯誤系統不能正常執行或者恢復,一般情況不容易發生;

共同點:都繼承自Throwable,在java中只有Throwable的子類可以被catch或者throw;

ERROR一般是後端服務掛了,一般無法恢復,提示501 服務不可用或者404 ;

Throwable 異常的基類,一般不直接處理;

重點處理的Exception和RuntimeException

異常分類 說明
CheckedException 檢查型異常, 一般直接繼承Exception,(RuntimeException除外),需要顯示的try-catch 否則編譯報錯
RuntimeException 執行時異常 程式執行過程中發生的異常,編譯器無法提前發現,一般的業務異常都是執行時異常;

在處理異常的時候,有4個基本規則需要注意:

  1. 不要catch 最普遍的Exception ,而應該優先捕獲具體的異常,可以留下足夠的診斷資訊;
  2. 不要生吞異常,應該嘗試丟擲或者寫到日誌,否則無法判斷異常發生的位置;
  3. 不要使用e.printStackTrace(),在分散式系統中,無法確定輸出到了什麼位置,應該輸出到日誌中;
  4. 提早丟擲,晚點捕獲;提高效率

自定義異常的時候需要注意兩點:
1,儘量不要定義檢查異常
2,異常需要保留足夠的診斷資訊,但是也需要脫敏;

新業務系統錯誤碼統一管理

  1. 按照微服務統一的規範列舉統一管理錯誤碼,錯誤資訊,並填充建議操作資訊,可通過共同介面進行規範;

比如design微服務定義微服務級別的錯誤碼列舉 需要實現 ErrorCodeI介面,填充服務名稱,錯誤碼,錯誤提示資訊,正確操作指引資訊;

public interface ErrorCodeI {
    /**
     * 錯誤碼
     * @return
     */
    String getErrCode();

    /**
     * 錯誤描述
     * @return
     */
    String getErrDesc();

    /**
     * 獲取微服務的名稱
     * @return
     */
    default String getServiceName(){
        return null;
    }

    /**
     * 獲取恢復錯誤的正確指導
     * @return
     */
    default String getCorrectGuid(){
        return null;
    }
}
  1. 收縮自定義的errcode,errmessage到對應的列舉中,進行統一的編碼和錯誤資訊配置;

  2. 閘道器提供介面和前端頁面,展示所有的錯誤碼和錯誤資訊,建議處理方法; 作為一個補充的查詢建議操作的方案;(可在閘道器匯聚所有的微服務中的錯誤碼資訊,並展示出來)

新業務系統異常體系分級分類

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

  1. 後端返回資料格式
    | 欄位 | 說明 |
    | --- | --- |
    | errCode | 錯誤碼 |
    | errMessage | 提示訊息 |
    | data | 返回結果 |
    | traceId | skywalking的跟蹤ID |

  2. 前端實現axios攔截器異常捕獲,封裝元件實現,展示邏輯&形式

file

元件文件: 部署到伺服器上再統一放開

file

更多的需求點

崗位角度 需求補充
後端 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樣例。

原創不易,關注誠可貴,轉發價更高!轉載請註明出處,讓我們互通有無,共同進步,歡迎溝通交流。

相關文章