程序員你是如何解決軟件系統的易排錯性?


希望大家可以收獲:

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樣例。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM