好久沒有寫文章了,搞得不知道寫啥,最近遇到了一個(gè)問題,決定把東西總結(jié)一下,記錄下來。
1. 背景
最近開發(fā),發(fā)現(xiàn)遇到了一個(gè)問題,那就是關(guān)于如何定義錯誤碼。寫代碼寫著寫著迷糊了,突然發(fā)現(xiàn)不知道如何定義錯誤碼,錯誤碼對我們來說,到底有什么用?今天我結(jié)合自己的實(shí)現(xiàn),來說一下如何定義錯誤碼
2. 處理錯誤的方式
在Spring時(shí)代,spring提供的ControllerAdvice,RestControllerAdvice給我們集中處理異常提供了一種很好的解決方案。我們也經(jīng)常用這樣的方式來解決方案。但是,結(jié)合我們一部分常用的也給我?guī)砹艘徊糠譄?,我來說一下為什么。
2.1 處理方案
在我們自己的代碼中,經(jīng)常會定義一種通用的返回結(jié)果,大多數(shù)都叫Result。代碼如下:
@Data
@AllArgsConstructor
public class Result<T> implements Serializable {
/**
* 狀態(tài)碼
*/
private Object code;
/***
* 說明信息
*/
private String message;
/**
* 是否成功
*/
private boolean success;
/**
* 返回?cái)?shù)據(jù)
*/
private T data;
}
我們經(jīng)常會用這個(gè)對象當(dāng)做接口的公共返回,所以理所當(dāng)然的,在定義公共異常處理邏輯的時(shí)候,也會寫成如下的樣子。代碼如下:
@RestControllerAdvice
@Slf4j
public class DefaultExceptionHandler {
/**
* 業(yè)務(wù)異常統(tǒng)一捕獲
*
* @param req 請求
* @param e 業(yè)務(wù)異常
* @return 對應(yīng)結(jié)果
*/
@ExceptionHandler(value = BizException.class)
public Result<String> businessExceptionHandler(HttpServletRequest req, BizException e) {
// 已知異常不打印堆棧,避免多余的日志IO輸出
return new Result<>(e.getCode(), e.getMessage());
}
}
這是一個(gè)處理公共異常的類,里面也理所當(dāng)然的返回了Result對象,看起來似乎都是天衣無縫,沒什么問題,但是,也給我?guī)砹艘恍├Щ蟆?/p>
2.2 我的困惑
當(dāng)我們遇到異常的時(shí)候,會寫下如下的代碼:
throw new BizException(errorCode, errorMessage);
錯誤碼是我們自己定義的,例如:errorCode=400, errorMsg="session timeout",然后這個(gè)異常會理所當(dāng)然的被我們的公共異常處理器捕獲,然后返回給前端一個(gè)公共的Result,前端拿到狀態(tài)碼來處理相應(yīng)的邏輯,例如:跳轉(zhuǎn)到登錄頁面。這一切看起來都是正常的,但是,有個(gè)但是,如果我們返回的狀態(tài)碼不是400的時(shí)候呢? 或者有其他的狀態(tài)碼的時(shí)候呢?600, 601,602 ,前端都要一一處理嗎?這個(gè)說實(shí)話,看起來怪怪的,處理的要求太多了,前端的改動也太大了。還有一個(gè)問題是每個(gè)請求都返回的HttpStatus都是200,給測試也帶來了一定的煩惱,測試需要認(rèn)真仔細(xì),一一的看你的接口返回的 內(nèi)容,來判斷你的返回內(nèi)容是都正確, 這無疑是增加了許多的工作量。看起來狀態(tài)碼似乎也沒帶來一個(gè)好的效果。
2.3 我的思考
就我目前遇到的情況來說,我覺得錯誤碼是一個(gè)可有可無的東西,因?yàn)槲抑恍枰袛鄐uccess是否為True就好了,因?yàn)楫?dāng)success為false的時(shí)候,都需要拋出message來展示。那我是不是可以隨意定義錯誤碼了呢?如果想要用上錯誤碼,怎么用最合適,最有價(jià)值呢?錯誤碼用來做錯誤接口提示可以嗎?
3. 解開迷惑
對錯誤碼理解少,或者說當(dāng)錯誤直接提示message,或者說想在發(fā)生異常的時(shí)候,通過鏈路id或者其他方式找異常的我們都太天真了。我頓悟是在一張圖上,圖上的內(nèi)容大概如下。
| 接口 | 錯誤時(shí)處理方式 |
|---|---|
| A | 前端不做處理,不允許阻塞下單流程 |
| F | 前端不做處理,不允許阻塞下單流程 |
| G | 前端不做處理,不允許阻塞下單流程 |
| B | 前端做兜里邏輯 |
| W | 前端做兜里邏輯 |
| C | 需要給出錯誤提示 |
看到這個(gè)圖的第一反應(yīng),我覺得需要定義好多錯誤碼,好麻煩,能不能給出一種公共的,然后讓前端好處理一些。并且我想改變我的代碼,不想讓測試費(fèi)勁的看哪個(gè)接口出問題,便于我自己找問題,也便于測試。而且我還想前端基于目前的代碼,不要有太大的改動,就算有改動,沒覆蓋到的地方,在以后的迭代中進(jìn)行修改。
思前想后,想到一個(gè)這樣的處理邏輯,一下子處理的當(dāng)前的困境。
3.1 如何定義錯誤碼
想來想去,覺得這樣的定義錯誤碼蠻合適的,給人一種靠譜的感覺。
錯誤碼公式:系統(tǒng)編碼 + 業(yè)務(wù)編碼 + 錯誤碼 + 接口編碼
3.1.1 系統(tǒng)編碼
系統(tǒng)編碼:系統(tǒng)編碼就是對每個(gè)系統(tǒng)進(jìn)行編碼,如有三個(gè)系統(tǒng)A,B,C,然后我進(jìn)行了編號,分別為: 01,02,03,通過這幾個(gè)數(shù)字,我就可以找到對應(yīng)的系統(tǒng)。
3.1.2 業(yè)務(wù)編碼
業(yè)務(wù)編碼:業(yè)務(wù)編碼就是對應(yīng)具體的業(yè)務(wù),如:我的系統(tǒng)中涉及到了D,E,F(xiàn)三個(gè)業(yè)務(wù),然后我對三個(gè)業(yè)務(wù)也進(jìn)行了編碼,分別是100,200,300,通過這幾個(gè)數(shù)字,我也可以找對對應(yīng)的業(yè)務(wù)。
3.1.3 錯誤碼
錯誤碼就是要根據(jù)具體的情況來了,如下。
public enum SystemStatusEnum implements BaseEnum<Integer> {
/**
* 接口熔斷
*/
HYSTRIX_RETURN_CODE(602, "接口熔斷"),
/**
* 業(yè)務(wù)異常
*/
BUSINESS_ERROR(600, "業(yè)務(wù)異常"),
}
這些就要根據(jù)具體的情況,來定義一些編碼,便于知道當(dāng)出現(xiàn)這些異常的時(shí)候,需要如果做處理。
3.1.4 接口編碼
接口編碼,就是對每個(gè)接口進(jìn)行編碼,如:D業(yè)務(wù)的接口有:m,n兩個(gè)接口。定義一個(gè)接口編碼如下:
/**
* <br>接口編碼</br>
*
* @author fattyca1
* @since 1.0
*/
public class InterfaceCode {
public interface M {
String GET_NAME = "01";
}
public interface B {
String SAY_HELLO = "01";
}
}
通過對具體的接口進(jìn)行錯誤碼定義,就可以通過錯誤碼找到對應(yīng)的接口。
3.2 如果使用錯誤碼
我們光定義好了錯誤碼還是沒有太大作用的,當(dāng)生產(chǎn)環(huán)境出現(xiàn)問題的時(shí)候,不方便我們第一時(shí)間找出出現(xiàn)問題的地方,所以我們需要前端配合起來將錯誤碼展示出來。
如何展示呢? 其實(shí)很簡單,就是通過在msg后邊,把錯誤代碼括號起來。給一個(gè)demo:`系統(tǒng)繁忙,請稍后再試(000160012)` ,我們通過前端展示的錯誤碼`000160012` 就可以知道出現(xiàn)的問題是:00號系統(tǒng),01號業(yè)務(wù),600(系統(tǒng)異常,是兜底,還是放過),01號業(yè)務(wù)的12號接口,這樣,我們就可以快速定位到具體的代碼中去,在配合日志,很快就可以揪出bug出現(xiàn)的地方,是不是很方便?
哈哈, 不知道大家發(fā)現(xiàn)一個(gè)問題沒? 這樣雖然解決了快速定位接口錯誤的問題,但是沒有給測試帶來收益,因?yàn)槟壳皝碚f,接口的返回狀態(tài)都是200,這樣就無法區(qū)分出哪一個(gè)接口是具體的錯誤,所以我們還要針對這一種情況進(jìn)行處理。處理的代碼如下:
/**
* 業(yè)務(wù)異常統(tǒng)一捕獲
*
* @param req 請求
* @param e 業(yè)務(wù)異常
* @return 對應(yīng)結(jié)果
*/
@ExceptionHandler(value = BizException.class)
public ResponseEntity<Result<?>> businessExceptionHandler(HttpServletRequest req, BizException e) {
String errorCode = e.getCode() + "";
// 處理會話超時(shí)情況
if (SystemStatusEnum.SESSION_TIMEOUT.getCode().equals(errorCode)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Result.fail(errorCode, e.getMessage()));
}
// 屬于業(yè)務(wù)異常的系統(tǒng)異常
if (SystemStatusEnum.DEFAULT_ERR_CODE.getCode().equals(errorCode)) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Result.fail(errorCode, e.getMessage()));
}
// 其他異常
errorCode = StringUtils.isBlank(errorCode) ? SystemStatusEnum.DEFAULT_ERR_CODE.getCode() : errorCode;
return ResponseEntity.ok(Result.fail(errorCode, e.getMessage()));
}
我這個(gè)處理業(yè)務(wù)的方法,先說一下為什么要這么做。
- 因?yàn)槭菢I(yè)務(wù)異常,是我們自己拋的,所以不用打印多余的堆棧日志。
- 處理登錄超時(shí),可以給出HttpStatus=401,讓前端請求一目了然,可以針對HttpStatus做出具體處理
- 當(dāng)系統(tǒng)異常的時(shí)候,需要把此次請求HttpStatus=500,前端一目了然。
通過返回ResponseEntity而不是簡單的Result,就解決了測試?yán)щy的情況。 不要通過固定思維,返回Result,局限了自己,致使每次返回的HttpStatus = 200, 使測試情況復(fù)雜化。
4. 總結(jié)
這一次,自己通過總結(jié),算是理解了錯誤碼的具體用法,分享出來,有問題,歡迎大家討論呀!