Spring cloud zuul 路由原理

配置

我們知道在SpringBoot中,第三方庫在META-INF/spring.factories文件中指定自動配置文件。於是我們從spring-cloud-netflix-zuul-2.0.0.RC1.jarspring.factories文件入手:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration,\
org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration

可以看到spring.factories文件中指定了兩個類:ZuulServerAutoConfigurationZuulProxyAutoConfiguration用於自動配置。我們來看看ZuulServerAutoConfiguration類中配置了哪些相關的bean,這裏我們主要講解CompositeRouteLocatorSimpleRouteLocator

  • 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有三個實現類:SimpleRouteLocatorDiscoveryClientRouteLocatorCompositeRouteLocator。它們的關係圖如下:

RouteLocato

  • 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收到事件之後進行路由的刷新。調用流程如下:

  1. ZuulRefreshListener.onApplicationEvent
  2. ZuulRefreshListener.reset
  3. ZuulHandlerMapping.setDirty
  4. CompositeRouteLocator.refresh
  5. DiscoveryClientRouteLocator.refresh
  6. SimpleRouteLocator.doRefresh
  7. DiscoveryClientRouteLocator.locateRoutes

DiscoveryClientRouteLocator.locateRoutes方法是初始化路由的核心,主要分爲兩步:

  1. 調用SimpleRouteLocator.locateRoutes加載配置文件中的路由
  2. 將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方法中執行。調用流程如下:

  1. ZuulHandlerMapping.lookupHandler
  2. ZuulHandlerMapping.registerHandlers

ZuulHandlerMapping.registerHandlers方法中首先獲取所有的路由,然後調用AbstractUrlHandlerMapping.registerHandler將路由中的路徑和ZuulHandlerMapping相關聯。

ZuulHandlerMapping的工作原理

當接收到一個請求後,處理請求的過程統一在DispatcherServlet.doDispatch中進行。

DispatcherServlet.doDispatch方法中調用DispatcherServlet.getHandler方法獲取handler,在該方法中遍歷所有的HandlerMapping,調用其getHandler方法獲得HandlerExecutionChain,如果不爲null說明正是我們要找的handler。

ZuulHandlerMapping的獲取

對於ZuulHandlerMapping的getHandler方法的調用流程如下:

  1. AbstractUrlHandlerMapping.getHandlerInternal:根據request的path查找匹配的handler

    getHandlerInternal方法根據lookupPath(請求路徑)、request(請求)調用ZuulHandlerMapping.lookupHandler方法查找匹配的handler。

    ZuulHandlerMapping.lookupHandler的調用流程如下:

    1. 判斷是否在請求errorPath

    2. 請求的路徑是否處於routeLocator被忽略的路徑中

    3. 請求上下文中是否包含forward.to

    4. 調用AbstractUrlHandlerMapping.lookupHandler

      AbstractUrlHandlerMapping.lookupHandler的調用流程如下:

    5. 檢查handlerMap中是否包含了請求路徑對應的Handler。(handlerMap是在ZuulHandlerMapping執行registerHandlers()方法是註冊的。將所有Route的路徑映射爲ZuulController)

    6. 將請求路徑與handlerMap中的路徑進行匹配,將handlerMap中匹配的路徑添加到matchingPatterns列表中

    7. matchingPatterns列表中取得第一個路徑作爲最佳匹配的路徑bestMatch

    8. handlerMap中獲取bestMatch對應的Handler,即ZuulController

    9. 將handler、bestMatch等包裝成HandlerExecutionChain返回

  2. 因爲返回的handler不爲null,調用getHandlerExecutionChain將其包裝成HandlerExecutionChain,加入攔截器信息。返回executionChain

ZuulHandlerMapping的調用

ZuulHandlerMapping的調用發生在DispatcherServlet.doDispatch執行時。調用流程如下:

  1. SimpleControllerHandlerAdapter.handle
  2. ZuulController.handleRequest
  3. ServletWrappingController.handleRequestInternal
  4. ZuulServlet.service

可以看到ZuulHandlerMapping最終調用了ZuulServlet.service方法。

ZuulServlet

Zuul的主要流程發生在ZuulServlet中,它的調用流程如下:

  1. DispatcherServlet.doService
  2. DispatcherServlet.doDispatch
  3. SimpleControllerHandlerAdapter.handle
  4. ZuulController.handleRequest
  5. ServletWrappingController.handleRequestInternal
  6. 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();    }}
  1. 調用init方法。ZuulServlet中的方法都是對ZuulRunner中方法的包裝,調用的是ZuulRunner.init方法:它將HttpServletRequestHttpServletResponse分撥包裝成HttpServletRequestWrapperHttpServletResponseWrapper。然後將他們保存在RequestContext中,RequestContext保存在ThreadLocal中,每個請求線程都有不同的RequestContext
  2. RequestContext中加入zuulEngineRan=true的鍵值對,表示這個請求經過Zuul的處理。
  3. 調用preRoute()route()postRoute()方法,對請求執行”pre”、”route”、”post”三種過濾器

過濾器

前文我們知道了過濾器的調用在ZuulServlet.service方法中完成。過濾器是Zuul實現API網關功能最爲核心的部件,每一個進入Zuul的HTTP請求都會經過一系列的過濾器處理鏈得到請求響應並返回給客戶端。Zuul中的過濾器統一實現了ZuulFilter抽象類,其中有四個抽象方法:

String filterType();int filterOrder();boolean shouldFilter();Object run();

它們各自的含義和功能總結如下:

  • filterType:該函數需要返回一個字符串來代表過濾器的類型,Zuul默認定義了四種不同生命週期的過濾器類型:preroutingposterror
  • 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;}

可以看到執行流程就是簡單的兩步:

  1. 調用FilterLoadergetFiltersByType方法獲取響應類型的過濾器

  2. 遍歷這個類型下所有的過濾器,調用FilterProcessor.processZuulFilter方法執行過濾器方法。

    processZuulFilter方法主要調用的是ZuulFilter.runFilter方法,主要流程爲兩步:

    1. 調用shouldFilter()方法判斷該過濾器是否該執行
    2. 如果shouldFilter()方法返回true,則調用run()方法執行過濾器中具體的方法

pre過濾器

ServletDetectionFilter

  • 執行順序:-3
  • 執行條件:總是執行
  • 功能:檢測當前請求是通過Spring的DispatcherServlet處理運行,還是通過ZuulServlet來處理運行。檢測結果會以布爾類型保存在RequestContextisDispatcherServletRequest參數中,這樣在後續的過濾器中,我們就可以通過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-urlencodedmultipart/form-data
  • 功能:將符合條件的請求包裝成FormBodyRequestWrapper對象

DebugFilter

  • 執行順序:1

  • 執行條件:請求中的debug參數(該參數可以通過zuul.debug.parameter來自定義)爲true,或者配置參數zuul.debug.requesttrue

  • 功能:將當前RequestContext中的debugRoutingdebugRequest參數設置爲true

    由於在同一個請求的不同聲明週期中,都可以訪問到這兩個值,所以我們在後續的各個過濾器中可以利用這兩個值來定義一下debug信息,這樣當線上環境出現問題的時候,可以通過請求參數的方式來激活這些debug信息以幫助分析問題。

PreDecorationFilter

  • 執行順序:5
  • 執行條件:RequestContext不存在forward.toserviceId兩個參數。如果有一個存在的話,說明當前請求已經被處理過了,因爲這兩個信息就是根據當前請求的路由信息加載進來的。

處理流程如下:

  1. 根據request獲取請求路徑

    RequestContext ctx = RequestContext.getCurrentContext();final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
    
  2. 根據請求路徑獲取路由

    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繼承了SimpleRouteLocatorgetMatchingRoute方法實際上位於SimpleRouteLocator類中。

    SimpleRouteLocator.getMatchingRoute方法調用getSimpleMatchingRoute,這個方法根據請求路徑獲取相應的Route,主流程代碼如下:

    getRoutesMap();String adjustedPath = adjustPath(path);ZuulRoute route = getZuulRoute(adjustedPath);return getRoute(route, adjustedPath);
    
    1. 調用getRoutesMap。如果路徑與路由的映射關係沒有初始化,則在getRoutesMap方法中進行初始化
    2. 調用getZuulRoute方法,遍歷所有路由對應的路徑,根據請求路徑調用AntPathMatcher.match()方法找到匹配的路由。
    3. 調用getRoute方法,將傳入的ZuulRoutepath包裝成Route
  3. 根據是否能根據請求路徑找到路由,執行不同的路徑

路由存在
  1. 獲取Route的location(如果url不爲空返回url,否則返回serviceId)
  2. 在RequestContext中將requestURI設置爲路由的path
  3. 在RequestContext中將proxy設置爲路由的id
  4. 如果路由有自定義的sensitiveHeader(敏感的頭部信息,不向真實的請求傳遞),則將其添加到RequestContext的ignoredHeaders中。否則添加默認的sensitiveHeaders,包括CookieSet-CookieAuthorization
  5. 如果路由的retryable不爲空,則將其添加到RequestContext的retryable
  6. 確定路由轉發的路徑:
    1. 如果location以httphttps開頭,將其添加到RequestContext的routeHost中,在RequestContext的originResponseHeaders中添加X-Zuul-Service與location的鍵值對;
    2. 如果location以forward:開頭,則將其添加到RequestContext的forward.to中,將RequestContext的routeHost設置爲null並返回;
    3. 否則將location添加到RequestContext的serviceId中,將RequestContext的routeHost設置爲null,在RequestContext的originResponseHeaders中添加X-Zuul-ServiceId與location的鍵值對。
  7. 如果我們沒有將zuul.addProxyHeaders參數設置爲false,則在RequestContext的zuulRequestHeaders中添加一系列請求頭:X-Forwarded-HostX-Forwarded-PortX-Forwarded-ProtoX-Forwarded-PrefixX-Forwarded-For
  8. 如果我們沒有將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_codejavax.servlet.error.exceptionjavax.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中zuulResponseHeadersresponseDataStreamresponseBody三者有一樣不爲null(說明實際請求的響應不爲空)。
  • 功能:在請求響應中增加頭信息(根據設置有X-Zuul-Debug-HeaderDateContent-TypeContent-Length等):addResponseHeaders;發送響應內容:writeResponse
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章