前言
簡單介紹了關於
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();
//執行順序 越小越先執行
int filterOrder();
//是否執行 返回false 不執行此過濾器
boolean shouldFilter();
//過濾器執行邏輯
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一樣的,不一樣的會具體指出的。大家可查看《第九章:路由網關(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 ,可以看見已經是按自定義返回值返回了。

另外注意的是,前面也有提到,當訪問不存在的路徑或者轉發路徑時,依舊是普通的異常,可通過統一異常進行攔截,返回值拼裝的。
參考資料
總結
本章節主要介紹了關於
Zuul過濾器和相關異常處理的相關知識點。可能還是存在不完整的情況,大家在遇見相關問題時,可查閱下官方文檔的。Zuul本身還有一些其他的高級功能的,本人也用的不多,相關配置也是看了官方文檔時才知道如何配置和使用的。所以,不知道相關配置時,可以去查閱下相關文檔,比如一些忽略頭部信息、忽略服務等等配置,都未涉及。主要還是用的不多。。原來我們都是自建一個restful服務進行統一網關調用的,當頻繁修改api時此方法就有點麻煩需要多次變動了。主要看業務需求吧,這東西可大可小的。最簡單當然創建個簡單的web就行了,而當需要實現一些高級功能,比如灰度發布,動態引流時可能就需要考慮下使用Zuul或者gateway。有時間去看看gateway,據說性能好呀。關於網關的暫時就告一段落了,接下來會分享一些服務之間調用異常處理的,敬請期待~
最后
目前互聯網上大佬都有分享
SpringCloud系列教程,內容可能會類似,望多多包涵了。原創不易,碼字不易,還希望大家多多支持。若文中有錯誤之處,還望提出,謝謝。
老生常談
- 個人QQ:
499452441 - 微信公眾號:
lqdevOps

個人博客:http://blog.lqdev.cn
源碼示例:https://github.com/xie19900123/spring-cloud-learning
原文地址:http://blog.lqdev.cn/2018/10/17/SpringCloud/chapter-ten/
