Springcloud原始碼學習筆記1—— Zuul閘道器原理

2023-01-10 06:00:49

系列文章目錄和關於我

原始碼基於 spring-cloud-netflix-zuul-2.2.6.RELEASE.jar
需要具備SpringMVC原始碼功底 推薦學習https://www.cnblogs.com/cuzzz/p/16538064.html

零丶概述

Zuul是netflix旗下開源閘道器,作為微服務系統的閘道器元件,是微服務請求的前門。微服務閘道器的作用有點類似於AOP,將負載均衡,限流熔斷等"橫切關注點"都集中於此,避免每一個服務都需要寫重複的功能(解決不了的問題,我們就加一層doge)

且微服務閘道器是統一入口,系統有多個服務,不能每個服務一個對外地址,使用者需要一個統一的系統入口進行操作,故zuul閘道器是系統的統一入口。

為微服務雲平臺提供統一的入口是API閘道器最主要的用途,除此之外,閘道器還可承擔認證授權、存取控制、路由、負載均衡、快取、紀錄檔、限流限額、轉換、對映、過濾、熔斷、註冊、服務編排、API管理、監控、統計分析等等非業務性的功能。

下面是zuul的架構圖,結合圖我們開始Zuul原始碼之旅。

一丶ZuulHandlerMapping 處理器對映器

zuul和spring的結合,請求到達服務的時候,tomcat會根據請求路徑呼叫到指定的servlet,這裡會呼叫到DispatcherServlet,DispatcherServlet會使用HandlerMapping處理器對映器找到當前請求的處理器Handler,這裡便會找到ZuulHandlerMapping

ZuulHandlerMapping是一個HandlerMapping,HandlerMapping稱為處理器對映器,這裡的處理器表示處理http請求,對映器表示根據請求中的一些特徵(一般是url,請求頭等)對映到一個處理器。當一個請求交由DispatcherServlet處理的時候,會首先通過處理器對映器找到合適的處理器

1.DispatcherServlet根據請求找到合適的處理器

DispatcherServlet會遍歷spring容器中的所有HandlerMapping的實現呼叫其getHandler方法找到處理器,然後和HandlerInterceptor一起包裝成HandlerExecutionChain(後續會先呼叫HandlerInterceptor的preHandle前置處理,postHandle後置處理,請求處理完畢後呼叫afterCompletion)

2.ZuulHandlerMapping 發光發熱

當一個請求呼叫到我們的zuul閘道器的時候,DispatcherServlet根據請求找到合適的處理器的這一步,會呼叫到zuul自動設定注入到spring容器中的ZuulHandlerMapping,那麼ZuulHandlerMapping 做了寫什麼呢

2.1利用RouteLocator註冊Handler

在ZuulHandlerMapping中,根據請求找合適的處理器最終交給lookupHandler方法,此方法呼叫registerHandlers註冊請求處理器

這裡出現一個新的類RouteLocator路由定位器,顧名思義就是用來獲取路由的方法

這裡的路由定位器是CompositeRouteLocator,其內部使用一個集合儲存其他的RouteLocator,一般zuul自動設定會為我們注入一個DiscoveryClientRouteLocator(父類別是SimpleRouteLocator,會讀取組態檔中的路由設定)其基於服務發現(DiscoveryClient#getServices方法)並且將微服務的serviceId如A註冊成路由/A/**,意味著A開頭的請求後續都將路由到微服務A

最終會將這些路由的url註冊到一個map當中,map的value是ZuulController

2.2.路徑匹配獲取請求處理器ZuulController

ZuulHandlerMapping是一個AbstractUrlHandlerMapping,顧名思義會根據url找對應的請求處理器,邏輯如下

這裡找到的請求處理器,必然是ZuulController(如果路徑和設定,或者服務發現中serviceId匹配上的話)

二丶ZuulController處理請求

1.將zuulController組裝成HandlerExecutionChain

HandlerExecutionChain內部使用陣列儲存攔截器HandlerInterceptor,在ZuulHandlerMapping找到對應的ZuulController(其實是一個單例物件,都是同一個),會從容器中拿到HandlerInterceptor型別的bean,和ZuulController包裝到一起生成HandlerExecutionChain

2.獲取ZuulController匹配的HandlerAdapter

想呼叫ZuulController?還需要其對應的HandlerAdapter,由於SpringMVC定義了多種處理器(比如RequestMappingHandlerAdapter(我們最常用的,基於@RequestMapping註解,反射呼叫Controller方法的處理器對應的介面卡)SimpleServletHandlerAdapter(適配Servlet),SimpleControllerHandlerAdapter(適配Controller介面物件)),為了遮蔽這種差異springmvc又定義了一個HandlerAdapter——處理器介面卡,使用介面卡模式,如同一個多功能轉接頭來適配多種處理器

找到合適處理器介面卡的程式碼如下

這裡自然找到的是SimpleControllerHandlerAdapter

隨後會執行攔截器HandlerInterceptor#preHandle方法

3.介面卡呼叫ZuulController處理請求

在攔截器的preHandler執行完成後,接下來會呼叫ZuulController#handleRequest,這一步是使用SimpleControllerHandlerAdapter進行的方法呼叫,我們看下ZuulController是如何處理請求的

ZuulController是一個ServletWrappingController(內部包裝servlet),對請求的處理最終交給ZuulServlet

兜兜轉轉最終還是來到了ZuulServlet,其本質是一個Servlet物件,看來Zuul處理Spring框架下可以用,普通的Servlet應用也可以使用呀

三丶ZuulServlet處理請求

1.ZuulServlet初始化ZuulRunner

在zuulServlet真正處理器請求之前,會初始化一個zuulRunner,ZuulRunner的init方法會被呼叫,zuul在這裡面初始化了一個基於ThreadLocal的請求上下文,ZuulRunner提供了preRoute,postRoute,routeerror方法,顧名思義分別在路由前,路由後,路由,以及發生錯誤的時候進行呼叫

2.ZuulServlet處理請求

可以看到ZuulServlet存在三個階段,preRoute,route,postRoute,如果出現異常那麼將呼叫errorRoute

這些方法的呼叫都委託給ZuulRunner進行,ZuulRunner會呼叫FilterProcessor.getInstance()對應的方法

最終是FilterLoader.getInstance()負責找到合適的Filter並且排序得到先後順序之後依次呼叫。

具體存在哪些Filter做了什麼事情,我們在ZuulFilter章節細說

3.ZuulFilter

ZuulFilter實現了Comparable 和 IZuulFilter

  • 實現Comparable ,並且定義了方法filterOrder,在比較方法中,起始是呼叫filterOrder得到順序然後進行比較

  • IZuulFilter 是Zuul定義的過濾器介面(注意不是Servlet規範中的過濾器)

    但是據我看到的原始碼,FilterLoader拿到的過濾器都是實現了ZuulFilter的

3.1 preRoute

上面原始碼我們說到了,對過濾器的呼叫,最終是委託給FilterLoader,在FilterRegistry中找到對應型別的過濾器,並且進行排序,然後依次呼叫。下面我們看下pre型別的過濾器有哪些,都做了什麼事情

3.1.1 ServletDetectionFilter

這個過濾器,負責呼叫ServletRequest#getAttribute方法判斷,請求是否通過DispatcherServlet處理,然後來到ZuulController,接著呼叫ZuulServlet來到此,判斷的依據就是經過DispatcherServlet處理的請求,會被呼叫DispatcherServlet#setAttribute(常數字串,WebApplicationContext web環境下spring上下文)

3.1.2 Servlet30WrapperFilter

負責將請求適配成Servlet3.0所規範的樣子

使用Servlet30RequestWrapper包裝原有的請求來實現,有點介面卡,又有點裝飾器設計模式的意思

3.1.3 FormBodyWrapperFilter

如果請求的content-type中帶有application/x-www-form-urlencodedmultipart/form-data

  • application/x-www-form-urlencoded:表示資料傳送到伺服器之前,所有字元都會進行編碼。屬於比較常用的編碼方式。
  • multipart/form-data:指表單資料有多部分構成,既有文字資料,又有檔案等二進位制資料的意思。

那麼FormBodyWrapperFilter 會生效,將對原本請求使用FormBodyRequestWrapper進行包裝,並提取原始請求的contentData、contentLength、contentType賦值到FormBodyRequestWrapper的屬性上

3.1.4 DebugFilter

如果請求中zuul.debug.parameter引數對應的值是true 那麼,將呼叫RequestContext#setDebugRouting(true)RequestContext#setDebugRequest(true)

這兩個設定的作用應該是讓zuul列印出一些幫助偵錯的紀錄檔資訊

3.1.5 PreDecorationFilter

根據提供的RouteLocator確定路由的位置和方式。還為下游請求設定各種與代理相關的檔頭。此類執行邏輯分支很多

3.2 route

route型別閘道器負責,將請求路由到對應的服務,其中最為關鍵的便是RibbonRoutingFilter,它基於Ribbon負載均衡

3.2.1 RibbonRoutingFilter

基於Ribbon負載均衡,實現服務呼叫的過濾器。

  • 構建RibbonCommandContext,就是對下游微服務的請求的上下文資料的封裝,會記錄目標服務的id,請求方式,請求資源uri,請求頭,請求引數,請求體等等(設定中通過sensitiveHeaders指定哪些請求頭不透傳)

  • forward是根據serviceId,和微服務中註冊的應用,依據負載均衡的策略,挑選一個健康的範例傳送請求

    • RibbonCommandFactory#Create

    其中FallbackProvider在呼叫失敗後,將呼叫fallbackResponse來進行服務降級

    RibbonLoadBalancingHttpClient 使用ribbon實現負載均衡的Http使用者端

    • RibbonCommand#execute

    其會根據ILoadBalance呼叫到IRule(ribbon負載均衡策略)選擇一個健康的範例,然後使用HttpClient傳送請求,包裝結果然後返回

  • setResponse就是在上下文中記錄下,forward呼叫得到的請求結果。

3.2.2 SendForwardFilter

使用RequestDispatcher轉發請求的Route ZuulFilter。一般是在組態檔中設定 location:forward.to或者設定url:forward:xxxx的時候會生效。會使用RequestDispatcher#forward來進行轉發

3.3 post

3.3.1 SendResponseFilter

負責將路由請求結果寫入到HttpServletResponse

  • 寫請求頭 呼叫HttpServletResponse#addHeader將請求頭加到HttpServletResponse物件中
  • 寫請求內容,就是使用流拷貝route階段的請求結果到HttpServletResponse

3.4 error

3.4.1 SendErrorFilter

如果前面階段的過濾器出現錯誤,將呼叫此過濾器,它負責使用RequestDispatcher將請求路由到/error

至此我們看完了zuul轉發請求,寫請求內容到`HttpServletResponse`的流程
接下來將回到 DispatchServlet 中

四丶DispatchServlet收尾工作

接下來便是呼叫HandlerInterceptor#postHandle然後呼叫HandlerInterceptor#afterCompletion,並推播一個ServletRequestHandledEvent事件其中記錄了請求資訊和處理時間等等資訊

最後DispatchServlet處理結束,tomcat負責將請求結果返回給呼叫方。

五丶擴充套件自己的ZuulFilter

實現ZuulFilter,並將自己的ZuulFilter註冊到Spring容器中,可以實現一些自定義操作。

ZuulFilter需要實現filterType(過濾器型別,字串,可以選擇pre route post)filterOrder(決定Filter的順序,值越大順序越後)

在公司我見過使用自定義的zuulFilter將Tracer注入到請求中,實現分散式鏈路紀錄檔