天天看點

踩坑日記(異步線程異常消失 & RequestContextHolder)

在一個SpringMVC項目裡,通常我們可以使用如下代碼來擷取和目前線程綁定的HttpServletRequest對象:

最近在公司的項目開發中,子產品A負責統計業務資料并發送郵件,其接口,提供給一個排程器子產品通過定時任務去調用。排程器子產品那邊,要求被排程子產品的接口“立即傳回”。也就是說,當滿足觸發條件,排程器發起對子產品A的調用,子產品A應立即做出響應,并以異步的方式執行任務,在任務執行完後,再通過一個回調接口,告知排程器某某任務已完成。

一開始我沒有注意需要“立即傳回”,接口的處理采用的是同步的方式,于是,在調用子產品A的資料統計接口時,非常耗時,排程器遲遲收不到子產品A的響應,于是認為此次任務失敗,并觸發了失敗重試,不斷地對子產品A的資料統計接口發出調用。最後導緻子產品A的資料統計接口被調用多次。

後來我改為異步處理,将資料統計的耗時操作放在

CompletableFuture.runAsync

中。接着出現了錯誤,資料報表的郵件沒有發送出來,看日志也沒有任何錯誤記錄。最後在本地調試時,一步一步debug,發現程式在一個擷取上下文Request對象的地方,戛然而止。就是下面這行代碼。在程式執行到這段代碼時,就停止了,之後的代碼也沒有被執行,日志也沒有錯誤資訊輸出。

我打開debug模式,跟到這個方法的内部,發現擷取不到目前上下文的Request對象,并走到了抛出異常的代碼塊

/** RequestContextHolder.class**/
public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
        RequestAttributes attributes = getRequestAttributes();
        if (attributes == null) {
            if (jsfPresent) {
                attributes = RequestContextHolder.FacesRequestAttributesFactory.getFacesRequestAttributes();
            }

            if (attributes == null) {
                throw new IllegalStateException("No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.");
            }
        }

        return attributes;
    }
           

按理說這個運作時異常被抛了出來,JVM應該會捕捉到并列印出其錯誤資訊,但是這個異常并沒有被捕捉到,查找資料了解到大概因為是發生在子線程中出現的異常,不會被向上抛給主線程。但是主線程中的運作時異常會由JVM捕捉并處理。可以嘗試一下如下代碼:

public static void main(String[] args) throws InterruptedException {
        CompletableFuture.runAsync(() -> {
           System.out.println("start"); 
           String a = null;
           String[] split = a.split(",");
           System.out.println("end");
        });
        System.out.println("wait");
    	//這裡主線程睡眠2s以保證異步線程得到執行
        Thread.sleep(2000);
        System.out.println("exit");
    }
           

本應報NPE的異步代碼塊,執行後卻沒有在控制台看到任何資訊。

另外,注意

RequestContextHolder.currentRequestAttributes()

這一句擷取的Request上下文,是與線程綁定的,具體的邏輯在

FrameworkServlet

中的

processRequest

方法,SpringMVC中的DispatcherServlet繼承自FrameworkServlet,一個Request請求到來時,觸發

service()

方法,在

FrameworkServlet

中,會先調用

processRequest

,在這個方法内,使用ThreadLocal将目前的上下文環境儲存到

RequestContextHolder

的ThreadLocal變量中。而在異步代碼塊裡,運作的線程,很明顯和處理Request請求的線程不是同一個,故無法擷取到Request上下文。

異步代碼塊中出現運作時異常,要如何處理,有待進一步研究。

初步的解決方案是在異步代碼塊内部,用一個try-catch包裹住,并在catch子句中進行異常處理。

或者用CompletableFuture執行異步任務時,用Future變量将該任務的執行儲存下來,後調用get方法,能夠使得異步塊中的異常被扔到主線程中

2020/2/17 更新

可以使用

CompletableFuture.runAsync().exceptionally()

在exceptionally方法中将異常資訊記錄到日志就可以了

繼續閱讀