SpringCloud系列之網(wǎng)關(guān)gateway-13.如何借助網(wǎng)關(guān)層對服務(wù)端各類異常做統(tǒng)一處理

異常的種類

網(wǎng)關(guān)層的異常分為以下兩種:

調(diào)用請求異常 通常由調(diào)用請求直接拋出的異常,比如在訂單服務(wù)中直接報(bào)錯(cuò)

throw new RuntimeException("error")

l 網(wǎng)關(guān)層異常 由網(wǎng)關(guān)層觸發(fā)的異常,比如Gateway通過服務(wù)發(fā)現(xiàn)找不到可用節(jié)點(diǎn),或者任何網(wǎng)關(guān)層內(nèi)部的問題。這部分異常通常是在實(shí)際調(diào)用請求發(fā)起之前發(fā)生的。

在以上兩種問題中,我認(rèn)為網(wǎng)關(guān)層只應(yīng)該關(guān)注第二個(gè)點(diǎn),也就是自身異常。在實(shí)際應(yīng)用中我們應(yīng)該盡量保持網(wǎng)關(guān)層的“純潔性”并且做好職責(zé)劃分,Gateway只要做好路由的事情,不要牽扯到具體業(yè)務(wù)層的事兒,最好也不要替調(diào)用請求的異常操心。對于業(yè)務(wù)調(diào)用中的異常情況,如果需要采用統(tǒng)一格式封裝調(diào)用異常,那就交給每個(gè)具體服務(wù)去定義結(jié)構(gòu),讓各自業(yè)務(wù)方和前端頁面協(xié)調(diào)好異常消息的結(jié)構(gòu)。

但是在實(shí)際項(xiàng)目中,不能保證每個(gè)接口都實(shí)現(xiàn)了異常封裝,如果想給前臺(tái)頁面一個(gè)統(tǒng)一風(fēng)格的JSON格式異常結(jié)構(gòu),那就需要讓Gateway做一些分外的事兒,比如攔截Response并修改返回值。(我還是強(qiáng)烈建議讓服務(wù)端自己定義異常結(jié)構(gòu),因?yàn)镚ateway本身不應(yīng)該對這些異常做額外封裝只是原封不動(dòng)的返回)

Gateway已經(jīng)將網(wǎng)關(guān)層直接拋出的異常(沒有調(diào)用遠(yuǎn)程服務(wù)之前的異常)做了結(jié)構(gòu)化封裝,對于POST的調(diào)用來說其本身也會(huì)返回結(jié)構(gòu)化的異常信息,但是對于GET接口的異常來說,則是直接返回一個(gè)HTML頁面,前端根本無法抓取具體的異常信息。所以我們今天就主要聚焦在如何處理調(diào)用請求異常。

服務(wù)調(diào)用異常

我們定義一個(gè)主動(dòng)拋出異常的GET接口,然后通過網(wǎng)關(guān)層發(fā)起調(diào)用,會(huì)發(fā)現(xiàn)默認(rèn)返回了HTML的異常頁面。

[圖片上傳失敗...(image-879ec9-1631685916923)] 當(dāng)我們使用常規(guī)的全局異常處理方式會(huì)發(fā)現(xiàn)根本不起作用,這是為什么呢?因?yàn)槲覀兡壳笆褂玫腉reenwich版本底層是基于WebFlux來實(shí)現(xiàn)的,并不是Pure Servlet應(yīng)用,因此常規(guī)的手段在這里不起作用。那么接下來,我就帶大家通過添加一個(gè)過濾器,來處理異常調(diào)用,并且將返回值改為JSON格式。

改造客戶端異常

我們先來看一看Gateway網(wǎng)關(guān)層異常情況下的返回?cái)?shù)據(jù)

{

"timestamp": "2019-10-26T15:13:29.870+0000",

"path": "/gateway/error",

"status": 500,

"error": "Internal Server Error",

"message": "Unable to find instance for FEIGN-SERVICE-PROVIDER"

}

看起來干凈整潔,那我們是否可以在網(wǎng)關(guān)層對服務(wù)端返回的異常做一番改造,也呈現(xiàn)類似的效果呢?接下來,我們就運(yùn)用最開始Eureka章節(jié)中學(xué)到的裝飾器編程模式+代理模式,給Gateway加一層特效,改變ResponseBody中的數(shù)據(jù)結(jié)構(gòu),順帶也體驗(yàn)一下如何將編程模式運(yùn)用到實(shí)際需求中。

代理模式 - BodyHackerFunction接口

在最開始我們先定義一個(gè)代理模式的接口

package com.imooc.training;

import org.reactivestreams.Publisher;

import org.springframework.core.io.buffer.DataBuffer;

import org.springframework.http.server.reactive.ServerHttpResponse;

import reactor.core.publisher.Mono;

import java.util.function.BiFunction;

public interface BodyHackerFunction extends

BiFunction<ServerHttpResponse, Publisher<? extends DataBuffer>, Mono<Void>> {

}

這里引入代理模式是為了將裝飾器和具體業(yè)務(wù)代理邏輯拆分開來,在裝飾器中只需要依賴一個(gè)代理接口,而不需要和具體的代理邏輯綁定起來。

裝飾器模式 - BodyHackerDecrator

接下來我們定義一個(gè)裝飾器類,這個(gè)裝飾器繼承自ServerHttpResponseDecorator類,我們這里就用裝飾器模式給Response Body的構(gòu)造過程加上一層特效。

public class BodyHackerHttpResponseDecorator extends ServerHttpResponseDecorator {

    /**
     * 負(fù)責(zé)具體寫入Body內(nèi)容的代理類
     */
    private BodyHackerFunction delegate = null;

    public BodyHackerHttpResponseDecorator(BodyHackerFunction bodyHandler, ServerHttpResponse delegate) {
        super(delegate);
        this.delegate = bodyHandler;
    }

    @Override
    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
        return delegate.apply(getDelegate(), body);
    }

}

這個(gè)裝飾器的構(gòu)造方法接收一個(gè)BodyHancker代理類,其中的關(guān)鍵方法writeWith就是用來向Response Body中寫入內(nèi)容的。這里我們覆蓋了該方法,使用代理類來托管方法的執(zhí)行,而在整個(gè)裝飾器類中看不到一點(diǎn)業(yè)務(wù)邏輯,這就是我們常說的單一職責(zé)。

創(chuàng)建Filter


@Component
@Slf4j
public class ErrorFilter implements GatewayFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        final ServerHttpRequest request = exchange.getRequest();
        BodyHackerFunction delegate = (resp, body) -> Flux.from(body)
                .flatMap(orgBody -> {
                    // 原始的response body
                    byte[] orgContent = new byte[orgBody.readableByteCount()];
                    orgBody.read(orgContent);

                    String content = new String(orgContent);
                    log.info("original content {}", content);

                    // 如果500錯(cuò)誤,則替換
                    if (resp.getStatusCode().value() == 500) {
                        content = String.format("{\"status\": %d,\"path\":\"%s\"}",
                                resp.getStatusCode().value(),
                                request.getPath().value());
                    }


                    // 告知客戶端Body的長度,如果不設(shè)置的話客戶端會(huì)一直處于等待狀態(tài)不結(jié)束
                    HttpHeaders headers = resp.getHeaders();
                    headers.setContentLength(content.length());
                    return resp.writeWith(Flux.just(content)
                            .map(bx -> resp.bufferFactory().wrap(bx.getBytes())));
                }).then();

        // 將裝飾器當(dāng)做Response返回
        BodyHackerHttpResponseDecorator responseDecorator = new BodyHackerHttpResponseDecorator(delegate, exchange.getResponse());

        return chain.filter(exchange.mutate().response(responseDecorator).build());
    }

    @Override
    public int getOrder() {
        // WRITE_RESPONSE_FILTER的執(zhí)行順序是-1,我們的Hacker在它之前執(zhí)行
        return -2;
    }

}

在這個(gè)Filter中,我們定義了一個(gè)裝飾器類BodyHackerHttpResponseDecorator,同時(shí)聲明了一個(gè)匿名內(nèi)部類(代碼TODO部分),實(shí)現(xiàn)了BodyHackerFunction代理類的Body替換邏輯,并且將這個(gè)代理類傳入了裝飾器。這個(gè)裝飾器將直接參與構(gòu)造Response Body。

我們還覆蓋了getOrder方法,是為了確保我們的filter在默認(rèn)的Response構(gòu)造器之前執(zhí)行。

我們對500的HTTP Status做了特殊定制,使用我們自己的JSON內(nèi)容替換了原始內(nèi)容,同學(xué)們可以根據(jù)需要向JSON中加入其它參數(shù)。對于其他非500 Status的Response來說,我們還是返回初始的Body。

這里有個(gè)需要注意的地方就是記得在header中設(shè)置content-length,讓客戶端知道Response中內(nèi)容的長度,否則的話客戶端會(huì)認(rèn)為傳輸未結(jié)束,一直等在那里。

使用Filter

上面步驟都完成以后,接著我們就可以將這個(gè)filter應(yīng)用在指定的路由規(guī)則中,或者定義成global filter,對所有路由規(guī)則生效。經(jīng)過這次的改造,遠(yuǎn)程服務(wù)拋出的異常也在網(wǎng)關(guān)層做了統(tǒng)一處理,從HTML頁面轉(zhuǎn)為了JSON格式的數(shù)據(jù)。

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

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

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