Spring Cloud(七):服務閘道器zuul過濾器
上文介紹了Zuul的基本使用與路由功能,本文接著介紹Zuul的核心概念 —— Zuul過濾器(filter)。
Zuul的功能基本通過Zuul過濾器來實現(類比於Struts的攔截器,只是Struts攔截器用到責任鏈模式,Zuul則是通過FilterProcessor來控制執行),在不同的階段,通過不同型別的過濾器來實現相應的功能。
Zuul過濾器
過濾器型別
zuul的過濾器根據對HTTP請求的不同處理階段包括如下四種類型
- pre :在請求轉發到後端目標服務之前執行,一般用於請求認證、確定路由地址、日誌記錄等
- route :轉發請求,使用Apache HttpClient 或 Ribbon來構造對目標服務的請求
- post :在目標服務返回結果後對結果進行處理,比如新增響應頭、收集統計效能資料等
- error :在請求處理的整個流程中如果出現錯誤,則會觸發error過濾器執行,對錯誤進行處理
客戶端請求經過zuul過濾器處理的流程如下圖
zuul使用RequestContext
來在過濾器之間傳遞資料,資料存於每個request的ThreadLocal,包含請求路由到哪裡,錯誤,HttpServletRequest,HttpServletResponse 等這些資料都儲存於RequestContext中。RequestContext 擴充套件了ConcurrentHashMap,所以我們可以根據需要將資訊存於context中進行傳遞。
@EnableZuulProxy vs @EnableZuulServer
zuul提供了兩個註解 @EnableZuulProxy, @EnableZuulServer,來啟用不同的過濾器集合。@EnableZuulProxy 啟用的過濾器 是@EnableZuulServer 的超集, 它包含了@EnableZuulServer 的所有過濾器,proxy主要多了一些提供路由功能的過濾器(可見@EnableZuulServer 不提供路由功能,作為server模式而不是代理模式執行)
@EnableZuulServer 註解啟用的過濾器包括
filter型別 | 實現類 | filter順序值 | 功能說明 |
---|---|---|---|
pre | ServletDetectionFilter | -3 | 檢測請求是否通過Spring Dispatcher,並在RequestContext 中新增一個key為isDispatcherServletRequest, 值為true(不通過則為false)的屬性 |
pre | FormBodyWrapperFilter | -1 | 解析Form data,為請求的下游進行重新編碼 |
pre | DebugFilter | 1 | 如果請求引數設定了debug,則會將RequestContext.setDebugRouting() ,RequestContext.setDebugRequest() 設定為ture |
route | SendForwardFilter | 500 | 使用RequestDispatch servlet來轉發請求,轉發地址存於RequestContext中key為FilterConstants.FORWARD_TO_KEY的屬性中,對於轉發到當前應用的介面比較有用 |
post | SendResponseFilter | 1000 | 將代理請求的響應內容寫到當前的響應中 |
error | SendErrorFilter | 0 | 如果RequestContext.getThrowable() 不為空,則會轉發到/error,可以通過error.path來改變預設的轉發路徑/error |
@EnableZuulProxy 除了上面的過濾器,還包含如下過濾器
filter型別 | 實現類 | filter順序值 | 功能說明 |
---|---|---|---|
pre | PreDecorationFilter | 5 | 確定路由到哪裡,如何路由,依賴提供的RouteLocator,同時也為下游請求設定多個與proxy相關的header |
route | RibbonRoutingFilter | 10 | 使用ribbon,hystrix,以及內嵌的http client來發送請求,可在RequestContext中通過FilterConstants.SERVICE_ID_KEY 來找到路由Service的ID |
route | SimpleHostRoutingFilter | 100 | 使用Apache httpClient來發送請求到一個預先確定的url,可通過RequestContext.getRouteHost()來獲取urls |
由上可見@EnableZuulServer 註解並不包含往後端服務負載均衡地路由請求的代理功能,@EnableZuulProxy的PreDecorationFilter,RibbonRoutingFilter過濾器才能擔當此任。PreDecorationFilter通過提供的DiscoveryClientRouteLocator 從 DiscoveryClient(如Eureka)與屬性檔案中載入路由定義, 為每個serviceId建立一個route,新服務新增進來,路由也會動態重新整理。路由確定了,在RibbonRoutingFilter 中通過ribbon與hystrix結合來向後端目標服務發起請求,並進行負載均衡。過濾器的順序值表示在同類型過濾器中的執行順序,值越小越先執行。
自定義Zuul過濾器
自定義的zuul過濾器與框架自帶過濾器類似,包括四部分
- 過濾器型別,包括pre, route, post
- 過濾器順序,定義在同類型過濾器中的執行順序,數值越小越先執行
- 是否執行過濾,通過一些條件判斷來確定是否執行該過濾器
- 過濾器執行體,定義具體執行的操作
比如我們需要在Http請求頭中設定一個值,供請求鏈路的下游環節訪問,則可以自定義一個過濾器如下,
@Component
public class ReqIdPreFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1; //在PreDecorationFilter過濾器之前執行
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader("reqId", UUID.randomUUID().toString());
return null;
}
}
在請求的後續環節,比如後端服務的filter或介面中,則可直接從HttpServletRequest 獲取該header值,如
@GetMapping("hello/reqId")
public String getReqId(HttpServletRequest request) {
return "hello-service返回:" + request.getHeader("reqId");
}
Zuul的錯誤處理
在zuul過濾器的生命週期中,如果任何一個環節丟擲異常,則error過濾器會被執行,SendErrorFilter只有當RequestContext.getThrowable()不為null時才會執行,會設定javax.servlet.error.* 屬性到request中,然後將請求轉發到spring boot的error page, 預設為BasicErrorController實現的/error介面。 有時候我們需要將返回響應格式進行統一,而預設的/error介面實現可能不滿足要求,則可以自定義/error介面。需要實現ErrorController 介面以使預設的BasicErrorController 失效。
@RestController
public class ZuulErrorController implements ErrorController {
@RequestMapping("/error")
public Map<String, String> error(HttpServletRequest request){
Map<String, String> result = Maps.newHashMap();
result.put("code", request.getAttribute("javax.servlet.error.status_code").toString());
result.put("message", request.getAttribute("javax.servlet.error.message").toString());
result.put("exception", request.getAttribute("javax.servlet.error.exception").toString());
return result;
}
@Override
public String getErrorPath() {
return "/error";
}
}
Zuul的服務降級
當呼叫服務出現超時或異常時,在zuul側可提供回撥進行服務降級,返回預設響應結果,如
@Component
public class MyFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return null; //指定這個回撥針對的route Id,如果對所有route,則返回* 或null
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
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 {
Map<String, String> result = Maps.newLinkedHashMap();
result.put("code", "" + status.value());
String msg = HttpStatus.GATEWAY_TIMEOUT == getStatusCode() ? "請求服務超時" : "伺服器內部錯誤";
result.put("message", msg);
return new ByteArrayInputStream(new ObjectMapper().writeValueAsString(result).getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
則當服務請求失敗時,統一返回如下格式的響應
{
"code": "500",
"message": "伺服器內部錯誤"
}
總結
本文主要對Zuul過濾器相關內容及自定義使用進行了介紹,同時對過濾器執行過程中異常的處理及服務呼叫失敗的降級回撥進行了簡單說明。出於篇幅,開發過程中更具體的細節我們後續再繼續探討。
認真生活,快樂分享
歡迎關注微信公眾號:空山新雨的技術空間
獲取Spring Boot,Spring Cloud,Docker等系列技術文章