白話SpringCloud | 第十章:路由閘道器(Zuul)進階:過濾器、異常處理
前言
簡單介紹了關於 Zuul
的一些簡單使用以及一些路由規則的簡單說明。而對於一個統一閘道器而言,需要處理各種各類的請求,對不同的url進行攔截,或者對呼叫服務的異常進行二次處理等等。今天,我們就來了解下這方面的相關知識點。
一點知識
開始實踐前,我們先來了解下 Zuul
預設的過濾器(注意,這裡講解的 Zuul
都是 1.X
版本的)。上一章節,也提到了 Zuul
的核心就是一系列過濾器。現在我們來看看 Zuul
的過濾器相關資訊。
過濾器的定義
Zuul
中定義了四種標準過濾器型別,這些過濾器型別對應於請求的典型生命週期。
- PRE :可以在請求被路由之前呼叫。我們可利用這種過濾器實現身份驗證、在叢集中選擇請求的微服務、記錄除錯資訊等。
- ROUTING :在路由請求時候被呼叫。這種過濾器用於構建傳送給微服務的請求,並使用
Apache HttpClient
或Netfilx Ribbon
請求微服務。 - POST :在
routing
和error
過濾器之後被呼叫。這種過濾器可用來為響應新增標準的HTTP Header
、收集統計資訊和指標、將響應從微服務傳送給客戶端等。 - ERROR :處理請求時發生錯誤時被呼叫。
現在看下官網wiki提供的四種過濾器的生命週期圖。
一個請求會先按順序通過所有的前置過濾器,之後在路由過濾器中轉發給後端應用,得到響應後又會通過所有的後置過濾器,最後響應給客戶端。在整個流程中如果發生了異常則會跳轉到錯誤過濾器中。
一般來說,如果需要在請求到達後端應用前就進行處理的話,會選擇 pre(前置過濾器)
,例如鑑權、請求轉發、增加請求引數等行為。在請求完成後需要處理的操作放在 (post)後置過濾器
中完成,例如統計返回值和呼叫時間、記錄日誌、增加跨域頭等行為。路由過濾器一般只需要選擇 Zuul 中內建的即可,錯誤過濾器一般只需要一個,這樣可以在遇到錯誤邏輯時直接丟擲異常中斷流程,並直接統一處理返回結果
說下 error
過濾器: pre
、 routing
的任意一個階段如果拋異常了,則執行 error
過濾器,然後再執行 post
給出響應。而 post
異常了,就直接呼叫 error
了。
過濾器介面定義
知道了過濾器的定義,我們看看過濾器是怎麼被定義的。檢視類 com.netflix.zuul.ZuulFilter
類,可知其個抽象類:
以下為需要實現的方法,其他具體的可自行查閱下
//過濾器型別 String filterType(); //執行順序 越小越先執行 intfilterOrder(); //是否執行 返回false 不執行此過濾器 booleanshouldFilter(); //過濾器執行邏輯 Object run();
具體說明下:
- filterType :該函式需要返回一個字串來代表過濾器的型別,而這個型別就是在HTTP請求過程中定義的各個階段。在Zuul中預設定義了四種不同生命週期的過濾器型別,具體如下:
- pre:可以在請求被路由之前呼叫。
- routing:在路由請求時候被呼叫。
- post:在routing和error過濾器之後被呼叫。
- error:處理請求時發生錯誤時被呼叫。
- filterOrder :通過int值來定義過濾器的執行順序,數值越小優先順序越高。
- shouldFilter :返回一個
boolean
型別來判斷該過濾器是否要執行。我們可以通過此方法來指定過濾器的有效範圍。 - run :過濾器的具體邏輯。在該函式中,我們可以實現自定義的過濾邏輯,來確定是否要攔截當前的請求,不對其進行後續的路由,或是在請求路由返回結果之後,對處理結果做一些加工等。
所以,瞭解了過濾器抽象類的定義,自定義抽象類就簡單了。
zuul自帶過濾器
通過 IDE
我們來看下已經實現 ZuulFilter
的過濾器類。具體的類在:
看看已經提供的過濾器:
可以看見, Spring cloud zuul
提供了很多過濾器,基本上就開箱即用了。簡單說明下:
型別 | 順序 | 過濾器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 標記處理Servlet的型別 |
pre | -2 | Servlet30WrapperFilter | 包裝HttpServletRequest請求 |
pre | -1 | FormBodyWrapperFilter | 包裝請求體 |
pre | 1 | DebugFilter | 標記除錯標誌 |
pre | 5 | PreDecorationFilter | 處理請求上下文供後續使用 |
route | 10 | RibbonRoutingFilter | serviceId請求轉發 |
route | 100 | SimpleHostRoutingFilter | url請求轉發 |
route | 500 | SendForwardFilter | forward請求轉發 |
error | 0 | SendErrorFilter | 處理有錯誤的請求響應 |
post | 1000 | SendResponseFilter | 處理正常的請求響應 |
禁用過濾器
元件實現的過濾器,滿足執行條件時都是會執行的,若我們想禁用某個過濾器時,可以在配置檔案中配置。
規則:zuul.<SimpleClassName>.<filterType>.disable=true
說明: SimpleClassName 為 類名 , filterType 過濾器 型別
#禁用DebugFilter過濾器 zuul.DebugFilter.pre.disable=true
Zuul進階示例
為了區分不混淆,建立一個新的專案進行示例: spring-cloud-zuul-advanced
。
對於通用部分,如pom依賴等都是和專案 spring-cloud-zuul
一樣的,不一樣的會具體指出的。大家可檢視《 ofollow,noindex" target="_blank">第九章:路由閘道器(Zuul)的使用 》,這裡就不重複貼了。
自定義filter
通過以上幾個小節的說明,我們通過繼承 ZuulFilter
類進行自定義過濾器的編寫。這裡直接校驗請求的引數是否帶有 token
,若無此引數時,直接進行請求攔截。
/** * 自定義過濾器-校驗請求引數是否合法:包含token引數 * @author oKong * */ @Slf4j public class AccessZuulFilter extends ZuulFilter{ @Override public boolean shouldFilter() { //此方法可以根據請求的url進行判斷是否需要攔截 return true; } @Override public Object run() throws ZuulException { //獲取請求的上下文類 注意是:com.netflix.zuul.context包下的 RequestContext ctx = RequestContext.getCurrentContext(); //獲取request物件 HttpServletRequest request = ctx.getRequest(); //避免中文亂碼 ctx.addZuulResponseHeader("Content-type", "text/json;charset=UTF-8"); ctx.getResponse().setCharacterEncoding("UTF-8"); //列印日誌 log.info("請求方式:{},地址:{}", request.getMethod(),request.getRequestURI()); String token = request.getParameter("token"); if(StringUtils.isBlank(token)) { //使其不進行轉發 自定義route型別時,在shouldFilter中也需要進行此引數判斷。 ctx.setSendZuulResponse(false); ctx.setResponseBody("{\"code\":\"999500\",\"msg\":\"非法訪問\"}"); ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());//401 //或者新增一個額外引數也可以 傳遞引數可以使用 //ctx.set("checkAuth",false); } //這返回值沒啥用 return null; } @Override public String filterType() { //前置過濾器 return PRE_TYPE; } @Override public int filterOrder() { //執行順序0 靠前執行 在spring cloud zuul提供的pre過濾器之後執行,預設的是小於0的。 //除了引數校驗類的過濾器 一般上直接放在 PreDecoration前 //即:PRE_DECORATION_FILTER_ORDER - 1; //常量類都在:org.springframework.cloud.netflix.zuul.filters.support.FilterConstants 下 return 0; } }
同時在啟動類中使用 @Bean
標記,使其生效。
@Bean public AccessZuulFilter accessZuulFilter() { return new AccessZuulFilter(); }
注意: Spring cloud
為我們提供了常量類: org.springframework.cloud.netflix.zuul.filters.support.FilterConstants
靜態引入對於的常量即可。裡面包含了各過濾器的執行順序值、過濾器型別常量以及一些頭部引數或者變數引數名: 請求服務ID
、 請求URI
等。這些引數都是很有用的,比如 請求服務ID
,若為空,則直接使用 SimpleHostRoutingFilter
進行請求轉發,否則是 RibbonRoutingFilter
進行服務轉發。這些變數都是通過 PreDecorationFilter
前置過濾器進行賦值處理的。
啟動應用,訪問:http://127.0.0.1:8889/myapi/hello?name=oKong 可以看見,請求被攔截了,返回了非法訪問提示。
接著,我們請求引數帶上 token
:http://127.0.0.1:8889/myapi/hello?name=oKong&token=okong ,可以看見請求被正常轉發了。
異常處理
從目前的檔案中,我們可以知曉:目前可以通過 serviceId
、 url
進行請求轉發,根據 PreDecorationFilter
前置過濾器鑑別不同的型別,最後通過 ribbon
或者常規的 http
訪問目標服務。在訪問目標服務,發生異常是在正常不過的了。從第一小節我們可以獲悉,當過濾器發生異常時,會呼叫 error
過濾器進行異常資訊處理,預設情況下就是: SendErrorFilter
。首先,我們看看,預設情況下,以上兩種異常是如何進行異常資訊展現的。
首先,我們 spring-cloud-eureka-client
服務停止了,之後訪問下:http://127.0.0.1:8889/eureka/hello?name=oKong&token=okong ,可以看見返回的就是正常 boot
預設異常,即: /error
頁面。
接著,訪問下:http://127.0.0.1:8889/myapi/hello?name=oKong&token=okong ,相同的都是跳轉至 /error
頁面。
可以發現,第二種錯誤資訊更加直觀也更有用,可以獲悉是服務不可用造成的。
現在,我們來看看, SendErrorFilter
類的 run
方法。
可以獲悉,其主要的生效條件是 包含異常物件: throwable
,而第二個條件只是為了避免二次執行。為了瞭解下其呼叫關係,我們檢視下 com.netflix.zuul.http.ZuulServlet
類的 service
方法,這個類它定義了Zuul處理外部請求過程時,各個型別過濾器的執行邏輯。
以上截圖了此類的 service
方法,可以看見,每呼叫一個過濾器型別時,外部都是用 try..catch
包裹了,異常發生時都呼叫了 error
方法,現在我們看看 error()
方法。
可以看見,當一個觸發器發生異常時,統一設定了異常物件 throwable
,而後去呼叫 error
型別的過濾器。
針對閘道器自己的 api
介面時,和普通的 web
應用是一樣的了。也是跳轉至 /error
上,此時可以使用 @ControllerAdvice
進行統一異常處理。關於統一異常的處理,可以檢視《 SpringBoot | 第八章:統一異常、資料校驗處理 》,這裡就不闡述了。
服務異常回退
通過前一章節,我們值得可以通過註冊中心的服務ID進行自動轉發,當遠端服務不可用時,我們可以通過 Hystrix
進行服務回退處理。官網文件也說明了,只需實現 FallbackProvider
介面類即可。
建立一個服務 eureka-client
的異常回退類: myEurekaClientFallback
。
/** * 服務 eureka-client 的異常退回處理類 * @author oKong */ public class MyEurekaClientFallback implements FallbackProvider { @Override public String getRoute() { // TODO Auto-generated method stub return "eureka-client"; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { //標記不同的異常為不同的http狀態值 if (cause instanceof HystrixTimeoutException) { return response(HttpStatus.GATEWAY_TIMEOUT); } else { //可繼續新增自定義異常類 return response(HttpStatus.INTERNAL_SERVER_ERROR); } } //處理 private ClientHttpResponse response(final HttpStatus status) { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return status; } @Override public int getRawStatusCode() throws IOException { return status.value(); } @Override public String getStatusText() throws IOException { return status.getReasonPhrase(); } @Override public void close() { } @Override public InputStream getBody() throws IOException { //可替換成相應的json串的 看業務規定了 return new ByteArrayInputStream("{\"code\":\"999999\",\"msg\":\"服務暫時不可用\"}".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; } }
同時在啟動類中使用 @Bean
標記,使其生效。
@Bean public MyEurekaClientFallback eurekaClientFallback() { return new MyEurekaClientFallback(); }
此時,我們停止 spring-cloud-eureka-client
服務,訪問:http://127.0.0.1:8889/eureka/hello?name=oKong&token=okong ,可以看見看見已經正確返回錯誤資訊了。
另外,需要細化異常的,可對 fallbackResponse
的 Throwable
進行異常判斷的,以獲取具體的異常資訊,如超時、處理異常等等。而且,設定了服務回退,此時對於 route
過濾器而言是正常呼叫,未發生異常,所以也就不會呼叫 error
過濾器了。
常規http請求異常
當使用 Ribbon
進行服務呼叫時,我們可以使用 FallbackProvider
進行呼叫,而當我們常規的使用 url
進行轉發時,我們也應該進行異常結果處理,以保持返回值一致。已經知道,發生異常時,會呼叫 SendErrorFilter
異常過濾器,對異常經常處理,同時重定向至 /error
中,所以,一般上我們可以 自定義ErrorController類 或者 參照SendErrorFilter進行二次開發 ,對返回值進行個性化處理即可。這裡簡單演示下通過 自定義異常過濾器
進行異常處理。
/** * 自定義異常類 過濾器 直接擴充套件 SendErrorFilter 類 * @author oKong * */ @Slf4j public class CustomErrorFilter extends SendErrorFilter{ @Override public Object run() { //重寫 run方法 try{ RequestContext ctx = RequestContext.getCurrentContext(); //直接複用異常處理類 ExceptionHolder exception = findZuulException(ctx.getThrowable()); log.info("異常資訊:{}", exception.getThrowable()); //這裡可對不同異常返回不同的錯誤碼 HttpServletResponse response = ctx.getResponse(); response.getOutputStream().write(("{\"code\":\"999999\",\"msg\":\"" + exception.getErrorCause() + "\"}").getBytes()); }catch (Exception ex) { ReflectionUtils.rethrowRuntimeException(ex); } return null; } }
同時,禁用 SendErrorFilter
過濾器。
## 停用預設的異常處理器SendErrorFilter zuul.SendErrorFilter.error.disable=true
在啟動類,使用 @Bean
生效自定義過濾器。
@Bean @ConditionalOnProperty(name="zuul.SendErrorFilter.error.disable") public CustomErrorFilter customErrorFilter() { return new CustomErrorFilter(); }
重啟應用,訪問:http://127.0.0.1:8889/myapi/hello?name=oKong&token=okong ,可以看見已經是按自定義返回值返回了。
另外注意的是,前面也有提到,當訪問不存在的路徑或者轉發路徑時,依舊是普通的異常,可通過統一異常進行攔截,返回值拼裝的。
參考資料
-
Netflix/zuul/wiki/How-it-Works" rel="nofollow,noindex" target="_blank">https://github.com/Netflix/zuul/wiki/How-it-Works
總結
本章節主要介紹了關於 Zuul
過濾器和相關異常處理的相關知識點。可能還是存在不完整的情況,大家在遇見相關問題時,可查閱下官方文件的。 Zuul
本身還有一些其他的高階功能的,本人也用的不多,相關配置也是看了官方文件時才知道如何配置和使用的。所以,不知道相關配置時,可以去查閱下相關文件,比如一些忽略頭部資訊、忽略服務等等配置,都未涉及。主要還是用的不多。。原來我們都是自建一個 restful
服務進行統一閘道器呼叫的,當頻繁修改api時此方法就有點麻煩需要多次變動了。主要看業務需求吧,這東西可大可小的。最簡單當然建立個簡單的 web
就行了,而當需要實現一些高階功能,比如灰度釋出,動態引流時可能就需要考慮下使用 Zuul
或者 gateway
。有時間去看看 gateway
,據說效能好呀。關於閘道器的暫時就告一段落了,接下來會分享一些服務之間呼叫異常處理的,敬請期待~
最後
目前網際網路上大佬都有分享 SpringCloud
系列教程,內容可能會類似,望多多包涵了。 原創不易,碼字不易 ,還希望大家多多支援。若文中有錯誤之處,還望提出,謝謝。
老生常談
499452441 lqdevOps
個人部落格: http://blog.lqdev.cn
原始碼示例: https://github.com/xie19900123/spring-cloud-learning
原文地址: http://blog.lqdev.cn/2018/10/17/SpringCloud/chapter-ten/