Java | zuul 1.x 是如何實(shí)現(xiàn)請(qǐng)求轉(zhuǎn)發(fā)的

文檔寫的再好,也不如源碼寫的好
源碼地址:
GitHub: https://github.com/Netflix/zuul
Gitee: https://gitee.com/github_mirror_plus/zuul


Table of Contents


簡(jiǎn)介

官方簡(jiǎn)介,其實(shí)你要看這篇,說(shuō)明你知道 zuul

Zuul is an edge service that provides dynamic routing, monitoring, resiliency, security, and more. Please view the wiki for usage, information, HOWTO, etc https://github.com/Netflix/zuul/wiki

Here are some links to help you learn more about the Zuul Project. Feel free to PR to add any other info, presentations, etc.

實(shí)現(xiàn)邏輯

上一篇文章 Go | Go 結(jié)合 Consul 實(shí)現(xiàn)動(dòng)態(tài)反向代理 里面簡(jiǎn)單的實(shí)現(xiàn)了一個(gè)反向代理,并簡(jiǎn)述了一下步驟,這里復(fù)述一下

根據(jù)代理的描述一共分成幾個(gè)步驟:

  1. 代理接收到客戶端的請(qǐng)求,復(fù)制了原來(lái)的請(qǐng)求對(duì)象
  2. 根據(jù)一些規(guī)則,修改新請(qǐng)求的請(qǐng)求指向
  3. 把新請(qǐng)求發(fā)送到根據(jù)服務(wù)器端,并接收到服務(wù)器端返回的響應(yīng)
  4. 將上一步的響應(yīng)根據(jù)需求處理一下,然后返回給客戶端

源碼

注意:這里的源碼指的是 1.x 分支的代碼

基于 Servlet 的請(qǐng)求轉(zhuǎn)發(fā)

在一開始學(xué)習(xí) Java Web 時(shí),Servlet 是一個(gè)繞不過(guò)去的坎,zuul 也是基于 Servlet 實(shí)現(xiàn)的,在源碼

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
  
    <listener>
        <listener-class>com.netflix.zuul.StartServer</listener-class>
    </listener>

    <servlet>
        <servlet-name>ZuulServlet</servlet-name>
        <servlet-class>com.netflix.zuul.http.ZuulServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>ZuulServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

    <filter>
        <filter-name>ContextLifecycleFilter</filter-name>
        <filter-class>com.netflix.zuul.context.ContextLifecycleFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>ContextLifecycleFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
  
</web-app>

在這里需要重點(diǎn)關(guān)注下 com.netflix.zuul.http.ZuulServletcom.netflix.zuul.context.ContextLifecycleFilter

ZuulServlet 核心代碼

代碼在 com.netflix.zuul.http.ZuulServlet

下面的代碼中省略了一部分,在這個(gè)過(guò)程中主要做了以下幾件事

  1. 將 將原始的 Request,Response 保存在 ThreadLocal 中,方便以后處理。因?yàn)?Tomcat 等 Servlet 容器默認(rèn)使用了一個(gè)請(qǐng)求一個(gè)線程處理的方式,所以存在 ThreadLocal 即可在以后的處理流程中方便處理
  2. 執(zhí)行前置過(guò)濾器 preRoute()
  3. 執(zhí)行轉(zhuǎn)發(fā)中過(guò)濾器 route()
  4. 執(zhí)行后置過(guò)濾器 postRoute()

其中轉(zhuǎn)發(fā)的關(guān)鍵就在 route() 方法

public class ZuulServlet extends HttpServlet {

    private ZuulRunner zuulRunner;

    @Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            // 這一步是將原始的 Request,Response 保存在 ThreadLocal 中
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                // 前置處理
                preRoute();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                // 轉(zhuǎn)發(fā)中處理
                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();
        }
    }

    void postRoute() throws ZuulException {
        zuulRunner.postRoute();
    }

    void route() throws ZuulException {
        zuulRunner.route();
    }

    void preRoute() throws ZuulException {
        zuulRunner.preRoute();
    }

    void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
        zuulRunner.init(servletRequest, servletResponse);
    }

    void error(ZuulException e) {
        RequestContext.getCurrentContext().setThrowable(e);
        zuulRunner.error();
    }
}

ZuulRunner 核心代碼

從上面的代碼可以看出轉(zhuǎn)發(fā)的關(guān)鍵在于 ZuulServlet#route(), 而 ZuulServlet#route() 在于 zuulRunner.route()

ZuulRunner 主要功能

  1. init 將 Request 和 Response 保存到 RequestContext.getCurrentContext(), 這里面就是上面提到的 ThreadLocal 的處理類
  2. 調(diào)用下 FilterProcessor.getInstance().route()
public class ZuulRunner {

    private boolean bufferRequests;

    public ZuulRunner() {
        this.bufferRequests = true;
    }

    public ZuulRunner(boolean bufferRequests) {
        this.bufferRequests = bufferRequests;
    }

    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 route() throws ZuulException {
        FilterProcessor.getInstance().route();
    }
}

RequestContext 核心代碼

主要是 ThreadLocal 和 copy

public class RequestContext extends ConcurrentHashMap<String, Object> {

    private static final Logger LOG = LoggerFactory.getLogger(RequestContext.class);

    protected static Class<? extends RequestContext> contextClass = RequestContext.class;

    private static RequestContext testContext = null;

    protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
        @Override
        protected RequestContext initialValue() {
            try {
                return contextClass.newInstance();
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
        }
    };

    /**
     * sets the "responseBody" value as a String. This is the response sent back to the client.
     *
     * @param body
     */
    public void setResponseBody(String body) {
        set("responseBody", body);
    }

    /**
     * Use this instead of response.setStatusCode()
     *
     * @param nStatusCode
     */
    public void setResponseStatusCode(int nStatusCode) {
        getResponse().setStatus(nStatusCode);
        set("responseStatusCode", nStatusCode);
    }

    /**
     * Mkaes a copy of the RequestContext. This is used for debugging.
     *
     * @return
     */
    public RequestContext copy() {
        RequestContext copy = new RequestContext();
        // 這里省略了一部分代碼,意思就是把原來(lái)的 request 深度復(fù)制一份
        return copy;
    }
}

FilterProcessor 核心代碼

主要邏輯就是找到對(duì)應(yīng) type 的 List<ZuulFilter> 并執(zhí)行 runFilter()

public class FilterProcessor {

    static FilterProcessor INSTANCE = new FilterProcessor();

    /**
     * @return the singleton FilterProcessor
     */
    public static FilterProcessor getInstance() {
        return INSTANCE;
    }

    /**
     * Runs all "route" filters. These filters route calls to an origin.
     *
     * @throws ZuulException if an exception occurs.
     */
    public void route() throws ZuulException {
        try {
            runFilters("route");
        } catch (ZuulException e) {
            throw e;
        } catch (Throwable e) {
            throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName());
        }
    }

    /**
     * runs all filters of the filterType sType/ Use this method within filters to run custom filters by type
     *
     * @param sType the filterType.
     * @return
     * @throws Throwable throws up an arbitrary exception
     */
    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;
    }

    /**
     * Processes an individual ZuulFilter. This method adds Debug information. Any uncaught Thowables are caught by this method and converted to a ZuulException with a 500 status code.
     *
     * @param filter
     * @return the return value for that filter
     * @throws ZuulException
     */
    public Object processZuulFilter(ZuulFilter filter) throws ZuulException {

        RequestContext ctx = RequestContext.getCurrentContext();
        boolean bDebug = ctx.debugRouting();
        final String metricPrefix = "zuul.filter-";
        long execTime = 0;
        String filterName = "";
        try {
            long ltime = System.currentTimeMillis();
            filterName = filter.getClass().getSimpleName();
            
            RequestContext copy = null;
            Object o = null;
            Throwable t = null;

            if (bDebug) {
                Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
                copy = ctx.copy();
            }
            
            ZuulFilterResult result = filter.runFilter();
            ExecutionStatus s = result.getStatus();
            execTime = System.currentTimeMillis() - ltime;

            switch (s) {
                case FAILED:
                    t = result.getException();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                    break;
                case SUCCESS:
                    o = result.getResult();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
                    if (bDebug) {
                        Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
                        Debug.compareContextState(filterName, copy);
                    }
                    break;
                default:
                    break;
            }
            
            if (t != null) throw t;

            usageNotifier.notify(filter, s);
            return o;

        } catch (Throwable e) {
            if (bDebug) {
                Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
            }
            usageNotifier.notify(filter, ExecutionStatus.FAILED);
            if (e instanceof ZuulException) {
                throw (ZuulException) e;
            } else {
                ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
                ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                throw ex;
            }
        }
    }

    /**
     * Publishes a counter metric for each filter on each use.
     */
    public static class BasicFilterUsageNotifier implements FilterUsageNotifier {
        private static final String METRIC_PREFIX = "zuul.filter-";

        @Override
        public void notify(ZuulFilter filter, ExecutionStatus status) {
            DynamicCounter.increment(METRIC_PREFIX + filter.getClass().getSimpleName(), "status", status.name(), "filtertype", filter.filterType());
        }
    }
}

通過(guò)上面的代碼中,可以看到得到簡(jiǎn)單的流程圖

zuul_filter.png

在官方示例中,提供了兩個(gè)簡(jiǎn)單的 Route 的 ZuulFilter 實(shí)現(xiàn)

SimpleHostRoutingFilter.groovy

在這個(gè)示例中,在 Filter 實(shí)現(xiàn)中將請(qǐng)求復(fù)制并轉(zhuǎn)發(fā)到目標(biāo)服務(wù),這個(gè)是簡(jiǎn)單的邏輯

class SimpleHostRoutingFilter extends ZuulFilter {

    // 聲明這個(gè)過(guò)濾器是 route 類型
    @Override
    String filterType() {
        return 'route'
    }

    // 過(guò)濾器的執(zhí)行邏輯
    Object run() {
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
        Header[] headers = buildZuulRequestHeaders(request)
        String verb = getVerb(request);
        InputStream requestEntity = request.getInputStream();
        CloseableHttpClient httpclient = CLIENT.get()

        String uri = request.getRequestURI()
        if (RequestContext.getCurrentContext().requestURI != null) {
            uri = RequestContext.getCurrentContext().requestURI
        }

        try {
            // 將請(qǐng)求轉(zhuǎn)發(fā)到指定服務(wù)器
            HttpResponse response = forward(httpclient, verb, uri, request, headers, requestEntity)
            setResponse(response)
        } catch (Exception e) {
            throw e;
        }
        return null
    }

    HttpResponse forward(CloseableHttpClient httpclient, String verb, String uri, HttpServletRequest request, Header[] headers, InputStream requestEntity) {

        requestEntity = debug(verb, uri, request, headers, requestEntity)
        HttpHost httpHost = getHttpHost()
        HttpRequest httpRequest;

        switch (verb) {
            case 'POST':
                httpRequest = new HttpPost(uri + getQueryString())
                InputStreamEntity entity = new InputStreamEntity(requestEntity, request.getContentLength())
                httpRequest.setEntity(entity)
                break
            case 'PUT':
                httpRequest = new HttpPut(uri + getQueryString())
                InputStreamEntity entity = new InputStreamEntity(requestEntity, request.getContentLength())
                httpRequest.setEntity(entity)
                break;
            default:
                httpRequest = new BasicHttpRequest(verb, uri + getQueryString())
        }

        try {
            httpRequest.setHeaders(headers)
            return forwardRequest(httpclient, httpHost, httpRequest)
        } finally {
            //httpclient.close();
        }
    }

    HttpResponse forwardRequest(HttpClient httpclient, HttpHost httpHost, HttpRequest httpRequest) {
        return httpclient.execute(httpHost, httpRequest);
    }
}

ZuulNFRequest 結(jié)合 Netflix 的 route 過(guò)濾器

這個(gè)示例中,從 HttpClient 轉(zhuǎn)發(fā)改為了使用 RibbonCommand 轉(zhuǎn)發(fā),從而使用了 Ribbon 的功能。關(guān)于 Ribbon 以后有時(shí)間再說(shuō)

class ZuulNFRequest extends ZuulFilter {

    @Override
    String filterType() {
        return 'route'
    }

    boolean shouldFilter() {
        return NFRequestContext.currentContext.getRouteHost() == null && RequestContext.currentContext.sendZuulResponse()
    }

    Object run() {
        NFRequestContext context = NFRequestContext.currentContext
        HttpServletRequest request = context.getRequest();

        MultivaluedMap<String, String> headers = buildZuulRequestHeaders(request)
        MultivaluedMap<String, String> params = buildZuulRequestQueryParams(request)
        Verb verb = getVerb(request);
        Object requestEntity = getRequestBody(request)
        IClient restClient = ClientFactory.getNamedClient(context.getRouteVIP());

        String uri = request.getRequestURI()
        if (context.requestURI != null) {
            uri = context.requestURI
        }
        //remove double slashes
        uri = uri.replace("http://", "/")

        HttpResponse response = forward(restClient, verb, uri, headers, params, requestEntity)
        setResponse(response)
        return response
    }

    def HttpResponse forward(RestClient restClient, Verb verb, uri, MultivaluedMap<String, String> headers, MultivaluedMap<String, String> params, InputStream requestEntity) {
        debug(restClient, verb, uri, headers, params, requestEntity)

//        restClient.apacheHttpClient.params.setVirtualHost(headers.getFirst("host"))

        String route = NFRequestContext.getCurrentContext().route
        if (route == null) {
            String path = RequestContext.currentContext.requestURI
            if (path == null) {
                path = RequestContext.currentContext.getRequest() getRequestURI()
            }
            route = "route" //todo get better name
        }
        route = route.replace("/", "_")


        RibbonCommand<AbstractLoadBalancerAwareClient<HttpRequest, HttpResponse>> command = new RibbonCommand<>(restClient, verb, uri, headers, params, requestEntity);
        try {
            HttpResponse response = command.execute();
            return response
        } catch (HystrixRuntimeException e) {
            if (e?.fallbackException?.cause instanceof ClientException) {
                ClientException ex = e.fallbackException.cause as ClientException
                throw new ZuulException(ex, "Forwarding error", 500, ex.getErrorType().toString())
            }
            throw new ZuulException(e, "Forwarding error", 500, e.failureType.toString())
        }
    }
}

總結(jié)

從 zuul 實(shí)現(xiàn)中看,還是基于 Servlet 的,并在過(guò)程中加入 前、中、后和異常處理鏈。因?yàn)榛?Servlet 其處理流程是阻塞的,性能會(huì)有所下降。

在 zuul 里面采用了 java 和 groovy 混合編程的方式,編程更加靈活。通過(guò)自定了一個(gè) GroovyCompiler 來(lái)加載指定路徑的 groovy 文件來(lái)實(shí)現(xiàn)在運(yùn)行中動(dòng)態(tài)添加 ZuulFilter 這種動(dòng)態(tài)機(jī)制在一定程度上實(shí)現(xiàn)了熱更新 ZuulFilter 功能,也是值得學(xué)習(xí)的。

參考

GitHub: https://github.com/Netflix/zuul

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容