異常處理的最佳實(shí)踐

針對(duì)異常的一般操作
轉(zhuǎn)換,即轉(zhuǎn)換新的異常拋出。對(duì)于新拋出的異常,最好具有特定的分類(lèi)和明確的異常消息,而不是隨便拋一個(gè)無(wú)關(guān)或沒(méi)有任何信息的異常,并最好通過(guò) cause 關(guān)聯(lián)老異常。

重試,即重試之前的操作。比如遠(yuǎn)程調(diào)用服務(wù)端過(guò)載超時(shí)的情況,盲目重試會(huì)讓問(wèn)題更嚴(yán)重,需要考慮當(dāng)前情況是否適合重試。

恢復(fù),即嘗試進(jìn)行降級(jí)處理,或使用默認(rèn)值來(lái)替代原始數(shù)據(jù)


一些錯(cuò)誤示例

:不應(yīng)該用 AOP 對(duì)所有方法進(jìn)行統(tǒng)一異常處理,異常要么不捕獲不處理,要么根據(jù)不同的業(yè)務(wù)邏輯、不同的異常類(lèi)型進(jìn)行精細(xì)化、針對(duì)性處理

IllegalArgumentException: 入?yún)㈠e(cuò)誤,比如參數(shù)類(lèi)型int輸入string。
IllegalStateException: 狀態(tài)錯(cuò)誤,比如訂單已經(jīng)支付完成,二次請(qǐng)求支付接口。
UnsupportedOperationException: 不支持操作錯(cuò)誤,比如對(duì)一筆不能退款的訂單退款。
其他異常
SecurityException: 權(quán)限錯(cuò)誤,比如未登陸用戶(hù)調(diào)用修改用戶(hù)信息接口。

image.png

每層架構(gòu)的工作性質(zhì)不同,且從業(yè)務(wù)性質(zhì)上異常可能分為業(yè)務(wù)異常和系統(tǒng)異常兩大類(lèi),這就決定了很難進(jìn)行統(tǒng)一的異常處理。我們從底向上看一下三層架構(gòu):

Repository 層出現(xiàn)異?;蛟S可以忽略,或許可以降級(jí),或許需要轉(zhuǎn)化為一個(gè)友好的異常。如果一律捕獲異常僅記錄日志,很可能業(yè)務(wù)邏輯已經(jīng)出錯(cuò),而用戶(hù)和程序本身完全感知不到。

Service 層往往涉及數(shù)據(jù)庫(kù)事務(wù),出現(xiàn)異常同樣不適合捕獲,否則事務(wù)無(wú)法自動(dòng)回滾。此外 Service 層涉及業(yè)務(wù)邏輯,有些業(yè)務(wù)邏輯執(zhí)行中遇到業(yè)務(wù)異常,可能需要在異常后轉(zhuǎn)入分支業(yè)務(wù)流程。如果業(yè)務(wù)異常都被框架捕獲了,業(yè)務(wù)功能就會(huì)不正常。

如果下層異常上升到 Controller 層還是無(wú)法處理的話,Controller 層往往會(huì)給予用戶(hù)友好提示,或是根據(jù)每一個(gè) API 的異常表返回指定的異常類(lèi)型,同樣無(wú)法對(duì)所有異常一視同仁。因此,我不建議在框架層面進(jìn)行異常的自動(dòng)、統(tǒng)一處理,尤其不要隨意捕獲異常。但,框架可以做兜底工作。如果異常上升到最上層邏輯還是無(wú)法處理的話,可以以統(tǒng)一的方式進(jìn)行異常轉(zhuǎn)換,比如通過(guò) @RestControllerAdvice + @ExceptionHandler,來(lái)捕獲這些“未處理”異常:


對(duì)于自定義的業(yè)務(wù)異常,以 Warn 級(jí)別的日志記錄異常以及當(dāng)前 URL、執(zhí)行方法等信息后,提取異常中的錯(cuò)誤碼和消息等信息,轉(zhuǎn)換為合適的 API 包裝體返回給 API 調(diào)用方;

對(duì)于無(wú)法處理的系統(tǒng)異常,以 Error 級(jí)別的日志記錄異常和上下文信息(比如 URL、參數(shù)、用戶(hù) ID)后,轉(zhuǎn)換為普適的“服務(wù)器忙,請(qǐng)稍后再試”異常信息,同樣以 API 包裝體返回給調(diào)用方

比如,下面這段代碼的做法

@RestControllerAdvice
@Slf4j
public class RestControllerExceptionHandler {
        private static int GENERIC_SERVER_ERROR_CODE = 2000;
        private static String GENERIC_SERVER_ERROR_MESSAGE = "服務(wù)器忙,請(qǐng)稍后再試";

        @ExceptionHandler
        public APIResponse handle(HttpServletRequest req, HandlerMethod method, Exception ex) {
            if (ex instanceof BusinessException) {
                BusinessException exception = (BusinessException) ex;
                log.warn(String.format("訪問(wèn) %s -> %s 出現(xiàn)業(yè)務(wù)異常!", req.getRequestURI(), method.toString()), ex);
                return new APIResponse(false, null, exception.getCode(), exception.getMessage());
            } else {
                log.error(String.format("訪問(wèn) %s -> %s 出現(xiàn)系統(tǒng)異常!", req.getRequestURI(), method.toString()), ex);
                return new APIResponse(false, null, GENERIC_SERVER_ERROR_CODE, GENERIC_SERVER_ERROR_MESSAGE);
            }
        }
    }

出現(xiàn)運(yùn)行時(shí)系統(tǒng)異常后,異常處理程序會(huì)直接把異常轉(zhuǎn)換為 JSON 返回給調(diào)用方:


image.png

:其次,處理異常應(yīng)該杜絕生吞,并確保異常棧信息得到保留;
:小心異常被覆蓋


1.異常生吞
說(shuō)明:

在任何時(shí)候,我們捕獲了異常都不應(yīng)該生吞,也就是直接丟棄異常不記錄、不拋出。這樣的處理方式還不如不捕獲異常,因?yàn)楸簧痰舻漠惓R坏?dǎo)致 Bug,就很難在程序中找到蛛絲馬跡,使得 Bug 排查工作難上加難。


2.丟棄異常的原始信息
錯(cuò)誤示例1

private void readFile() throws IOException { 
Files.readAllLines(Paths.get("a_file"));
}

說(shuō)明
像這樣調(diào)用 readFile 方法,捕獲異常后,完全不記錄原始異常,直接拋出一個(gè)轉(zhuǎn)換后異常,導(dǎo)致出了問(wèn)題不知道 IOException 具體是哪里引起的:


錯(cuò)誤示例2

public void wrong1(){
 try { 
readFile(); 
} catch (IOException e) {
 //原始異常信息丟失 
throw new RuntimeException("系統(tǒng)忙請(qǐng)稍后再試");
 }
}

說(shuō)明
像這樣調(diào)用 readFile 方法,捕獲異常后,完全不記錄原始異常,直接拋出一個(gè)轉(zhuǎn)換后異常,導(dǎo)致出了問(wèn)題不知道 IOException 具體是哪里引起的:


錯(cuò)誤示例3

catch (IOException e) { 
//只保留了異常消息,棧沒(méi)有記錄 
log.error("文件讀取錯(cuò)誤, {}", e.getMessage()); 
throw new RuntimeException("系統(tǒng)忙請(qǐng)稍后再試");
}

說(shuō)明
只記錄了異常消息,卻丟失了異常的類(lèi)型、棧等重要信息:

修改方式

catch (IOException e) { 
throw new RuntimeException("系統(tǒng)忙請(qǐng)稍后再試", e);
}

如果需要重新拋出異常的話,請(qǐng)使用具有意義的異常類(lèi)型和異常消息。


錯(cuò)誤示例4

public void wrong() { 
try { 
log.info("try"); 
//異常丟失 
throw new RuntimeException("try"); 
} finally { 
log.info("finally"); 
throw new RuntimeException("finally");
 }
}

說(shuō)明
try里面的異常被finally里面的異常覆蓋了

修復(fù)方式
把 try 中的異常作為主異常拋出,使用 addSuppressed 方法把 finally 中的異常附加到主異常上:

public void right2() throws Exception {
        Exception e = null;
        try {
            log.info("try");
            throw new RuntimeException("try");
        } catch (Exception ex) {
            e = ex;
        } finally {
            log.info("finally");
            try {
                throw new RuntimeException("finally");
            } catch (Exception ex) {
                if (e != null) {
                    e.addSuppressed(ex);
                } else {
                    e = ex;
                }
            }
        }
        throw e;
    }

備注
對(duì)于實(shí)現(xiàn)了 AutoCloseable 接口的資源,建議使用 try-with-resources 來(lái)釋放資源,否則也可能會(huì)產(chǎn)生剛才提到的,釋放資源時(shí)出現(xiàn)的異常覆蓋主異常的問(wèn)題。


確保正確處理了線程池中任務(wù)的異常
如果任務(wù)通過(guò) execute 提交,那么出現(xiàn)異常會(huì)導(dǎo)致線程退出,大量的異常會(huì)導(dǎo)致線程重復(fù)創(chuàng)建引起性能問(wèn)題,我們應(yīng)該盡可能確保任務(wù)不出異常,同時(shí)設(shè)置默認(rèn)的未捕獲異常處理程序來(lái)兜底;
如果任務(wù)通過(guò) submit 提交意味著我們關(guān)心任務(wù)的執(zhí)行結(jié)果,應(yīng)該通過(guò)拿到的 Future 調(diào)用其 get 方法來(lái)獲得任務(wù)運(yùn)行結(jié)果和可能出現(xiàn)的異常,否則異??赡芫捅簧?/strong>了。

修復(fù)方式
1.以 execute 方法提交到線程池的異步任務(wù),最好在任務(wù)內(nèi)部做好異常處理;

new ThreadFactoryBuilder().
                setNameFormat(prefix + "%d").
                setUncaughtExceptionHandler((thread, throwable) -> 
                        log.error("ThreadPool {} got exception", thread, throwable))
                .get()

2.設(shè)置自定義的異常處理程序作為保底,比如在聲明線程池時(shí)自定義線程池的未捕獲異常處理程序
設(shè)置全局的默認(rèn)未捕獲異常處理程序

static { Thread.setDefaultUncaughtExceptionHandler((thread, throwable)-> 
                log.error("Thread {} got exception", thread, throwable));
        }

異常棧是跟據(jù)當(dāng)前上下文生成,不可用static定義異常,否則會(huì)導(dǎo)致異常棧固化,從而影響問(wèn)題定位(認(rèn)為是走到其他分支了)。

錯(cuò)誤示例

public class Exceptions {
        public static BusinessException ORDEREXISTS = new BusinessException("訂單已經(jīng)存在", 3001);
    }

說(shuō)明
把異常定義為靜態(tài)變量會(huì)導(dǎo)致異常信息固化,這就和異常的棧一定是需要根據(jù)當(dāng)前調(diào)用來(lái)動(dòng)態(tài)獲取相矛盾。

修復(fù)方式

public class Exceptions { 
        public static BusinessException orderExists(){
            return new BusinessException("訂單已經(jīng)存在", 3001); 
        }
    }

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容