Spring Cloud實(shí)戰(zhàn)小貼士:Zuul統(tǒng)一異常處理(二)

在前幾天發(fā)布的《Spring Cloud實(shí)戰(zhàn)小貼士:Zuul統(tǒng)一異常處理(一)》一文中揍堕,我們?cè)敿?xì)說(shuō)明了當(dāng)Zuul的過(guò)濾器中拋出異常時(shí)會(huì)發(fā)生客戶(hù)端沒(méi)有返回任何內(nèi)容的問(wèn)題以及針對(duì)這個(gè)問(wèn)題的兩種解決方案:一種是通過(guò)在各個(gè)階段的過(guò)濾器中增加try-catch塊隶垮,實(shí)現(xiàn)過(guò)濾器內(nèi)部的異常處理毫深;另一種是利用error類(lèi)型過(guò)濾器的生命周期特性,集中地處理pre段磨、route孩革、post階段拋出的異常信息。通常情況下术羔,我們可以將這兩種手段同時(shí)使用,其中第一種是對(duì)開(kāi)發(fā)人員的基本要求乙漓;而第二種是對(duì)第一種處理方式的補(bǔ)充级历,以防止一些意外情況的發(fā)生。這樣的異常處理機(jī)制看似已經(jīng)完美叭披,但是如果在多一些應(yīng)用實(shí)踐或源碼分析之后寥殖,我們會(huì)發(fā)現(xiàn)依然存在一些不足。

不足之處

下面趋观,我們不妨跟著源碼來(lái)看看扛禽,到底上面的方案還有哪些不足之處需要我們注意和進(jìn)一步優(yōu)化的。先來(lái)看看外部請(qǐng)求到達(dá)API網(wǎng)關(guān)服務(wù)之后皱坛,各個(gè)階段的過(guò)濾器是如何進(jìn)行調(diào)度的:

try {
    preRoute();
} catch (ZuulException e) {
    error(e);
    postRoute();
    return;
}
try {
    route();
} catch (ZuulException e) {
    error(e);
    postRoute();
    return;
}
try {
    postRoute();
} catch (ZuulException e) {
    error(e);
    return;
}

上面代碼源自com.netflix.zuul.http.ZuulServletservice方法實(shí)現(xiàn)编曼,它定義了Zuul處理外部請(qǐng)求過(guò)程時(shí),各個(gè)類(lèi)型過(guò)濾器的執(zhí)行邏輯剩辟。從代碼中我們可以看到三個(gè)try-catch塊掐场,它們依次分別代表了preroute贩猎、post三個(gè)階段的過(guò)濾器調(diào)用熊户,在catch的異常處理中我們可以看到它們都會(huì)被error類(lèi)型的過(guò)濾器進(jìn)行處理(之前使用error過(guò)濾器來(lái)定義統(tǒng)一的異常處理也正是利用了這個(gè)特性);error類(lèi)型的過(guò)濾器處理完畢之后吭服,除了來(lái)自post階段的異常之外嚷堡,都會(huì)再被post過(guò)濾器進(jìn)行處理。而對(duì)于從post過(guò)濾器中拋出異常的情況艇棕,在經(jīng)過(guò)了error過(guò)濾器處理之后蝌戒,就沒(méi)有其他類(lèi)型的過(guò)濾器來(lái)接手了串塑,這就是使用之前所述方案存在不足之處的根源。

問(wèn)題分析與進(jìn)一步優(yōu)化

回想一下之前實(shí)現(xiàn)的兩種異常處理方法北苟,其中非常核心的一點(diǎn)桩匪,這兩種處理方法都在異常處理時(shí)候往請(qǐng)求上下文中添加了一系列的error.*參數(shù),而這些參數(shù)真正起作用的地方是在post階段的SendErrorFilter友鼻,在該過(guò)濾器中會(huì)使用這些參數(shù)來(lái)組織內(nèi)容返回給客戶(hù)端傻昙。而對(duì)于post階段拋出異常的情況下敢靡,由error過(guò)濾器處理之后并不會(huì)在調(diào)用post階段的請(qǐng)求秒赤,自然這些error.*參數(shù)也就不會(huì)被SendErrorFilter消費(fèi)輸出。所以舔示,如果我們?cè)谧远xpost過(guò)濾器的時(shí)候借杰,沒(méi)有正確的處理異常过吻,就依然有可能出現(xiàn)日志中沒(méi)有異常并且請(qǐng)求響應(yīng)內(nèi)容為空的問(wèn)題进泼。我們可以通過(guò)修改之前ThrowExceptionFilterfilterType修改為post來(lái)驗(yàn)證這個(gè)問(wèn)題的存在蔗衡,注意去掉try-catch塊的處理,讓它能夠拋出異常乳绕。

解決上述問(wèn)題的方法有很多種绞惦,比如最直接的我們可以在實(shí)現(xiàn)error過(guò)濾器的時(shí)候,直接來(lái)組織結(jié)果返回就能實(shí)現(xiàn)效果洋措,但是這樣的缺點(diǎn)也很明顯济蝉,對(duì)于錯(cuò)誤信息組織和返回的代碼實(shí)現(xiàn)就會(huì)存在多份,這樣非常不易于我們?nèi)蘸蟮拇a維護(hù)工作菠发。所以為了保持對(duì)異常返回處理邏輯的一致王滤,我們還是希望將post過(guò)濾器拋出的異常能夠交給SendErrorFilter來(lái)處理。

在前文中滓鸠,我們已經(jīng)實(shí)現(xiàn)了一個(gè)ErrorFilter來(lái)捕獲pre雁乡、routepost過(guò)濾器拋出的異常糜俗,并組織error.*參數(shù)保存到請(qǐng)求的上下文中踱稍。由于我們的目標(biāo)是沿用SendErrorFilter,這些error.*參數(shù)依然對(duì)我們有用悠抹,所以我們可以繼續(xù)沿用該過(guò)濾器珠月,讓它在post過(guò)濾器拋出異常的時(shí)候,繼續(xù)組織error.*參數(shù)楔敌,只是這里我們已經(jīng)無(wú)法將這些error.*參數(shù)再傳遞給SendErrorFitler過(guò)濾器來(lái)處理了啤挎。所以,我們需要在ErrorFilter過(guò)濾器之后再定義一個(gè)error類(lèi)型的過(guò)濾器卵凑,讓它來(lái)實(shí)現(xiàn)SendErrorFilter的功能庆聘,但是這個(gè)error過(guò)濾器并不需要處理所有出現(xiàn)異常的情況旺韭,它僅對(duì)post過(guò)濾器拋出的異常才有效。根據(jù)上面的思路掏觉,我們完全可以創(chuàng)建一個(gè)繼承自SendErrorFilter的過(guò)濾器区端,就能復(fù)用它的run方法,然后重寫(xiě)它的類(lèi)型澳腹、順序以及執(zhí)行條件织盼,實(shí)現(xiàn)對(duì)原有邏輯的復(fù)用,具體實(shí)現(xiàn)如下:

@Component
public class ErrorExtFilter extends SendErrorFilter {

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 30;  // 大于ErrorFilter的值
    }

    @Override
    public boolean shouldFilter() {
        // TODO 判斷:僅處理來(lái)自post過(guò)濾器引起的異常
        return true;
    }

}

到這里酱塔,我們?cè)谶^(guò)濾器調(diào)度上的實(shí)現(xiàn)思路已經(jīng)很清晰了沥邻,但是又有一個(gè)問(wèn)題出現(xiàn)在我們面前:怎么判斷引起異常的過(guò)濾器是來(lái)自什么階段呢?(shouldFilter方法該如何實(shí)現(xiàn))對(duì)于這個(gè)問(wèn)題羊娃,我們第一反應(yīng)會(huì)寄希望于請(qǐng)求上下文RequestContext對(duì)象唐全,可是在查閱文檔和源碼后發(fā)現(xiàn)其中并沒(méi)有存儲(chǔ)異常來(lái)源的內(nèi)容,所以我們不得不擴(kuò)展原來(lái)的過(guò)濾器處理邏輯蕊玷,當(dāng)有異常拋出的時(shí)候邮利,記錄下拋出異常的過(guò)濾器,這樣我們就可以在ErrorExtFilter過(guò)濾器的shouldFilter方法中獲取并以此判斷異常是否來(lái)自post階段的過(guò)濾器了垃帅。

為了擴(kuò)展過(guò)濾器的處理邏輯延届,為請(qǐng)求上下文增加一些自定義屬性,我們需要深入了解一下Zuul過(guò)濾器的核心處理器:com.netflix.zuul.FilterProcessor贸诚。該類(lèi)中定義了下面過(guò)濾器調(diào)用和處理相關(guān)的核心方法:

  • getInstance():該方法用來(lái)獲取當(dāng)前處理器的實(shí)例
  • setProcessor(FilterProcessor processor):該方法用來(lái)設(shè)置處理器實(shí)例方庭,可以使用此方法來(lái)設(shè)置自定義的處理器
  • processZuulFilter(ZuulFilter filter):該方法定義了用來(lái)執(zhí)行filter的具體邏輯,包括對(duì)請(qǐng)求上下文的設(shè)置酱固,判斷是否應(yīng)該執(zhí)行械念,執(zhí)行時(shí)一些異常處理等
  • getFiltersByType(String filterType):該方法用來(lái)根據(jù)傳入的filterType獲取API網(wǎng)關(guān)中對(duì)應(yīng)類(lèi)型的過(guò)濾器,并根據(jù)這些過(guò)濾器的filterOrder從小到大排序运悲,組織成一個(gè)列表返回
  • runFilters(String sType):該方法會(huì)根據(jù)傳入的filterType來(lái)調(diào)用getFiltersByType(String filterType)獲取排序后的過(guò)濾器列表龄减,然后輪詢(xún)這些過(guò)濾器,并調(diào)用processZuulFilter(ZuulFilter filter)來(lái)依次執(zhí)行它們
  • preRoute():調(diào)用runFilters("pre")來(lái)執(zhí)行所有pre類(lèi)型的過(guò)濾器
  • route():調(diào)用runFilters("route")來(lái)執(zhí)行所有route類(lèi)型的過(guò)濾器
  • postRoute():調(diào)用runFilters("post")來(lái)執(zhí)行所有post類(lèi)型的過(guò)濾器
  • error():調(diào)用runFilters("error")來(lái)執(zhí)行所有error類(lèi)型的過(guò)濾器

根據(jù)我們之前的設(shè)計(jì)扇苞,我們可以直接通過(guò)擴(kuò)展processZuulFilter(ZuulFilter filter)方法欺殿,當(dāng)過(guò)濾器執(zhí)行拋出異常的時(shí)候,我們捕獲它鳖敷,并往請(qǐng)求上下中記錄一些信息脖苏。比如下面的具體實(shí)現(xiàn):

public class DidiFilterProcessor extends FilterProcessor {

    @Override
    public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
        try {
            return super.processZuulFilter(filter);
        } catch (ZuulException e) {
            RequestContext ctx = RequestContext.getCurrentContext();
            ctx.set("failed.filter", filter);
            throw e;
        }
    }
}

在上面代碼的實(shí)現(xiàn)中,我們創(chuàng)建了一個(gè)FilterProcessor的子類(lèi)定踱,并重寫(xiě)了processZuulFilter(ZuulFilter filter)棍潘,雖然主邏輯依然使用了父類(lèi)的實(shí)現(xiàn),但是在最外層,我們?yōu)槠湓黾恿水惓2东@亦歉,并在異常處理中為請(qǐng)求上下文添加了failed.filter屬性恤浪,以存儲(chǔ)拋出異常的過(guò)濾器實(shí)例。在實(shí)現(xiàn)了這個(gè)擴(kuò)展之后肴楷,我們也就可以完善之前ErrorExtFilter中的shouldFilter()方法水由,通過(guò)從請(qǐng)求上下文中獲取該信息作出正確的判斷,具體實(shí)現(xiàn)如下:

@Component
public class ErrorExtFilter extends SendErrorFilter {

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 30;  // 大于ErrorFilter的值
    }

    @Override
    public boolean shouldFilter() {
        // 判斷:僅處理來(lái)自post過(guò)濾器引起的異常
        RequestContext ctx = RequestContext.getCurrentContext();
        ZuulFilter failedFilter = (ZuulFilter) ctx.get("failed.filter");
        if(failedFilter != null && failedFilter.filterType().equals("post")) {
            return true;
        }
        return false;
    }

}

到這里赛蔫,我們的優(yōu)化任務(wù)還沒(méi)有完成砂客,因?yàn)閿U(kuò)展的過(guò)濾器處理類(lèi)并還沒(méi)有生效。最后呵恢,我們需要在應(yīng)用主類(lèi)中鞠值,通過(guò)調(diào)用FilterProcessor.setProcessor(new DidiFilterProcessor());方法來(lái)啟用自定義的核心處理器以完成我們的優(yōu)化目標(biāo)。

本文節(jié)選自《Spring Cloud微服務(wù)實(shí)戰(zhàn)》并稍做加工渗钉,轉(zhuǎn)載請(qǐng)注明出處

本文由 程序猿DD-翟永超 創(chuàng)作彤恶,采用 CC BY 3.0 CN協(xié)議 進(jìn)行許可。 可自由轉(zhuǎn)載鳄橘、引用声离,但需署名作者且注明文章出處。
原文首發(fā)于:http://blog.didispace.com/spring-cloud-zuul-exception-2/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末挥唠,一起剝皮案震驚了整個(gè)濱河市抵恋,隨后出現(xiàn)的幾起案子焕议,更是在濱河造成了極大的恐慌宝磨,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盅安,死亡現(xiàn)場(chǎng)離奇詭異唤锉,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)别瞭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)窿祥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人蝙寨,你說(shuō)我怎么就攤上這事晒衩。” “怎么了墙歪?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵听系,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我虹菲,道長(zhǎng)靠胜,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮浪漠,結(jié)果婚禮上陕习,老公的妹妹穿的比我還像新娘。我一直安慰自己址愿,他們只是感情好该镣,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著响谓,像睡著了一般拌牲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上歌粥,一...
    開(kāi)封第一講書(shū)人閱讀 51,554評(píng)論 1 305
  • 那天塌忽,我揣著相機(jī)與錄音,去河邊找鬼失驶。 笑死土居,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的嬉探。 我是一名探鬼主播擦耀,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼涩堤!你這毒婦竟也來(lái)了眷蜓?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤胎围,失蹤者是張志新(化名)和其女友劉穎吁系,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體白魂,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡汽纤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了福荸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蕴坪。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖敬锐,靈堂內(nèi)的尸體忽然破棺而出背传,到底是詐尸還是另有隱情,我是刑警寧澤台夺,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布径玖,位于F島的核電站,受9級(jí)特大地震影響谒养,放射性物質(zhì)發(fā)生泄漏挺狰。R本人自食惡果不足惜明郭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丰泊。 院中可真熱鬧薯定,春花似錦、人聲如沸瞳购。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)学赛。三九已至年堆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間盏浇,已是汗流浹背变丧。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绢掰,地道東北人痒蓬。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像滴劲,于是被迫代替她去往敵國(guó)和親攻晒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容