異常的種類
網(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ù)。