Spring Cloud系列(二十六) Zuul過濾器詳解(Finchley.RC2版本)
Zuul核心過濾器
通過前兩篇對Zuul的介紹,我們對於Zuul的有了一個大概的印象:它包含了對請求的路由和過濾兩個功能,其中路由功能負責將外部請求轉發到具體的微服務例項上,是實現外部訪問統一入口的基礎;而過濾器功能則負責對請求的處理過程進行干預,是實現請求校驗、服務聚合等功能的基礎。然而實際上,路由功能在真正執行時,它的路由對映和請求轉發都是由幾個不同的過濾器完成的。其中,路由對映主要通過pre型別的過濾器完成,它將請求路徑與配置的路由規則進行匹配,以找到需要轉發的目標地址;而請求轉發的部分則是由route型別的過濾器來完成,對pre型別過濾器獲得的路由地址進行轉發。所以,過濾器是Zuul實現API閘道器功能最為核心的部件,每一個進入Zuul的HTTP請求都會經過一系列的過濾器處理鏈得到請求響應並返回給客戶端。
在Spring Cloud Zuul中實現的過濾器必須包含4個基本特徵:過濾型別、執行順序、執行條件、具體操作,這四個操作就是IZuulFilter介面以及ZuulFilter抽象類(ZuulFilter實現了IZuulFilter)和中定義的四個抽象方法:
String filterType();
int filterOrder();
boolean shouldFilter();
Object run();
它們各自的含義與功能總結如下:
- filterType:該函式需要返回一個字串來代表過濾器的型別,而這個型別就是在HTTP請求過程中定義的各個階段。在Zuul中預設定義了四種不同生命週期的過濾器型別,具體如下:
- pre
- routing:在路由請求時候被呼叫。
- post:在routing和error過濾器之後被呼叫。
- error:處理請求時發生錯誤時被呼叫。
- pre
- filterOrder:通過int值來定義過濾器的執行順序,數值越小優先順序越高。
- shouldFilter:返回一個boolean型別來判斷該過濾器是否要執行。我們可以通過此方法來指定過濾器的有效範圍。
- run:過濾器的具體邏輯。在該函式中,我們可以實現自定義的過濾邏輯,來確定是否要攔截當前的請求,不對其進行後續的路由,或是在請求路由返回結果之後,對處理結果做一些加工等。
請求生命週期
Zuul預設定義了四個不同的過濾器型別,它們覆蓋了一個外部HTTP請求到達API閘道器,直到返回請求結果的全部生命週期。下圖源自Zuul的官方WIKI中關於請求生命週期的圖解,它描述了一個HTTP請求到達API閘道器之後,如何在各個不同型別的過濾器之間流轉的詳細過程。
從上圖中,我們可以看到,當外部HTTP請求到達API閘道器服務的時候,首先它會進入第一個階段pre,在這裡它會被pre型別的過濾器進行處理,該型別的過濾器主要目的是在進行請求路由之前做一些前置加工,比如請求的校驗等。在完成了pre型別的過濾器處理之後,請求進入第二個階段routing,也就是之前說的路由請求轉發階段,請求將會被routing型別過濾器處理,這裡的具體處理內容就是將外部請求轉發到具體服務例項上去的過程,當服務例項將請求結果都返回之後,routing階段完成,請求進入第三個階段post,此時請求將會被post型別的過濾器進行處理,這些過濾器在處理的時候不僅可以獲取到請求資訊,還能獲取到服務例項的返回資訊,所以在post型別的過濾器中,我們可以對處理結果進行一些加工或轉換等內容。另外,還有一個特殊的階段error,該階段只有在上述三個階段中發生異常的時候才會觸發,但是它的最後流向還是post型別的過濾器,因為它需要通過post過濾器將最終結果返回給請求客戶端(實際實現上還有一些差別,下面介紹)。
核心過濾器
在Spring Cloud Zuul中,為了讓API閘道器元件可以更方便的上手使用,它在HTTP請求生命週期的各個階段預設地實現了一批核心過濾器,它們會在API閘道器服務啟動的時候被自動地載入和啟用。我們可以在原始碼中檢視和了解它們,它們定義於spring-cloud-netflix-core模組的org.springframework.cloud.netflix.zuul.filters包下。
pre過濾器
ServletDetectionFilter:它的執行順序為-3,是最先被執行的過濾器。該過濾器總是會被執行,主要用來檢測當前請求是通過Spring的DispatcherServlet處理執行,還是通過ZuulServlet來處理執行的。它的檢測結果會以布林型別儲存在當前請求上下文的isDispatcherServletRequest引數中,這樣在後續的過濾器中,我們就可以通過RequestUtils.isDispatcherServletRequest()和RequestUtils.isZuulServletRequest()方法判斷它以實現做不同的處理。一般情況下,傳送到API閘道器的外部請求都會被Spring的DispatcherServlet處理,除了通過/zuul/路徑訪問的請求會繞過DispatcherServlet,被ZuulServlet處理,主要用來應對處理大檔案上傳的情況。另外,對於ZuulServlet的訪問路徑/zuul/,我們可以通過zuul.servletPath引數來進行修改。
Servlet30WrapperFilter:它的執行順序為-2,是第二個執行的過濾器。目前的實現會對所有請求生效,主要為了將原始的HttpServletRequest包裝成Servlet30RequestWrapper物件。
FormBodyWrapperFilter:它的執行順序為-1,是第三個執行的過濾器。該過濾器僅對兩種類請求生效,第一類是Content-Type為application/x-www-form-urlencoded的請求,第二類是Content-Type為multipart/form-data並且是由Spring的DispatcherServlet處理的請求(用到了ServletDetectionFilter的處理結果)。而該過濾器的主要目的是將符合要求的請求體包裝成FormBodyRequestWrapper物件。
DebugFilter:它的執行順序為1,是第四個執行的過濾器。該過濾器會根據配置引數zuul.debug.request和請求中的debug引數來決定是否執行過濾器中的操作。而它的具體操作內容則是將當前的請求上下文中的debugRouting和debugRequest引數設定為true。由於在同一個請求的不同生命週期中,都可以訪問到這兩個值,所以我們在後續的各個過濾器中可以利用這兩值來定義一些debug資訊,這樣當線上環境出現問題的時候,可以通過請求引數的方式來啟用這些debug資訊以幫助分析問題。另外,對於請求引數中的debug引數,我們也可以通過zuul.debug.parameter來進行自定義。
PreDecorationFilter:它的執行順序為5,是pre階段最後被執行的過濾器。該過濾器會判斷當前請求上下文中是否存在forward.to和serviceId引數,如果都不存在,那麼它就會執行具體過濾器的操作(如果有一個存在的話,說明當前請求已經被處理過了,因為這兩個資訊就是根據當前請求的路由資訊載入進來的)。而它的具體操作內容就是為當前請求做一些預處理,比如:進行路由規則的匹配、在請求上下文中設定該請求的基本資訊以及將路由匹配結果等一些設定資訊等,這些資訊將是後續過濾器進行處理的重要依據,我們可以通過RequestContext.getCurrentContext()來訪問這些資訊。另外,我們還可以在該實現中找到一些對HTTP頭請求進行處理的邏輯,其中包含了一些耳熟能詳的頭域,比如:X-Forwarded-Host、X-Forwarded-Port。另外,對於這些頭域的記錄是通過zuul.addProxyHeaders引數進行控制的,而這個引數預設值為true,所以Zuul在請求跳轉時預設地會為請求增加X-Forwarded-*頭域,包括:X-Forwarded-Host、X-Forwarded-Port、X-Forwarded-For、X-Forwarded-Prefix、X-Forwarded-Proto。我們也可以通過設定zuul.addProxyHeaders=false關閉對這些頭域的新增動作。
補充兩點:
- 通過Zuul閘道器上傳檔案時導致中文名亂碼,是因為你使用了DispatcherServlet,需要使用ZuulServlet來避免中文亂碼的問題,使用方式就是請求加上字首/zuul/,比如講原本的/upload修改成/zuul/upload來請求API閘道器,當然如果沒有中文可以使用DispatcherServlet。
route過濾器
RibbonRoutingFilter:它的執行順序為10,是route階段第一個執行的過濾器。該過濾器只對請求上下文中存在serviceId引數的請求進行處理,即只對通過serviceId配置路由規則的請求生效。而該過濾器的執行邏輯就是面向服務路由的核心,它通過使用Ribbon和Hystrix來向服務例項發起請求,並將服務例項的請求結果返回。
SimpleHostRoutingFilter:它的執行順序為100,是route階段第二個執行的過濾器。該過濾器只對請求上下文中存在routeHost引數的請求進行處理,即只對通過url配置路由規則的請求生效。而該過濾器的執行邏輯就是直接向routeHost引數的實體地址發起請求,從原始碼中我們可以知道該請求是直接通過httpclient包實現的,而沒有使用Hystrix命令進行包裝,所以這類請求並沒有執行緒隔離和斷路器的保護。
SendForwardFilter:它的執行順序為500,是route階段第三個執行的過濾器。該過濾器只對請求上下文中存在forward.to引數的請求進行處理,即用來處理路由規則中的forward本地跳轉配置。
post過濾器
LocationRewriteFilter:它的執行順序是900,於重定向時,負責將標頭重寫為Zuul的URL,否則,瀏覽器會重定向到Web應用程式的URL而不是Zuul URL。
SendResponseFilter:它的執行順序為1000,是post階段最後執行的過濾器。該過濾器會檢查請求上下文中是否包含請求響應相關的頭資訊、響應資料流或是響應體,只有在包含它們其中一個的時候就會執行處理邏輯。而該過濾器的處理邏輯就是利用請求上下文的響應資訊來組織需要傳送回客戶端的響應內容。
error過濾器
SendErrorFilter:它的執行順序為0,是post階段第一個執行的過濾器。該過濾器僅在請求上下文中包含error.status_code引數(由之前執行的過濾器設定的錯誤編碼)並且還沒有被該過濾器處理過的時候執行。而該過濾器的具體邏輯就是利用請求上下文中的錯誤資訊來組織成一個forward到API閘道器/error錯誤端點的請求來產生錯誤響應。
下表是對上述過濾器根據順序、名稱、功能、型別做了綜合的整理,可以幫助我們在自定義過濾器或是擴充套件過濾器的時候用來參考並全面地考慮整個請求生命週期的處理過程。
型別 | 過濾器 | 順序 | 功能 |
pre | ServletDetectionFilter | -3 | 標記處理Servlet的型別 |
pre | Servlet30WrapperFilter | -2 | 包裝HttpServletRequest請求 |
pre | FormBodyWrapperFilter | -1 | 包裝請求體 |
pre | DebugFilter | 1 | 標記除錯標誌 |
pre | PreDecorationFilter | 5 | 處理請求上下文供後續使用 |
route | RibbonRoutingFilter | 10 | serviceId請求轉發 |
route | SimpleHostRoutingFilter | 100 | url請求轉發 |
route | SendForwardFilter | 500 | forward請求轉發 |
error | SendErrorFilter | 0 | 處理有錯誤的請求響應 |
post | LocationRewriteFilter | 900 | 重定向時,負責將標頭重寫為Zuul的URL |
post | SendResponseFilter | 1000 | 組織需要傳送回客戶端的響應內容 |
異常處理
我們來看看SendErrorFilter的shouldFilter。
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// only forward to errorPath if it hasn't been forwarded to already
return ctx.getThrowable() != null
&& !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}
可以看到,如果在Zuul過濾器生命週期的任何部分期間丟擲異常,則執行錯誤過濾器。 僅當RequestContext.getThrowable()不為null時,才會執行SendErrorFilter。 然後,它在請求中設定特定的javax.servlet.error.*屬性,並將請求轉發到Spring Boot錯誤頁面。
發生異常的處理邏輯變為如下圖所示的流程:
自定義異常處理
第一種方式:自定義ErrorController
看一下Zuul預設的異常處理過濾器的處理邏輯
public class SendErrorFilter extends ZuulFilter {
private static final Log log = LogFactory.getLog(SendErrorFilter.class);
protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";
@Value("${error.path:/error}")
private String errorPath;
@Override
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
ZuulException exception = findZuulException(ctx.getThrowable());
HttpServletRequest request = ctx.getRequest();
request.setAttribute("javax.servlet.error.status_code", exception.nStatusCode);
log.warn("Error during filtering", exception);
request.setAttribute("javax.servlet.error.exception", exception);
if (StringUtils.hasText(exception.errorCause)) {
request.setAttribute("javax.servlet.error.message", exception.errorCause);
}
RequestDispatcher dispatcher = request.getRequestDispatcher(
this.errorPath);
if (dispatcher != null) {
ctx.set(SEND_ERROR_FILTER_RAN, true);
if (!ctx.getResponse().isCommitted()) {
ctx.setResponseStatusCode(exception.nStatusCode);
dispatcher.forward(request, ctx.getResponse());
}
}
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
ZuulException findZuulException(Throwable throwable) {
if (throwable.getCause() instanceof ZuulRuntimeException) {
// this was a failure initiated by one of the local filters
return (ZuulException) throwable.getCause().getCause();
}
if (throwable.getCause() instanceof ZuulException) {
// wrapped zuul exception
return (ZuulException) throwable.getCause();
}
if (throwable instanceof ZuulException) {
// exception thrown by zuul lifecycle
return (ZuulException) throwable;
}
// fallback, should never get here
return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
}
public void setErrorPath(String errorPath) {
this.errorPath = errorPath;
}
}
1)在上述程式碼中,我們可以發現filter已經將相關的錯誤資訊放到request當中了:
request.setAttribute("javax.servlet.error.status_code", exception.nStatusCode);
request.setAttribute("javax.servlet.error.exception", exception);
request.setAttribute("javax.servlet.error.message", exception.errorCause);
2)錯誤處理完畢後,會轉發到 xxx/error的地址來處理
所以我們可以自定義ErrorController來實現異常的統一處理
第一步,建立MyErrorController 實現ErrorController
@RestController
public class MyErrorController implements ErrorController {
@Value("{error.path}")
private String errorPath;
@GetMapping(value = "{error.path}")
public ResponseEntity<ErrorBean> error(HttpServletRequest request) {
String message = request.getAttribute("javax.servlet.error.message").toString();
ErrorBean errorBean = new ErrorBean();
errorBean.setMessage(message);
errorBean.setReason("程式出錯");
return new ResponseEntity<ErrorBean>(errorBean, HttpStatus.BAD_GATEWAY);
}
public String getErrorPath() {
return errorPath;
}
}
public class ErrorBean {
private String message;
private String reason;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
}
第二步,配置檔案新增error.path
error:
path: /error
第三步,建立一個過濾器,在run方法內丟擲異常,用來測試效果。
@Component
public class ThrowExceptionFilter extends ZuulFilter{
private static Logger log = LoggerFactory.getLogger(ThrowExceptionFilter.class);
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
public boolean shouldFilter() {
return true;
}
public Object run() {
log.info("This is a pre filter, it will throw a RuntimeException");
doSomething();
return null;
}
private void doSomething() {
throw new RuntimeException("Exist some errors...");
}
}
第二種方式,禁用zuul預設的異常處理filter,使用自定義的ErrorFilter
第一步,禁用Zuul預設的異常處理filter—SendErrorFilter
zuul:
SendErrorFilter:
error:
disable: true
第二步,自定義ErrorFilter
/**
* 自定義異常處理Filter 需要禁用Zuul自帶的異常處理Filter--SendErrorFilter
*
* @author Administrator
*
*/
@Component
public class ErrorFilter extends ZuulFilter {
private static final Log log = LogFactory.getLog(ErrorFilter.class);
protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";
@Override
public String filterType() {
return ERROR_TYPE;
}
@Override
public int filterOrder() {
return SEND_ERROR_FILTER_ORDER;
}
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// only forward to errorPath if it hasn't been forwarded to already
return ctx.getThrowable() != null && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}
public Object run() {
try {
RequestContext context = RequestContext.getCurrentContext();
ZuulException exception = this.findZuulException(context.getThrowable());
log.error("進入系統異常攔截", exception);
HttpServletResponse response = context.getResponse();
response.setContentType("application/json; charset=utf8");
response.setStatus(exception.nStatusCode);
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.print("{code:" + exception.nStatusCode + ",message:\"" + exception.getMessage() + "\"}");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (writer != null) {
writer.close();
}
}
} catch (Exception var5) {
ReflectionUtils.rethrowRuntimeException(var5);
}
return null;
}
ZuulException findZuulException(Throwable throwable) {
if (throwable.getCause() instanceof ZuulRuntimeException) {
// this was a failure initiated by one of the local filters
return (ZuulException) throwable.getCause().getCause();
}
if (throwable.getCause() instanceof ZuulException) {
// wrapped zuul exception
return (ZuulException) throwable.getCause();
}
if (throwable instanceof ZuulException) {
// exception thrown by zuul lifecycle
return (ZuulException) throwable;
}
// fallback, should never get here
return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
}
}
第三步,測試
第三種方式,重寫錯誤資訊
在第一種方式中說到了Zuul的預設異常處理過濾器SendErrorFilter會把錯誤資訊forward到/error。/error端點來源於Spring Boot的org.springframework.boot.autoconfigure.web.BasicErrorController,它的邏輯如下:
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}
它通過getErrorAttributes方法來根據請求引數組織錯誤資訊返回結果,getErrorAttributes會將具體邏輯委託給org.springframework.boot.autoconfigure.web.ErrorAttributes來實現。
而org.springframework.boot.autoconfigure.web.DefaultErrorAttributes是ErrorAttributes的預設實現。在Error處理的自動化配置中,該介面採用了@ConditionalOnMissingBean修飾,說明DefaultErrorAttributes物件僅在ErrorAttributes介面的例項才會被建立使用,所以我們只需要自定義一個ErrorAttributes實現類來代替預設的DefaultErrorAttributes實現就可以達到控制錯誤資訊的效果。
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(ResourceProperties.class)
public class ErrorMvcAutoConfiguration {
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
}
第一步,建立自定義ErrorAttributes實現類
public class MyErrorAttributes extends DefaultErrorAttributes{
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> attributes = super.getErrorAttributes(webRequest, includeStackTrace);
System.out.println(attributes);
attributes.put("message","error");
return attributes;
}
}
第二步,在主類中配置Bean
@Bean
public DefaultErrorAttributes errorAttributes(){
return new MyErrorAttributes();
}
第三步,確認Zuul預設的異常處理過濾器SendErrorFilter是否開啟,測試
自定義ErrorAttributes前:
自定義ErrorAttributes後:
發現message資訊變成error了。這種方式侷限性還是很大的,輸出漢字會亂碼,不建議使用。
禁用Zuul過濾器
使用zuul.<SimpleClassName>.<filterType>.disable=true引數來實現禁用。
zuul.AccessFilter.pre.disable=true
zuul.SendErrorFilter.error.disable=true
其中<SimpleClassName>是代表過濾器類名(不能是包名+類名的形式),<filterType>代表過濾器型別。