配置
我們知道在SpringBoot中,第三方庫在META-INF/spring.factories
文件中指定自動配置文件。於是我們從spring-cloud-netflix-zuul-2.0.0.RC1.jar
的spring.factories
文件入手:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration,\
org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration
可以看到spring.factories
文件中指定了兩個類:ZuulServerAutoConfiguration
、ZuulProxyAutoConfiguration
用於自動配置。我們來看看ZuulServerAutoConfiguration
類中配置了哪些相關的bean,這裏我們主要講解CompositeRouteLocator
和SimpleRouteLocator
- CompositeRouteLocator
@Bean
@Primary
public CompositeRouteLocator primaryRouteLocator(
Collection<RouteLocator> routeLocators) {
return new CompositeRouteLocator(routeLocators);
}
首先定義了CompositeRouteLocator
,它是核心的路由定位器。路由定位器用於尋找特定路徑映射的路由。它有一個統一的接口RouteLocator
:
public interface RouteLocator {
// 獲取忽略的路徑
Collection<String> getIgnoredPaths();
// 獲取路由的列表
List<Route> getRoutes();
// 獲取指定路徑對應的路由
Route getMatchingRoute(String path);
}
RouteLocator
有三個實現類:SimpleRouteLocator
、DiscoveryClientRouteLocator
、CompositeRouteLocator
。它們的關係圖如下:
- ZuulHandlerMapping
@Bean
public ZuulController zuulController() {
return new ZuulController();
}
@Bean
public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
mapping.setErrorController(this.errorController);
return mapping;
}
ZuulHandlerMapping
是一個用於MVC處理的HandlerMapping
,它用於根據請求的path映射處理請求的Handler。
- ZuulServlet
@Bean
@ConditionalOnMissingBean(name = "zuulServlet")
public ServletRegistrationBean zuulServlet() {
ServletRegistrationBean<ZuulServlet> servlet = new ServletRegistrationBean<>(new ZuulServlet(),
this.zuulProperties.getServletPattern());
// The whole point of exposing this servlet is to provide a route that doesn't
// buffer requests.
servlet.addInitParameter("buffer-requests", "false");
return servlet;
}
這裏新建了一個ServletRegistrationBean
——Servlet的註冊器。通過這個ServletRegistrationBean
,向servlet容器中註冊了一個ZuulServlet。
- ZuulRefreshListener
@Bean
public ApplicationListener<ApplicationEvent> zuulRefreshRoutesListener() {
return new ZuulRefreshListener();
}
這裏註冊了一個事件監聽器,用於監聽事件來刷新路由。
路由的初始化
前文我們看到ZuulProxyAutoConfiguration
配置文件中配置了一個事件監聽器ZuulRefreshListener
來刷新路由。
當Spring啓動完成或者Eureka中的服務發生變化時都會發出事件,ZuulRefreshListener
收到事件之後進行路由的刷新。調用流程如下:
- ZuulRefreshListener.onApplicationEvent
- ZuulRefreshListener.reset
- ZuulHandlerMapping.setDirty
- CompositeRouteLocator.refresh
- DiscoveryClientRouteLocator.refresh
- SimpleRouteLocator.doRefresh
- DiscoveryClientRouteLocator.locateRoutes
DiscoveryClientRouteLocator.locateRoutes
方法是初始化路由的核心,主要分爲兩步:
- 調用
SimpleRouteLocator.locateRoutes
加載配置文件中的路由 - 將Eureka中註冊的service加載爲路由
接着上一篇RouteLocator
註冊ZuulHandlerMapping
我們來看看ZuulServerAutoConfiguration
類中配置了哪些相關的bean。
- ZuulHandlerMapping
@Bean
public ZuulController zuulController() {
return new ZuulController();
}
@Bean
public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
mapping.setErrorController(this.errorController);
return mapping;
}
ZuulHandlerMapping
是一個用於MVC處理的HandlerMapping
,它用於根據請求的path映射處理請求的Handler。
ZuulHandlerMapping在註冊發生在第一次請求發生的時候,在ZuulHandlerMapping.lookupHandler
方法中執行。調用流程如下:
- ZuulHandlerMapping.lookupHandler
- ZuulHandlerMapping.registerHandlers
在ZuulHandlerMapping.registerHandlers
方法中首先獲取所有的路由,然後調用AbstractUrlHandlerMapping.registerHandler
將路由中的路徑和ZuulHandlerMapping
相關聯。
ZuulHandlerMapping的工作原理
當接收到一個請求後,處理請求的過程統一在DispatcherServlet.doDispatch
中進行。
在DispatcherServlet.doDispatch
方法中調用DispatcherServlet.getHandler
方法獲取handler,在該方法中遍歷所有的HandlerMapping,調用其getHandler
方法獲得HandlerExecutionChain,如果不爲null說明正是我們要找的handler。
ZuulHandlerMapping的獲取
對於ZuulHandlerMapping的getHandler
方法的調用流程如下:
-
AbstractUrlHandlerMapping.getHandlerInternal:根據request的path查找匹配的handler
getHandlerInternal
方法根據lookupPath
(請求路徑)、request
(請求)調用ZuulHandlerMapping.lookupHandler
方法查找匹配的handler。ZuulHandlerMapping.lookupHandler
的調用流程如下:-
判斷是否在請求errorPath
-
請求的路徑是否處於routeLocator被忽略的路徑中
-
請求上下文中是否包含
forward.to
-
調用
AbstractUrlHandlerMapping.lookupHandler
AbstractUrlHandlerMapping.lookupHandler
的調用流程如下: -
檢查
handlerMap
中是否包含了請求路徑對應的Handler。(handlerMap
是在ZuulHandlerMapping執行registerHandlers()
方法是註冊的。將所有Route的路徑映射爲ZuulController) -
將請求路徑與
handlerMap
中的路徑進行匹配,將handlerMap
中匹配的路徑添加到matchingPatterns
列表中 -
從
matchingPatterns
列表中取得第一個路徑作爲最佳匹配的路徑bestMatch -
從
handlerMap
中獲取bestMatch對應的Handler,即ZuulController -
將handler、bestMatch等包裝成HandlerExecutionChain返回
-
-
因爲返回的handler不爲null,調用
getHandlerExecutionChain
將其包裝成HandlerExecutionChain,加入攔截器信息。返回executionChain
。
ZuulHandlerMapping的調用
ZuulHandlerMapping的調用發生在DispatcherServlet.doDispatch
執行時。調用流程如下:
- SimpleControllerHandlerAdapter.handle
- ZuulController.handleRequest
- ServletWrappingController.handleRequestInternal
- ZuulServlet.service
可以看到ZuulHandlerMapping最終調用了ZuulServlet.service
方法。
ZuulServlet
Zuul的主要流程發生在ZuulServlet中,它的調用流程如下:
- DispatcherServlet.doService
- DispatcherServlet.doDispatch
- SimpleControllerHandlerAdapter.handle
- ZuulController.handleRequest
- ServletWrappingController.handleRequestInternal
- ZuulServlet.service
在ZuulServlet.service
方法中,調用各個過濾器對請求進行處理,再將結果設置到response中返回:
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException { try { init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); // Marks this request as having passed through the "Zuul engine", as opposed to servlets // explicitly bound in web.xml, for which requests will not have the same data attached RequestContext context = RequestContext.getCurrentContext(); context.setZuulEngineRan(); try { preRoute(); } catch (ZuulException e) { error(e); postRoute(); return; } try { route(); } catch (ZuulException e) { error(e); postRoute(); return; } try { postRoute(); } catch (ZuulException e) { error(e); return; } } catch (Throwable e) { error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName())); } finally { RequestContext.getCurrentContext().unset(); }}
- 調用init方法。
ZuulServlet
中的方法都是對ZuulRunner
中方法的包裝,調用的是ZuulRunner.init
方法:它將HttpServletRequest
和HttpServletResponse
分撥包裝成HttpServletRequestWrapper
和HttpServletResponseWrapper
。然後將他們保存在RequestContext
中,RequestContext
保存在ThreadLocal中,每個請求線程都有不同的RequestContext
。 - 在
RequestContext
中加入zuulEngineRan=true
的鍵值對,表示這個請求經過Zuul的處理。 - 調用
preRoute()
、route()
、postRoute()
方法,對請求執行”pre”、”route”、”post”三種過濾器
過濾器
前文我們知道了過濾器的調用在ZuulServlet.service
方法中完成。過濾器是Zuul實現API網關功能最爲核心的部件,每一個進入Zuul的HTTP請求都會經過一系列的過濾器處理鏈得到請求響應並返回給客戶端。Zuul中的過濾器統一實現了ZuulFilter
抽象類,其中有四個抽象方法:
String filterType();int filterOrder();boolean shouldFilter();Object run();
它們各自的含義和功能總結如下:
- filterType:該函數需要返回一個字符串來代表過濾器的類型,Zuul默認定義了四種不同生命週期的過濾器類型:
pre
、routing
、post
、error
。 - filerOrder:通過int值來定義過濾器的執行順序,數值越小優先級越高。
- shouldFilter:返回一個boolean類型來判斷該過濾器是否要執行。我們可以通過此方法來指定過濾器的有效範圍。
- run:過濾器的具體邏輯。過濾器的具體邏輯。在該函數中,我們可以實現自定義的過濾邏輯,來確定是否要攔截當前的請求,不對其進行後續的路由,或是在請求路由返回結果之後,對處理結果做一些加工等。
過濾器的遍歷執行在FilterProcessor.runFilters
方法中:
public Object runFilters(String sType) throws Throwable { if (RequestContext.getCurrentContext().debugRouting()) { Debug.addRoutingDebug("Invoking {" + sType + "} type filters"); } boolean bResult = false; List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType); if (list != null) { for (int i = 0; i < list.size(); i++) { ZuulFilter zuulFilter = list.get(i); Object result = processZuulFilter(zuulFilter); if (result != null && result instanceof Boolean) { bResult |= ((Boolean) result); } } } return bResult;}
可以看到執行流程就是簡單的兩步:
-
調用
FilterLoader
的getFiltersByType
方法獲取響應類型的過濾器 -
遍歷這個類型下所有的過濾器,調用
FilterProcessor.processZuulFilter
方法執行過濾器方法。processZuulFilter
方法主要調用的是ZuulFilter.runFilter
方法,主要流程爲兩步:- 調用
shouldFilter()
方法判斷該過濾器是否該執行 - 如果
shouldFilter()
方法返回true
,則調用run()
方法執行過濾器中具體的方法
- 調用
pre過濾器
ServletDetectionFilter
- 執行順序:-3
- 執行條件:總是執行
- 功能:檢測當前請求是通過Spring的DispatcherServlet處理運行,還是通過ZuulServlet來處理運行。檢測結果會以布爾類型保存在
RequestContext
的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
或multipart/form-data
- 功能:將符合條件的請求包裝成
FormBodyRequestWrapper
對象
DebugFilter
-
執行順序:1
-
執行條件:請求中的
debug
參數(該參數可以通過zuul.debug.parameter
來自定義)爲true
,或者配置參數zuul.debug.request
爲true
-
功能:將當前
RequestContext
中的debugRouting
和debugRequest
參數設置爲true
。由於在同一個請求的不同聲明週期中,都可以訪問到這兩個值,所以我們在後續的各個過濾器中可以利用這兩個值來定義一下debug信息,這樣當線上環境出現問題的時候,可以通過請求參數的方式來激活這些debug信息以幫助分析問題。
PreDecorationFilter
- 執行順序:5
- 執行條件:RequestContext不存在
forward.to
和serviceId
兩個參數。如果有一個存在的話,說明當前請求已經被處理過了,因爲這兩個信息就是根據當前請求的路由信息加載進來的。
處理流程如下:
-
根據request獲取請求路徑
RequestContext ctx = RequestContext.getCurrentContext();final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
-
根據請求路徑獲取路由
Route route = this.routeLocator.getMatchingRoute(requestURI);
在
ZuulProxyAutoConfiguration
配置類中我們知道routeLocator是CompositeRouteLocator
,它的getMatchingRoute
方法如下:public Route getMatchingRoute(String path) { for (RouteLocator locator : routeLocators) { Route route = locator.getMatchingRoute(path); if (route != null) { return route; } } return null;}
可以看到它遍歷所有的路由定位器,返回匹配路徑的路由定位器。默認情況下,
routeLocators
中只有一個DiscoveryClientRouteLocator
。實際上這裏調用的就是DiscoveryClientRouteLocator.getMatchingRoute
方法,因爲DiscoveryClientRouteLocator
繼承了SimpleRouteLocator
,getMatchingRoute
方法實際上位於SimpleRouteLocator
類中。SimpleRouteLocator.getMatchingRoute
方法調用getSimpleMatchingRoute
,這個方法根據請求路徑獲取相應的Route,主流程代碼如下:getRoutesMap();String adjustedPath = adjustPath(path);ZuulRoute route = getZuulRoute(adjustedPath);return getRoute(route, adjustedPath);
- 調用
getRoutesMap
。如果路徑與路由的映射關係沒有初始化,則在getRoutesMap
方法中進行初始化 - 調用
getZuulRoute
方法,遍歷所有路由對應的路徑,根據請求路徑調用AntPathMatcher.match()
方法找到匹配的路由。 - 調用
getRoute
方法,將傳入的ZuulRoute
和path
包裝成Route
- 調用
-
根據是否能根據請求路徑找到路由,執行不同的路徑
路由存在
- 獲取Route的location(如果url不爲空返回url,否則返回serviceId)
- 在RequestContext中將
requestURI
設置爲路由的path - 在RequestContext中將
proxy
設置爲路由的id - 如果路由有自定義的sensitiveHeader(敏感的頭部信息,不向真實的請求傳遞),則將其添加到RequestContext的
ignoredHeaders
中。否則添加默認的sensitiveHeaders,包括Cookie
、Set-Cookie
、Authorization
。 - 如果路由的
retryable
不爲空,則將其添加到RequestContext的retryable
中 - 確定路由轉發的路徑:
- 如果location以
http
或https
開頭,將其添加到RequestContext的routeHost
中,在RequestContext的originResponseHeaders
中添加X-Zuul-Service
與location的鍵值對; - 如果location以
forward:
開頭,則將其添加到RequestContext的forward.to
中,將RequestContext的routeHost
設置爲null並返回; - 否則將location添加到RequestContext的
serviceId
中,將RequestContext的routeHost
設置爲null,在RequestContext的originResponseHeaders
中添加X-Zuul-ServiceId
與location的鍵值對。
- 如果location以
- 如果我們沒有將
zuul.addProxyHeaders
參數設置爲false
,則在RequestContext的zuulRequestHeaders
中添加一系列請求頭:X-Forwarded-Host
、X-Forwarded-Port
、X-Forwarded-Proto
、X-Forwarded-Prefix
、X-Forwarded-For
- 如果我們沒有將
zuul.addHostHeader
參數設置爲false
,則在則在RequestContext的zuulRequestHeaders
中添加host
路由不存在
在RequestContext中將forward.to
設置爲forwardURI
,默認情況下forwardURI
爲請求路徑。
route過濾器
RibbonRoutingFilter
- 執行順序:10
- 執行條件:RequestContext中的
routeHost
爲null,serviceId
不爲null。即只對通過serviceId配置路由規則的請求生效 - 功能:使用Ribbon和Hystrix來向服務實例發起請求,並將服務實例的請求結果返回
SimpleHostRoutingFilter
- 執行順序:100
- 執行條件:RequestContext中的
routeHost
不爲null。即只對通過url配置路由規則的請求生效 - 功能:直接向
routeHost
參數的物理地址發起請求,該請求是直接通過httpclient包實現的,而沒有使用Hystrix命令進行包裝,所以這類請求並沒有線程隔離和熔斷器的保護。
SendForwardFilter
- 執行順序:500
- 執行條件:RequestContext中的
forward.to
不爲null。即用來處理路由規則中的forward本地跳轉配置 - 功能:獲取
forward.to
中保存的跳轉地址,跳轉過去
error過濾器
SendErrorFilter
- 執行順序:0
- 執行條件:RequestContext中的
throwable
不爲null,且sendErrorFilter.ran
屬性爲false
。 - 功能:在request中設置
javax.servlet.error.status_code
、javax.servlet.error.exception
、javax.servlet.error.message
三個屬性。將RequestContext中的sendErrorFilter.ran
屬性設置爲true
。然後組織成一個forward到API網關/error
錯誤端點的請求來產生錯誤響應。
RequestContext中的sendErrorFilter.ran
屬性是爲了防止error過濾器處理完之後調用postRoute()
再一次發生異常,第二次發生的異常就不再處理。
post過濾器
SendResponseFilter
- 執行順序:1000
- 執行條件:沒有拋出異常,RequestContext中的
throwable
屬性爲null(如果不爲null說明已經被error過濾器處理過了,這裏的post過濾器就不需要處理了),並且RequestContext中zuulResponseHeaders
、responseDataStream
、responseBody
三者有一樣不爲null(說明實際請求的響應不爲空)。 - 功能:在請求響應中增加頭信息(根據設置有
X-Zuul-Debug-Header
、Date
、Content-Type
、Content-Length
等):addResponseHeaders
;發送響應內容:writeResponse
。