1. 程式人生 > >深入理解Zuul之原始碼解析

深入理解Zuul之原始碼解析

Zuul 架構圖

zuul.png

在zuul中, 整個請求的過程是這樣的,首先將請求給zuulservlet處理,zuulservlet中有一個zuulRunner物件,該物件中初始化了RequestContext:作為儲存整個請求的一些資料,並被所有的zuulfilter共享。zuulRunner中還有 FilterProcessor,FilterProcessor作為執行所有的zuulfilter的管理器。FilterProcessor從filterloader 中獲取zuulfilter,而zuulfilter是被filterFileManager所載入,並支援groovy熱載入,採用了輪詢的方式熱載入。有了這些filter之後,zuulservelet首先執行的Pre型別的過濾器,再執行route型別的過濾器,最後執行的是post 型別的過濾器,如果在執行這些過濾器有錯誤的時候則會執行error型別的過濾器。執行完這些過濾器,最終將請求的結果返回給客戶端。

zuul工作原理原始碼分析

在之前已經講過,如何使用zuul,其中不可缺少的一個步驟就是在程式的啟動類加上@EnableZuulProxy,該EnableZuulProxy類程式碼如下:

@EnableCircuitBreaker
@EnableDiscoveryClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyConfiguration.class)
public @interface EnableZuulProxy {
}

其中,引用了ZuulProxyConfiguration,跟蹤ZuulProxyConfiguration,該類注入了DiscoveryClient、RibbonCommandFactoryConfiguration用作負載均衡相關的。注入了一些列的filters,比如PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter,程式碼如如下:

 @Bean
    public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator, ProxyRequestHelper proxyRequestHelper) {
        return new PreDecorationFilter(routeLocator, this.server.getServletPrefix(), this.zuulProperties,
                proxyRequestHelper);
    }

    // route filters
    @Bean
    public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper,
            RibbonCommandFactory<?> ribbonCommandFactory) {
        RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, ribbonCommandFactory, this.requestCustomizers);
        return filter;
    }

    @Bean
    public SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties zuulProperties) {
        return new SimpleHostRoutingFilter(helper, zuulProperties);
    }

它的父類ZuulConfiguration ,引用了一些相關的配置。在缺失zuulServlet bean的情況下注入了ZuulServlet,該類是zuul的核心類。

    @Bean
    @ConditionalOnMissingBean(name = "zuulServlet")
    public ServletRegistrationBean zuulServlet() {
        ServletRegistrationBean 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;
    }

同時也注入了其他的過濾器,比如ServletDetectionFilter、DebugFilter、Servlet30WrapperFilter,這些過濾器都是pre型別的。

 @Bean
    public ServletDetectionFilter servletDetectionFilter() {
        return new ServletDetectionFilter();
    }

    @Bean
    public FormBodyWrapperFilter formBodyWrapperFilter() {
        return new FormBodyWrapperFilter();
    }

    @Bean
    public DebugFilter debugFilter() {
        return new DebugFilter();
    }

    @Bean
    public Servlet30WrapperFilter servlet30WrapperFilter() {
        return new Servlet30WrapperFilter();
    }

它也注入了post型別的,比如 SendResponseFilter,error型別,比如 SendErrorFilter,route型別比如SendForwardFilter,程式碼如下:


    @Bean
    public SendResponseFilter sendResponseFilter() {
        return new SendResponseFilter();
    }

    @Bean
    public SendErrorFilter sendErrorFilter() {
        return new SendErrorFilter();
    }

    @Bean
    public SendForwardFilter sendForwardFilter() {
        return new SendForwardFilter();
    }

初始化ZuulFilterInitializer類,將所有的filter 向FilterRegistry註冊。

    @Configuration
    protected static class ZuulFilterConfiguration {

        @Autowired
        private Map<String, ZuulFilter> filters;

        @Bean
        public ZuulFilterInitializer zuulFilterInitializer(
                CounterFactory counterFactory, TracerFactory tracerFactory) {
            FilterLoader filterLoader = FilterLoader.getInstance();
            FilterRegistry filterRegistry = FilterRegistry.instance();
            return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
        }

    }

而FilterRegistry管理了一個ConcurrentHashMap,用作儲存過濾器的,並有一些基本的CURD過濾器的方法,程式碼如下:

 public class FilterRegistry {

    private static final FilterRegistry INSTANCE = new FilterRegistry();

    public static final FilterRegistry instance() {
        return INSTANCE;
    }

    private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>();

    private FilterRegistry() {
    }

    public ZuulFilter remove(String key) {
        return this.filters.remove(key);
    }

    public ZuulFilter get(String key) {
        return this.filters.get(key);
    }

    public void put(String key, ZuulFilter filter) {
        this.filters.putIfAbsent(key, filter);
    }

    public int size() {
        return this.filters.size();
    }

    public Collection<ZuulFilter> getAllFilters() {
        return this.filters.values();
    }

}

FilterLoader類持有FilterRegistry,FilterFileManager類持有FilterLoader,所以最終是由FilterFileManager注入 filterFilterRegistry的ConcurrentHashMap的。FilterFileManager到開啟了輪詢機制,定時的去載入過濾器,程式碼如下:

  void startPoller() {
        poller = new Thread("GroovyFilterFileManagerPoller") {
            public void run() {
                while (bRunning) {
                    try {
                        sleep(pollingIntervalSeconds * 1000);
                        manageFiles();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        poller.setDaemon(true);
        poller.start();
    }

Zuulservlet作為類似於Spring MVC中的DispatchServlet,起到了前端控制器的作用,所有的請求都由它接管。它的核心程式碼如下:


   @Override
    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(),可以發現這個方法為每個請求生成了RequestContext,RequestContext繼承了ConcurrentHashMap

  public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {

        RequestContext ctx = RequestContext.getCurrentContext();
        if (bufferRequests) {
            ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
        } else {
            ctx.setRequest(servletRequest);
        }

        ctx.setResponse(new HttpServletResponseWrapper(servletResponse));

  }


 public void preRoute() throws ZuulException {
    FilterProcessor.getInstance().preRoute();
}

而FilterProcessor類為呼叫filters的類,比如呼叫pre型別所有的過濾器:

  public void preRoute() throws ZuulException {
        try {
            runFilters("pre");
        } catch (ZuulException e) {
            throw e;
        } catch (Throwable e) {
            throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName());
        }
    }

跟蹤runFilters()方法,可以發現,它最終呼叫了FilterLoader的getFiltersByType(sType)方法來獲取同一類的過濾器,然後用for迴圈遍歷所有的ZuulFilter,執行了 processZuulFilter()方法,跟蹤該方法可以發現最終是執行了ZuulFilter的方法,最終返回了該方法返回的Object物件。

 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;
    }

route、post型別的過濾器的執行過程和pre執行過程類似。

Zuul預設過濾器

預設的核心過濾器一覽表

Zuul預設注入的過濾器,它們的執行順序在FilterConstants類,我們可以先定位在這個類,然後再看這個類的過濾器的執行順序以及相關的註釋,可以很輕鬆定位到相關的過濾器,也可以直接開啟
spring-cloud-netflix-core.jar的 zuul.filters包,可以看到一些列的filter,現在我以表格的形式,列出預設注入的filter.

過濾器 order 描述 型別
ServletDetectionFilter -3 檢測請求是用 DispatcherServlet還是 ZuulServlet pre
Servlet30WrapperFilter -2 在Servlet 3.0 下,包裝 requests pre
FormBodyWrapperFilter -1 解析表單資料 pre
SendErrorFilter 0 如果中途出現錯誤 error
DebugFilter 1 設定請求過程是否開啟debug pre
PreDecorationFilter 5 根據uri決定呼叫哪一個route過濾器 pre
RibbonRoutingFilter 10 如果寫配置的時候用ServiceId則用這個route過濾器,該過濾器可以用Ribbon 做負載均衡,用hystrix做熔斷 route
SimpleHostRoutingFilter 100 如果寫配置的時候用url則用這個route過濾 route
SendForwardFilter 500 用RequestDispatcher請求轉發 route
SendResponseFilter 1000 用RequestDispatcher請求轉發 post

過濾器的order值越小,就越先執行,並且在執行過濾器的過程中,它們共享了一個RequestContext物件,該物件的生命週期貫穿於請求,可以看出優先執行了pre型別的過濾器,並將執行後的結果放在RequestContext中,供後續的filter使用,比如在執行PreDecorationFilter的時候,決定使用哪一個route,它的結果的是放在RequestContext物件中,後續會執行所有的route的過濾器,如果不滿足條件就不執行該過濾器的run方法。最終達到了就執行一個route過濾器的run()方法。

而error型別的過濾器,是在程式發生異常的時候執行的。

post型別的過濾,在預設的情況下,只注入了SendResponseFilter,該型別的過濾器是將最終的請求結果以流的形式輸出給客戶單。

現在來看SimpleHostRoutingFilter是如何工作?

進入到SimpleHostRoutingFilter類的方法的run()方法,核心程式碼如下:

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        //省略程式碼

        String uri = this.helper.buildZuulRequestURI(request);
        this.helper.addIgnoredHeaders();

        try {
            CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,
                    headers, params, requestEntity);
            setResponse(response);
        }
        catch (Exception ex) {
            throw new ZuulRuntimeException(ex);
        }
        return null;
    }

查閱這個類的全部程式碼可知,該類建立了一個HttpClient作為請求類,並重構了url,請求到了具體的服務,得到的一個CloseableHttpResponse物件,並將CloseableHttpResponse物件的儲存到RequestContext物件中。並呼叫了ProxyRequestHelper的setResponse方法,將請求狀態碼,流等資訊儲存在RequestContext物件中。

private void setResponse(HttpResponse response) throws IOException {
        RequestContext.getCurrentContext().set("zuulResponse", response);
        this.helper.setResponse(response.getStatusLine().getStatusCode(),
                response.getEntity() == null ? null : response.getEntity().getContent(),
                revertHeaders(response.getAllHeaders()));
    }

現在來看SendResponseFilter是如何工作?

這個過濾器的order為1000,在預設且正常的情況下,是最後一個執行的過濾器,該過濾器是最終將得到的資料返回給客戶端的請求。

在它的run()方法裡,有兩個方法:addResponseHeaders()和writeResponse(),即新增響應頭和寫入響應資料流。


    public Object run() {
        try {
            addResponseHeaders();
            writeResponse();
        }
        catch (Exception ex) {
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }

其中writeResponse()方法是通過從RequestContext中獲取ResponseBody獲或者ResponseDataStream來寫入到HttpServletResponse中的,但是在預設的情況下ResponseBody為null,而ResponseDataStream在route型別過濾器中已經設定進去了。具體程式碼如下:

private void writeResponse() throws Exception {
        RequestContext context = RequestContext.getCurrentContext();

        HttpServletResponse servletResponse = context.getResponse();
            //程式碼省略
        OutputStream outStream = servletResponse.getOutputStream();
        InputStream is = null;
        try {
            if (RequestContext.getCurrentContext().getResponseBody() != null) {
                String body = RequestContext.getCurrentContext().getResponseBody();
                writeResponse(
                        new ByteArrayInputStream(
                                body.getBytes(servletResponse.getCharacterEncoding())),
                        outStream);
                return;
            }

            //程式碼省略
            is = context.getResponseDataStream();
            InputStream inputStream = is;
                //程式碼省略

            writeResponse(inputStream, outStream);
                //程式碼省略
            }
        }
        ..//程式碼省略
    }

如何在zuul上做日誌處理

由於zuul作為api閘道器,所有的請求都經過這裡,所以在閘道器上,可以做請求相關的日誌處理。
我的需求是這樣的,需要記錄請求的 url,ip地址,引數,請求發生的時間,整個請求的耗時,請求的響應狀態,甚至請求響應的結果等。
很顯然,需要實現這樣的一個功能,需要寫一個ZuulFliter,它應該是在請求傳送給客戶端之前做處理,並且在route過濾器路由之後,在預設的情況下,這個過濾器的order應該為500-1000之間。那麼如何獲取這些我需要的日誌資訊呢?找RequestContext,在請求的生命週期裡這個物件裡,儲存了整個請求的所有資訊。

現在編碼,在程式碼的註釋中,做了詳細的說明,程式碼如下:

@Component
public class LoggerFilter extends ZuulFilter {


    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        String method = request.getMethod();//氫氣的型別,post get ..
        Map<String, String> params = HttpUtils.getParams(request);
        String paramsStr = params.toString();//請求的引數
        long statrtTime = (long) context.get("startTime");//請求的開始時間
        Throwable throwable = context.getThrowable();//請求的異常,如果有的話
        request.getRequestURI();//請求的uri
        HttpUtils.getIpAddress(request);//請求的iP地址
        context.getResponseStatusCode();//請求的狀態
        long duration=System.currentTimeMillis() - statrtTime);//請求耗時

        return null;
    }

}

現在讀者也許有疑問,如何得到的statrtTime,即請求開始的時間,其實這需要另外一個過濾器,在網路請求route之前(大部分耗時都在route這一步),在過濾器中,在RequestContext儲存一個時間即可,另寫一個過濾器,程式碼如下:

@Component
public class AccessFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.set("startTime",System.currentTimeMillis());

        return null;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

可能還有這樣的需求,我需要將響應結果,也要儲存在log中,在之前已經分析了,在route結束後,將從具體服務獲取的響應流儲存在RequestContext中,在SendResponseFilter過濾器寫入在HttpServletResponse中,最終返回給客戶端。那麼我只需要在SendResponseFilter寫入響應流之前把響應流寫入到 log日誌中即可,那麼會引發另外一個問題,因為響應流寫入到 log後,RequestContext就沒有響應流了,在SendResponseFilter就沒有流輸入到HttpServletResponse中,導致客戶端沒有任何的返回資料,那麼解決的辦法是這樣的:

InputStream inputStream =RequestContext.getCurrentContext().getResponseDataStream();
InputStream newInputStream= copy(inputStream);
transerferTolog(inputStream);
RequestContext.getCurrentContext().setResponseDataStream(newInputStream);
  • 1
  • 2
  • 3
  • 4
  • 5

從RequestContext獲取到流之後,首先將流 copy一份,將流轉化下字串,存在日誌中,再set到RequestContext中,
這樣SendResponseFilter就可以將響應返回給客戶端。這樣的做法有點影響效能,如果不是字元流,可能需要做更多的處理工作。

--------------------- 本文來自 方誌朋 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/forezp/article/details/76211680?utm_source=copy