1. 程式人生 > >Spring Cloud Netflix Zuul原始碼分析之請求處理篇-上

Spring Cloud Netflix Zuul原始碼分析之請求處理篇-上

微信公眾號:I am CR7
如有問題或建議,請在下方留言;
最近更新:2019-01-03

微信公眾號:I am CR7
如有問題或建議,請在下方留言
最近更新:2019-01-03

前言

經過前面兩篇文章的鋪墊,大戲正式上場。本文將對zuul是如何根據配置的路由資訊,轉發請求到後端微服務,進行詳細分析。補充一點:本著由淺入深的原則,這裡只對簡單URL方式進行分析,serviceId方式後續會單獨講解。

請求如何進入ZuulServlet

上一篇Spring Cloud Netflix Zuul原始碼分析之路由註冊篇簡化版doFilter()原始碼裡,我們講解了過濾器鏈的執行,下面,筆者將從servlet.service(request, response);入手,先來講一講,請求是如何進入到ZuulServlet中。

時序圖

ZuulServlet時序圖
ZuulServlet時序圖

高清大圖請看 user-gold-cdn.xitu.io/2019/1/3/16…

簡化版doDispatch原始碼

1protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
2    // 1、根據請求獲取handlerMapping對應的HandlerExecutionChain

3    HandlerExecutionChain mappedHandler = getHandler(processedRequest);
4    // 2、根據請求對應的handler,獲取對應的handlerAdapter
5    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
6    // 3、呼叫handlerAdapter的handle方法進行處理

7    ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
8    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
9}
複製程式碼

下面我們逐一來看每一行方法內部的原始碼。

1、getHandler()
 1protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
2    if (this.handlerMappings != null) {
3        // 之前初始化儲存到記憶體中的handlerMapping,遍歷查詢請求對應的Handler
4        for (HandlerMapping hm : this.handlerMappings) {
5            if (logger.isTraceEnabled()) {
6                logger.trace(
7                        "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
8            }
9            HandlerExecutionChain handler = hm.getHandler(request);
10            if (handler != null) {
11                return handler;
12            }
13        }
14    }
15    return null;
16}
複製程式碼

日誌:

12018-12-28 15:35:07.087 [http-nio-8558-exec-2] DEBUG o.s.c.n.zuul.web.ZuulHandlerMapping - Matching patterns for request [/role-api/info/7] are [/role-api/**]
22018-12-28 15:35:29.916 [http-nio-8558-exec-2] DEBUG o.s.c.n.zuul.web.ZuulHandlerMapping - URI Template variables for request [/role-api/info/7] are {}
32018-12-28 15:35:34.000 [http-nio-8558-exec-2] DEBUG o.s.c.n.zuul.web.ZuulHandlerMapping - Mapping [/role-api/info/7] to HandlerExecutionChain with handler [[email protected]f8] and 1 interceptor
複製程式碼

查詢handlerMappings後,找到了ZuulController,封裝到HandlerExecutionChain裡。

2、getHandlerAdapter()
 1protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
2    if (this.handlerAdapters != null) {
3        // 之前初始化儲存到記憶體中的handlerAdapters,遍歷查詢ZuulController對應的HandlerAdapter
4        for (HandlerAdapter ha : this.handlerAdapters) {
5            if (logger.isTraceEnabled()) {
6                logger.trace("Testing handler adapter [" + ha + "]");
7            }
8            // 檢視HandlerAdapter是否支援當前請求對應的Handler
9            if (ha.supports(handler)) {
10                return ha;
11            }
12        }
13    }
14    throw new ServletException("No adapter for handler [" + handler +
15            "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
16}
17
18// SimpleControllerHandlerAdapter
19@Override
20public boolean supports(Object handler) {
21    // ZuulController剛好滿足條件
22    return (handler instanceof Controller);
23}
複製程式碼

查詢handlerAdapters後,找到了ZuulController支援的SimpleControllerHandlerAdapter。

3、handle()
 1// SimpleControllerHandlerAdapter
2@Override
3@Nullable
4public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
5        throws Exception 
{
6
7    return ((Controller) handler).handleRequest(request, response);
8}
9
10// ZuulController
11@Override
12public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
13    try {
14        // We don't care about the other features of the base class, just want to
15        // handle the request
16        return super.handleRequestInternal(request, response);
17    }
18    finally {
19        // @see com.netflix.zuul.context.ContextLifecycleFilter.doFilter
20        RequestContext.getCurrentContext().unset();
21    }
22}
23
24// ServletWrappingController
25@Override
26protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
27        throws Exception 
{
28
29    Assert.state(this.servletInstance != null"No Servlet instance");
30    // 到這裡,我們就找到了呼叫ZuulServlet.service()的地方
31    this.servletInstance.service(request, response);
32    return null;
33}
複製程式碼

小結

至此,請求如何進入到ZuulServlet,我們就分析完畢了。小結一下:

  • 根據請求獲取對應的Handler[ZuulController]
  • 根據Handler獲取對應的HandlerAdapter[SimpleControllerHandlerAdapter]
  • 呼叫HandlerAdapter的handle方法

下面,我們開始講解本文最核心的部分:ZuulServlet的service方法。

ZuulServlet

時序圖

service時序圖
service時序圖

原始碼

 1Override
2public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException 
{
3    try {
4        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
5
6        // Marks this request as having passed through the "Zuul engine", as opposed to servlets
7        // explicitly bound in web.xml, for which requests will not have the same data attached
8        RequestContext context = RequestContext.getCurrentContext();
9        context.setZuulEngineRan();
10
11        try {
12            preRoute();
13        } catch (ZuulException e) {
14            error(e);
15            postRoute();
16            return;
17        }
18        try {
19            route();
20        } catch (ZuulException e) {
21            error(e);
22            postRoute();
23            return;
24        }
25        try {
26            postRoute();
27        } catch (ZuulException e) {
28            error(e);
29            return;
30        }
31
32    } catch (Throwable e) {
33        error(new ZuulException(e, 500"UNHANDLED_EXCEPTION_" + e.getClass().getName()));
34    } finally {
35        RequestContext.getCurrentContext().unset();
36    }
37}
複製程式碼

閱讀完上述原始碼,足以明白ZuulServlet的處理過程就是執行一系列過濾器。一共分為四種類型:前置、路由、後置、錯誤。下面按照正常請求,對各型別過濾器進行逐一講解。為了不讓大家困惑,這裡先列出筆者的思路:

  • 先讓大家知道,一次簡單URL請求經歷了哪些過濾器,日誌輸出了什麼
  • 再回頭去看日誌的輸出從何而來,深入具體的過濾器,進行原始碼分析

前置過濾器

Zuul預設提供了五個前置過濾器:

  • ServletDetectionFilter:執行順序為-3,五個前置過濾器中最先執行。

該過濾器總是會被執行,主要用來檢測當前請求是通過Spring的DispatcherServlet處理執行的,還是通過ZuulServlet來處理執行的。它的檢測結果會以布林型別儲存在當前請求上下文的isDispatcherServletRequest引數中,這樣後續的過濾器中,我們就可以通過RequestUtils.isDispatcherServletRequest()和RequestUtils.isZuulServletRequest()方法來判斷請求處理的源頭,以實現後續不同的處理機制。

  • Servlet30WrapperFilter:執行順序為-2,第二個執行的前置過濾器。

該過濾器總是會被執行,主要為了將原始的HttpServletRequest包裝成Servlet30RequestWrapper物件。

  • FormBodyWrapperFilter:執行順序為-1,第三個執行的前置過濾器。

該過濾器的執行有條件要求:要麼是Context-Type為application/x-www-form-urlencoded的請求,要麼是Context-Type為multipart/form-data,且是由String的DispatcherServlet處理的請求。主要用來將請求包裝成FormBodyRequestWrapper物件。

  • DebugFilter:執行順序為1,第四個執行的前置過濾器。

該過濾器的執行有兩個條件:要麼配置裡指定zuul.debug.request為true,要麼請求引數debug為true。主要用來將當前請求上下文中的debugRouting和debugRequest引數設定為true。這樣的好處是可以靈活的調整配置或者請求引數,來決定是否啟用debug模式,從而在後續過濾器中利用debug列印一些日誌,便於線上分析問題。

  • PreDecorationFilter:執行順序為5,最後一個執行的前置過濾器。

該過濾器的執行要求請求上下文中不存在forward.do和serviceId引數,如果有一個存在的話,說明當前請求已經被處理過了(因為這二個資訊就是根據當前請求的路由資訊載入進來的)。主要用來對當前請求做預處理操作,如路由的匹配,將匹配到的路由資訊存入請求上下文,便於後面的路由過濾器獲取。還包括為HTTP頭新增資訊,如X-Forwarded-Host、X-Forwarded-Port等等。

斷點圖
前置過濾器
前置過濾器
日誌
12018-12-29 14:35:13.785 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - Finding route for path: /role-api/info/7
22018-12-29 14:35:27.866 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - servletPath=/
32018-12-29 14:35:28.248 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - zuulServletPath=/zuul
42018-12-29 14:35:29.421 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - RequestUtils.isDispatcherServletRequest()=true
52018-12-29 14:35:30.492 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - RequestUtils.isZuulServletRequest()=false
62018-12-29 14:35:49.898 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - adjustedPath=/role-api/info/7
72018-12-29 14:36:09.935 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - Matching pattern:/user-api/**
82018-12-29 14:36:14.516 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - Matching pattern:/role-api/**
92018-12-29 14:36:23.042 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.filters.SimpleRouteLocator - route matched=ZuulRoute{id='role-api', path='/role-api/**', serviceId='null', url='http://localhost:9092/', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, }
複製程式碼

路由過濾器

Zuul預設提供了三個路由過濾器:

  • RibbonRoutingFilter:執行順序為10,三個路由過濾器中最先執行。

該過濾器的執行只有一個條件,那就是請求上下文中必須存在serviceId引數,即只對通過serviceId配置路由規則的請求生效,簡單URL請求不進行處理。

  • SimpleHostRoutingFilter:執行順序為100,第二個執行的路由過濾器。

該過濾器的執行只有一個條件,那就是請求上下文中必須存在routeHost引數,即只對通過url配置路由規則的請求生效。該過濾器呼叫原生的httpclient包,對routeHost引數對應的實體地址傳送請求,並沒有使用ribbon和hystrix,該類請求是沒有斷路器和執行緒隔離的保護機制。

  • SendForwardFilter:執行順序為500,最後一個執行的路由過濾器。

該過濾器的執行只有一個條件,那就是請求上下文中必須存在forward.do引數,即用來處理路由規則中的forward本地跳轉。

斷點圖
路由過濾器
路由過濾器
日誌
 12018-12-29 14:49:26.219 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.f.r.SimpleHostRoutingFilter - /info/7
22018-12-29 14:49:31.611 [http-nio-8558-exec-2] DEBUG o.s.c.n.z.f.r.SimpleHostRoutingFilter - localhost 9092 http
32018-12-29 14:49:50.557 [http-nio-8558-exec-2] DEBUG o.a.h.c.protocol.RequestAuthCache - Auth cache not set in the context
42018-12-29 14:49:50.584 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection request: [route: {}->http://localhost:9092][total kept alive: 0; route allocated: 0 of 1000; total allocated: 0 of 1000]
52018-12-29 14:49:50.907 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection leased: [id: 0][route: {}->http://localhost:9092][total kept alive: 0; route allocated: 1 of 1000; total allocated: 1 of 1000]
62018-12-29 14:49:50.941 [http-nio-8558-exec-2] DEBUG o.a.h.impl.execchain.MainClientExec - Opening connection {}->http://localhost:9092
72018-12-29 14:49:50.993 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.DefaultHttpClientConnectionOperator - Connecting to localhost/127.0.0.1:9092
82018-12-29 14:49:51.054 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.DefaultHttpClientConnectionOperator - Connection established 127.0.0.1:1172<->127.0.0.1:9092
92018-12-29 14:49:51.055 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.DefaultManagedHttpClientConnection - http-outgoing-0: set socket timeout to 10000
102018-12-29 14:49:51.056 [http-nio-8558-exec-2] DEBUG o.a.h.impl.execchain.MainClientExec - Executing request GET /info/7 HTTP/1.1
112018-12-29 14:49:51.056 [http-nio-8558-exec-2] DEBUG o.a.h.impl.execchain.MainClientExec - Target auth state: UNCHALLENGED
122018-12-29 14:49:51.059 [http-nio-8558-exec-2] DEBUG o.a.h.impl.execchain.MainClientExec - Proxy auth state: UNCHALLENGED
132018-12-29 14:49:51.118 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> GET /info/7 HTTP/1.1
142018-12-29 14:49:51.119 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> cache-control: no-cache
152018-12-29 14:49:51.119 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> postman-token: 70dbc6db-0aff-467b-a2d6-453b3627e856
162018-12-29 14:49:51.120 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> user-agent: PostmanRuntime/7.4.0
172018-12-29 14:49:51.120 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> accept: */*
182018-12-29 14:49:51.120 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> accept-encoding: gzip, deflate
192018-12-29 14:49:51.120 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> x-forwarded-host: localhost:8558
202018-12-29 14:49:51.120 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> x-forwarded-proto: http
212018-12-29 14:49:51.121 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> x-forwarded-prefix: /role-api
222018-12-29 14:49:51.121 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> x-forwarded-port: 8558
232018-12-29 14:49:51.121 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> x-forwarded-for: 0:0:0:0:0:0:0:1
242018-12-29 14:49:51.121 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> Host: localhost:9092
252018-12-29 14:49:51.121 [http-nio-8558-exec-2] DEBUG org.apache.http.headers - http-outgoing-0 >> Connection: Keep-Alive
262018-12-29 14:49:51.122 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "GET /info/7 HTTP/1.1[\r][\n]"
272018-12-29 14:49:51.122 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "cache-control: no-cache[\r][\n]"
282018-12-29 14:49:51.123 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "postman-token: 70dbc6db-0aff-467b-a2d6-453b3627e856[\r][\n]"
292018-12-29 14:49:51.123 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "user-agent: PostmanRuntime/7.4.0[\r][\n]"
302018-12-29 14:49:51.123 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "accept: */*[\r][\n]"
312018-12-29 14:49:51.123 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "accept-encoding: gzip, deflate[\r][\n]"
322018-12-29 14:49:51.124 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "x-forwarded-host: localhost:8558[\r][\n]"
332018-12-29 14:49:51.125 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "x-forwarded-proto: http[\r][\n]"
342018-12-29 14:49:51.126 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "x-forwarded-prefix: /role-api[\r][\n]"
352018-12-29 14:49:51.127 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "x-forwarded-port: 8558[\r][\n]"
362018-12-29 14:49:51.127 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "x-forwarded-for: 0:0:0:0:0:0:0:1[\r][\n]"
372018-12-29 14:49:51.127 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "Host: localhost:9092[\r][\n]"
382018-12-29 14:49:51.127 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "Connection: Keep-Alive[\r][\n]"
392018-12-29 14:49:51.128 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 >> "[\r][\n]"
402018-12-29 14:49:54.932 [http-nio-8558-exec-2] DEBUG org.apache.http.wire - http-outgoing-0 << "HTTP/1.1 200 [\r][\n]"
412018-12-29 14:49:54.933 [http-nio-8558-exec-2DEBUG org.apache.http.wire - http-outgoing-0 << "Content-Type: text/plain;charset=UTF-8[\r][\n]"
422018-12-29 14:49:54.933 [http-nio-8558-exec-2DEBUG org.apache.http.wire - http-outgoing-0 << "Content-Length: 30[\r][\n]"
432018-12-29 14:49:54.933 [http-nio-8558-exec-2DEBUG org.apache.http.wire - http-outgoing-0 << "Date: Sat29 Dec 2018 06:49:54 GMT[\r][\n]"
442018-12-29 14:49:54.933 [http-nio-8558-exec-2DEBUG org.apache.http.wire - http-outgoing-0 << "[\r][\n]"
452018-12-29 14:49:54.934 [http-nio-8558-exec-2DEBUG org.apache.http.wire - http-outgoing-0 << "hello I am is service Role-API"
462018-12-29 14:49:54.953 [http-nio-8558-exec-2DEBUG org.apache.http.headers - http-outgoing-0 << HTTP/1.1 200 
472018-12-29 14:49:54.954 [http-nio-8558-exec-2DEBUG org.apache.http.headers - http-outgoing-0 << Content-Type: text/plain;charset=UTF-8
482018-12-29 14:49:54.954 [http-nio-8558-exec-2DEBUG org.apache.http.headers - http-outgoing-0 << Content-Length: 30
492018-12-29 14:49:54.954 [http-nio-8558-exec-2DEBUG org.apache.http.headers - http-outgoing-0 << Date: Sat29 Dec 2018 06:49:54 GMT
502018-12-29 14:49:55.091 [http-nio-8558-exec-2DEBUG o.a.h.impl.execchain.MainClientExec - Connection can be kept alive indefinitely
51

複製程式碼

後置過濾器

Zuul預設提供了一個後置過濾器:

  • SendResponseFilter:執行順序為1000,只有這一個後置過濾器。

該過濾器執行條件要求請求上下文中必須包含請求響應相關的頭資訊或者響應資料流或者響應體,只要有一個滿足,就會執行。主要用來將請求上下文裡的響應資訊寫入到響應內容,返回給請求客戶端。

斷點圖
後置過濾器
後置過濾器
日誌
12018-12-29 14:54:02.786 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection [id: 0][route: {}->http://localhost:9092] can be kept alive indefinitely
22018-12-29 14:54:02.786 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.DefaultManagedHttpClientConnection - http-outgoing-0: set socket timeout to 0
32018-12-29 14:54:02.787 [http-nio-8558-exec-2] DEBUG o.a.h.i.c.PoolingHttpClientConnectionManager - Connection released: [id: 0][route: {}->http://localhost:9092][total kept alive: 1; route allocated: 1 of 1000; total allocated: 1 of 1000]
42018-12-29 14:55:33.931 [http-nio-8558-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapter completed request handling
52018-12-29 14:56:22.849 [http-nio-8558-exec-2] DEBUG o.s.web.servlet.DispatcherServlet - Successfully completed request
複製程式碼

小結

以上就是一次簡單URL請求在ZuulServlet的處理過程,下面我們深入研究三個重要的類,分別是:前置過濾器PreDecorationFilter、路由過濾器SimpleHostRoutingFilter、後置過濾器SendResponseFilter。因篇幅原因,後面內容請看Spring Cloud Netflix Zuul原始碼分析之請求處理篇-下

總結

到這裡,我們就講完了一個簡單URL請求在Zuul中整個處理過程。寫作過程中,筆者一直在思考,如何行文能讓大家更好的理解。雖然修改了很多次,但是還是覺得不夠完美,只能一邊寫一邊總結一邊改進。希望大家多多留言,給出意見和建議,那筆者真是感激不盡!!!最後,感謝大家的支援,祝新年快樂,祁琛,2019年1月3日。