前言
- 本文說的自定義異常指的是繼承了RuntimeException,專門用於Spring全局異常捕獲處理的自定義異常類型,需要應用程序顯示捕獲並處理的異常不在本次討論范圍內。
- 本隨筆經驗總結,如有不足還原留言指正。
場景
- 使用java提供服務的后端系統,使用者(可能是web前端或者是第三方調用者)通過api形式進行調用。
目的
- 大多時候實現業務流程時總是可以假設所有操作都是成功的,如果有異常,直接根據異常類型轉換為對應的自定義異常從而結束當前邏輯。
- 根據系統異常類型制定對應的異常,以便進行全局異常處理。
- 異常一般伴隨着對應的錯誤碼,所以需要實現不同類型的異常對應不通類型的錯誤碼,不得混用。
對異常進行分類
系統異常可以大致分為客戶端異常和服務端異常,即任何異常都可以轉換為這兩類異常。
- 客戶端異常:如參數錯誤等,建議http返回狀態碼為4xx。
- 服務端異常:如某些在意料之外並且不屬於客戶端異常操作引起的異常,建議http返回狀態碼為5xx。
異常體系設計類圖
類/接口
|
說明
|
---|---|
AbstractException | AbstractException 表示自定義已知異常,即該異常可以完全不用顯示處理,而是可以直接交給全局異常捕獲自動處理,其中它有兩個子類,一個是ClientException和SystemException,這兩個子類分別與上面提到的可能的系統異常想對應。 |
ClientException | ClientException表示"客戶端異常",如接口方法不對或者參數不符合要求等。引發該異常時,除了返回對應的錯誤碼與錯誤描述外,http響應狀態碼為4xx(一般為400)。 |
SystemException | SystemException表示“服務端異常”,如圖片下載失敗導致我們系統本身出現不可控異常以及系統調用通道發生異常等。引發該異常時,除了返回對應的錯誤碼與錯誤描述外,http響應狀態碼為5xx(一般為500)。 |
ClientErrorCodeMark | 創建ClientException異常時所需的錯誤碼抽象,即ClientException異常僅僅接受ClientErrorCodeMark類型的錯誤碼。目的是防止不同類型的錯誤碼混用,即客戶端異常錯誤碼不能用於服務端異常。 |
SystemErrorCodeMark | 創建SystemException異常時所需的錯誤碼抽象,即SystemException異常僅僅接受SystemErrorCodeMark類型的錯誤碼。目的是防止不同類型的錯誤碼混用,即客戶端異常錯誤碼不能用於服務端異常。 |
ClientErrorCodeMark.Code SystemErrorCodeMark.Code |
Code分別是ClientErrorCodeMark和SystemErrorCodeMark的實現類,同時也是枚舉對象。即在這里定義了各個錯誤碼以及錯誤碼對應的描述信息。 |
由以上類圖可以看出,所有接口或者類都實現了ErrorInfo這個接口,而通過該接口可以拿到異常錯誤碼和錯誤描述,也就是說,還是像以前一樣,一旦引發異常,可以隨時中斷處理並且能返回給調用方一個合理的錯誤信息。
具體代碼
/** * 公共接口,能獲取錯誤描述和錯誤碼. * <p> * Created by DaiWang on 2019/7/23. * </p> * * @author DaiWang */ public interface ErrorInfo { /** * 獲取異常對應的錯誤碼. * * @return 錯誤碼系統定義的錯誤碼 */ String getErrorCode(); /** * 獲取錯誤碼對應的描述信息. * <p> * 如果本地對該描述信息進行設置,則本地的優先,否則自動從錯誤碼系統獲取錯誤碼對應的描述信息. * </p> * @return 錯誤碼對應的描述信息 */ String getErrorDesc(); }
/** * 自定義全局異常. * @author DaiWang */ public abstract class AbstractException extends RuntimeException implements ErrorInfo { public AbstractException() { super(); } public AbstractException(String message) { super(message); } public AbstractException(String message, Throwable cause) { super(message, cause); } public AbstractException(Throwable cause) { super(cause); } protected AbstractException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }

/** * 外部傳參數非法等客戶端異常. * <p>即該異常由調用方導致.如調用通道時用戶傳遞的圖片過大等. * <p>直接提示用戶相關錯誤即可 * <p> * Created by DaiWang on 2019/7/23. * </p> * * @author DaiWang */ public class ClientException extends AbstractException { private ErrorInfo errorCode; /** * 本地錯誤描述信息,其優先級高於{@link ErrorInfo}中的errorDesc,即如果該值不為空,則以該值為准 */ @Setter private String errorDesc; /** * @param errorCode 枚舉的錯誤碼類型,其中包含了錯誤碼和對應的錯誤碼描述信息. */ public ClientException(ClientErrorCodeMark errorCode) { super(); this.errorCode = errorCode; } /** * @param errorCode 枚舉的錯誤碼類型,其中包含了錯誤碼和對應的錯誤碼描述信息. * @param message 最好是自定義的詳細描述信息,用於打日志,也可以和{@link ErrorInfo#getErrorDesc()}相同 */ public ClientException(ClientErrorCodeMark errorCode, String message) { super(message); this.errorCode = errorCode; } /** * @param errorCode 枚舉的錯誤碼類型,其中包含了錯誤碼和對應的錯誤碼描述信息. */ public ClientException(ClientErrorCodeMark errorCode, Throwable cause) { super(cause); this.errorCode = errorCode; } /** * @param errorCode 枚舉的錯誤碼類型,其中包含了錯誤碼和對應的錯誤碼描述信息. * @param message 最好是自定義的詳細描述信息,用於打日志,也可以和{@link ErrorInfo#getErrorDesc()}相同 */ public ClientException(ClientErrorCodeMark errorCode, String message, Throwable cause) { super(message, cause); this.errorCode = errorCode; } @Override public String getErrorCode() { return errorCode.getErrorCode(); } /** * 獲取錯誤碼對應的描述信息. * <p> * 如果本地對該描述信息進行設置,則本地的優先,否則自動從錯誤碼系統獲取錯誤碼對應的描述信息. * </p> * @return 錯誤碼對應的描述信息 */ @Override public String getErrorDesc() { if (!StringUtils.isEmpty(errorDesc)) { return errorDesc; } return errorCode.getErrorDesc(); } @Override public String getMessage() { String message = super.getMessage(); if (StringUtils.isEmpty(message)) { message = this.getErrorDesc(); } return message; } }

/** * 客戶端異常標記接口。 * 用於標記{@link ClientException}對象的錯誤碼參數類型,防止錯誤碼串用. * @author DaiWang */ public interface ClientErrorCodeMark extends ErrorInfo { /** * 用戶操作異常錯誤碼枚舉 */ @Getter enum Code implements ClientErrorCodeMark { // region 參數錯誤 /** * 參數錯誤(可以籠統的將所有參數錯誤都包含在內,並重新定義錯誤描述,一般用於框架參數自定義錯誤驗證) */ PARAMETER_ERROR("錯誤碼1", "參數錯誤"), // region 操作過於頻繁 錯誤碼前綴: BOX_GA12xx FREQUENTLY("錯誤碼2", "操作過於頻繁,請稍候再試"), // endregion // ---------------------------------------------- /** * 照片質量校驗不合格,請重新拍照后重試 */ BOX_GA0051("BOX_GA0051"), ; Code(String errorCode) { this.errorCode = errorCode; this.errorDesc = AssemblyUtil.getErrorOutDesc(errorCode); } Code(String errorCode, String errorDesc) { this.errorCode = errorCode; this.errorDesc = errorDesc; } /** * 錯誤碼. */ private String errorCode; /** * 錯誤碼對應的外部描述信息,該信息是通過錯誤碼自動獲取,並且直接返回給調用方. */ private String errorDesc; } /** * 快速構建一個自定義錯誤描述錯誤信息對象. * <p> * 一般用於相同的錯誤碼,但是不同的錯誤描述信息(類似). * 比如常見的參數錯誤,但是可以使用參數錯誤的通用錯誤碼在描述中提示調用者更具體的錯誤描述. * </p> * @param desc 新的錯誤描述 * @return 使用自定義描述替換原有描述的信息對象副本 */ default ClientErrorCodeMark as(String desc) { ErrorInfo errorInfo = this; return new ClientErrorCodeMark() { @Override public String getErrorCode() { return errorInfo.getErrorCode(); } @Override public String getErrorDesc() { return desc; } }; } }

/** * 轉換后的系統異常. * <p> * 系統環境異常、數據庫連接超時. * <p> * 提示用戶系統異常,並且需要人工排查. * <p> * Created by DaiWang on 2019/7/23. * </p> * * @author DaiWang */ public class SystemException extends AbstractException { private ErrorInfo errorCode; /** * 本地錯誤描述信息,其優先級高於{@link ErrorInfo}中的errorDesc,即如果該值不為空,則以該值為准 */ @Setter private String errorDesc; /** * @param errorCode 枚舉的錯誤碼類型,其中包含了錯誤碼和對應的錯誤碼描述信息. */ public SystemException(SystemErrorCodeMark errorCode) { super(); this.errorCode = errorCode; } /** * @param errorCode 枚舉的錯誤碼類型,其中包含了錯誤碼和對應的錯誤碼描述信息. * @param message 最好是自定義的詳細描述信息,用於打日志,也可以和{@link ErrorInfo#getErrorDesc()}相同 */ public SystemException(SystemErrorCodeMark errorCode, String message) { super(message); this.errorCode = errorCode; } /** * @param errorCode 枚舉的錯誤碼類型,其中包含了錯誤碼和對應的錯誤碼描述信息. */ public SystemException(SystemErrorCodeMark errorCode, Throwable cause) { super(cause); this.errorCode = errorCode; } /** * @param errorCode 枚舉的錯誤碼類型,其中包含了錯誤碼和對應的錯誤碼描述信息. * @param message 最好是自定義的詳細描述信息,用於打日志,也可以和{@link ErrorInfo#getErrorDesc()}相同 */ public SystemException(SystemErrorCodeMark errorCode, String message, Throwable cause) { super(message, cause); this.errorCode = errorCode; } @Override public String getErrorCode() { return errorCode.getErrorCode(); } /** * 獲取錯誤碼對應的描述信息. * <p> * 如果本地對該描述信息進行設置,則本地的優先,否則自動從錯誤碼系統獲取錯誤碼對應的描述信息. * </p> * @return 錯誤碼對應的描述信息 */ @Override public String getErrorDesc() { if (!StringUtils.isEmpty(errorDesc)) { return errorDesc; } return errorCode.getErrorDesc(); } @Override public String getMessage() { String message = super.getMessage(); if (StringUtils.isEmpty(message)) { message = this.getErrorDesc(); } return message; } }

/** * 標記接口. * 用於標記{@link SystemException}對象的錯誤碼參數類型,防止錯誤碼串用. * @author DaiWang */ public interface SystemErrorCodeMark extends ErrorInfo { /** * 我方系統異常錯誤碼枚舉 */ @Getter @AllArgsConstructor enum Code implements SystemErrorCodeMark { /** * 系統錯誤(本系統). */ SYSTEM_ERROR_LOCAL("錯誤碼3", "系統異常"), /** * 系統內部錯誤(其他服務引起的錯誤,如通道異常) */ SYSTEM_ERROR_OTHER("錯誤碼4", "系統繁忙"), // ----------------------------------------------------------------------------------------------------------------- /** * 系統內部錯誤 */ SYS_0001("錯誤碼5"), /** * API不存在 */ SYSTEM_NOT_API("錯誤碼6" , "API不存在"), /** * API不存在 */ HTTP_NOT_SUPPORTED("錯誤碼7" , "HTTP請求方法不支持"), ; Code(String errorCode) { this.errorCode = errorCode; this.errorDesc = AssemblyUtil.getErrorOutDesc(errorCode); } /** * 錯誤碼. */ private String errorCode; /** * 錯誤碼對應的外部描述信息,該信息是通過錯誤碼自動獲取,並且直接返回給調用方. */ private String errorDesc; } /** * 快速構建一個自定義錯誤描述錯誤信息對象. * <p> * 一般用於相同的錯誤碼,但是不同的錯誤描述信息(類似). * 比如常見的參數錯誤,但是可以使用參數錯誤的通用錯誤碼在描述中提示調用者更具體的錯誤描述. * </p> * @param desc 新的錯誤描述 * @return 使用自定義描述替換原有描述的信息對象副本 */ default SystemErrorCodeMark as(String desc) { ErrorInfo errorInfo = this; return new SystemErrorCodeMark() { @Override public String getErrorCode() { return errorInfo.getErrorCode(); } @Override public String getErrorDesc() { return desc; } }; } }