1. 程式人生 > >JAVA異常的最佳工程學實踐探索

JAVA異常的最佳工程學實踐探索

此文已由作者佔金武授權網易雲社群釋出。

歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。

先說明一下背景:

  • 專案日誌中的Exception會被哨兵統一監控並報警

  • 比較多的專案基於dubbo在做服務化


表單引數校驗中異常使用的建議

異常機制存在的一個最大好處是讓JAVA函式實現了“多返回值”,比如:

public int caculate(int a, int b) throws MyException {
}

這段程式碼的本質是讓函式caculate擁有了這樣一個返回值[int, MyException],這樣做有什麼好處呢?

假設不使用異常,上面的函式只能用-1、-2這類魔法數來表達異常情況,這樣做會比較糟糕,因為使用這個函式的人必須非常小心地去處理返回值裡的這些魔法數,而時常這是一件容易遺漏的事。

這樣看來,在進行入參檢驗的時候,發現不合法引數而返回IllegalArgumentException是非常合理且自然的用法。

結合一下web表單場景,假設這裡是對使用者輸入引數的校驗,後臺校驗不合法,由MVC的Controller層統一彙總封裝返回給前端會是一種比較優雅的做法,而前端要做的是配合後端的返回的資料結構把有用的錯誤資訊展示給使用者。再結合前面提到的背景,這裡出現的異常不應該列印堆疊日誌,否則會造成哨兵誤報(之所以說是誤報是因為這是一種常見情況不應該引起運維人員的注意並介入處理),建議的做法是記錄相應的異常日誌。

這裡我們有必要再思考清楚一些,如果沒有哨兵報警誤報的問題,我們是否有必要列印堆疊日誌呢?一般而言,列印異常堆疊是為了幫助運維人員(或開發人員)迅速定位異常原因,進而修復異常。而這裡的場景其實是不需要運維人員介入的,由前端頁面提示給使用者,使用者調整相應的引數後重新發起請求即可恢復。

說得有點囉嗦,但其實是為了更清楚的強調這樣一個觀點:使用異常並不代表一定要把堆疊打印出來,比如web表單入參的檢測


dubbo介面中異常使用的建議

先丟擲幾個dubbo異常相關的常見問題:


  • dubbo provider方法的實現底層使用了自定義的XXRuntimeException,在api jar中並未包含此XXRuntimeException定義,consumer呼叫發現無法識別XXRuntimeException,提示“Got unchecked and undeclared exception...”

  • dubbo provider方法的實現底層使用了IllegalArgumentException,consumber呼叫產生IllegalArgumentException,而provider並未發現自己系統產生了這些異常(比較典型的情況是provider的資料庫連線異常),也沒有相應的監控,只能等到consumber來投訴。


以上兩個問題的產生與 dubbo 的實現有關,來看看 dubbo 是怎麼處理異常的(ExceptionFilter):


Result result = invoker.invoke(invocation);if (result.hasException() && GenericService.class != invoker.getInterface()) {    try {
        Throwable exception = result.getException();        // 如果是checked異常,直接丟擲
        if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) {            return result;
        }        // 在方法簽名上有宣告,直接丟擲
        try {
            Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
            Class<?>[] exceptionClassses = method.getExceptionTypes();            for (Class<?> exceptionClass : exceptionClassses) {                if (exception.getClass().equals(exceptionClass)) {                    return result;
                }
            }
        } catch (NoSuchMethodException e) {            return result;
        }        // 未在方法簽名上定義的異常,在伺服器端列印ERROR日誌
        logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()
                + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
                + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);
...


從以上實現可以看出,如果unchecked異常未顯示宣告,則會自動列印error日誌。


那該如何優雅地解決呢?


  • dubbo介面要避免向外丟擲RuntimeException(不僅僅是為了避免擾人的error提醒,更為了避免異常洩漏)。建議的一種方法是在介面實現程式碼的最外層統一使用類似以下示例的方式進行包裝:


Response r;try {
    r = ...;
} catch (RuntimeException e) {
    logger.error("error msg", e);//相當於單個專案下異常處理的最外層,需要把異常記錄下來
    r = Response.buildErrorResponse(e.getMessage());
}return r;


  • 如果一定要使用異常來表達介面語義,使用Checked Exception


使用異常 vs 使用null

前面已經提到,異常的使用會帶來編碼的便利,但同時也為更多的無用日誌輸出埋下了隱患。就上面包裝程式碼的例子,如果RuntimeException是網路連線或者資料庫連線異常倒還好,屬於有用的異常,但如果是因為某項資料不存在而丟擲IllegalArgumentException,則日誌就顯得有點多餘了(無法根據日誌內容採取有效的行動來阻止)。


public Permission loadPermission(Long userId) {    if(...) {        return ...
    } else {        throw new IllegalArgumentException();// or throw new NotFoundException();
    }
}


這種寫法下,loadPermission需要 try{...}catch(){...}的額外‘照應’才得處理得當,是不是有點繁瑣呢?此時直接返回null可能來得更加直接呢?出錯了和沒有其實是兩回事,應該仔細斟酌。

所以,這裡我給出的建議是:如果能簡單方便地避免使用異常,則避免之。


異常統一處理的建議

前面提到了:與前端互動時的異常處理、dubbo介面中的異常處理,其中都提到了一點:執行時異常建議統一處理(Checked異常已經強制由程式設計師進行處理了)。擴充套件一下,還有哪些異常需要統一處理呢?是以什麼樣的維度進行統一呢?我理解可以圍繞執行緒用途進行聚合處理。常見的WEB系統中一般有以下幾類執行緒在執行:


  • 主執行緒(Main函式,異常交由JVM處理)

  • HttpRequest執行緒(一般由Controller提供的hook方法一處理)

  • 非同步任務執行緒池中的工作執行緒(一般由執行緒池提供hook介面方法進行統一處理)

  • dubbo執行緒(由ExceptionFilter進行統一處理)


以上思路理清以後,大家就可以參照進行異常的統一處理了。

Spring MVC:

@ExceptionHandlerpublic Object exception(Exception exception, HttpServletRequest request, HttpServletResponse response) {    return ExceptionUtil.processException(request, response, exception, logger);
}


執行緒池:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(11, 100, 1, TimeUnit.MINUTES, //   
        new ArrayBlockingQueue<Runnable>(10000),//   
        new DefaultThreadFactory()) {   
    protected void afterExecute(Runnable r, Throwable t) {   
        super.afterExecute(r, t);   
        printException(r, t);   
    }   
};


dubbo:

public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
    Result result = invoker.invoke(invocation);    if(result.hasException()) {
        printException(result.getException());
    }    return result;
}


結束語

網際網路上關於JAVA異常使用和最佳實踐的文章比較多,大多流於理論,缺乏對實際工作的指導意義,缺少對常見應用場景如web層、dubbo介面層的實踐討論。本文試圖結合實際應用描述異常使用場景,展開了工程學上的最佳異常實踐探索。由於水平有限,內容難免出現理解上的偏差,還請大家批評指正。


免費體驗雲安全(易盾)內容安全、驗證碼等服務

更多網易技術、產品、運營經驗分享請點選




相關文章:
【推薦】 訊息中介軟體客戶端消費控制實踐
【推薦】 分散式儲存系統可靠性系列二:系統估算示例