Spring zuul 快速入門實踐 --服務轉發實現解析

  zuul 做爲springCloud 的全家桶組件之一,有着不可或缺的份量。它做爲一個普通java API網關,自有網關的好處:html

    避免將內部信息暴露給外部;
    統一服務端應用入口;
    爲微服務添加額外的安全層;
    支持混合通訊協議;
    下降構建微服務的複雜性;
    微服務模擬與虛擬化;java

  zuul 基本上已經被springCloud 處理爲一個開箱即用的一個組件了,因此基本上只須要添加相應依賴和一些必要配置,該網關就能夠跑起來了。(這表面和nginx反向代理部分功能看起來是差很少的)nginx

  讓咱們來快速實踐一下吧!web

 

1、zuul入坑基本實踐步驟

1.1. 引入 pom 依賴

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <modelVersion>4.0.0</modelVersion>
    <groupId>zuul-test</groupId>
    <artifactId>com.youge</artifactId>
    <version>1.0</version>
    
    <!-- 引入spingcloud 全家桶 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.RC2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <dependencies>
        <!-- 導入服務網關zuul -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>

  以上就是咱們整個demo的所有maven依賴了,很簡潔吧。這也是springboot的初衷,把全部的工做都移到幕後,讓業務更簡潔。spring

 

1.2. 編寫網關入口類

  以下爲整個網關的入口類,實際上就是兩個註解發生了化學反應。@EnableZuulProxy 是本文的主角,它會開啓網關相關的服務。編程

@SpringBootApplication
@EnableZuulProxy
public class MyZuulGateway {
    // 只有一個空的main方法
    public static void main(String[] args) {
        SpringApplication.run(MyZuulGateway.class, args);
    }
}

  就是這麼簡單!後端

 

1.3. 添加測試配置項

  在application.properties配置文件中添加以下配置,主要使用一個路由配置驗證便可!api

server.port=9000
spring.application.name=my-zuul-gateway

#本地環境配置zuul轉發的規則:
# 忽略全部微服務,只路由指定微服務
# 以下配置爲將 /sapi/** 的路徑請求,轉發到 http://127.0.0.1:8082/fileenc/ 上去。
zuul.ignored-services=*
zuul.routes.fileenc1.url=http://127.0.0.1:8082/fileenc/
zuul.routes.fileenc1.path=/sapi/**

  如上就能夠將網關跑起來了,若是你連後臺服務也沒有,不要緊,本身寫一個就行了。瀏覽器

    @GetMapping("hello")
    public Object hello() {
        return "hello, world";
    }

  

1.4. 測試網關

  以上就已經將整個網關搞好了,run一下就ok. 測試方式就是直接瀏覽器裏訪問下該網關地址就行了:安全

http://localhost:9000/sapi/test/hello?a=1&b=22 .

  若是你看到 「hello, world」, 恭喜你,zuul已入坑。

 

2、zuul是如何轉發請求的?

  根據上面的觀察,zuul已經基本能夠知足咱們的開發需求了,後續更多要作的可能就是一些安全相關,業務相關,優化相關的東西了。不過在作這些以前,咱們能夠先多問一個問題,zuul是如何將請求轉發給後臺服務的呢?

  這實際上和zuul的架構相關:

 

  zuul的中核心概念是:Filter. 運行時邏輯上分爲多種類型的Filter,各種型Filter處理時機不一樣!  PRE:這種過濾器在請求被路由以前調用;ROUTING:這種過濾器將請求路由到微服務;POST:這種過濾器在路由到微服務之後執行;ERROR:在其餘階段發生錯誤時執行該過濾器;

  因此,總體上來講,它的轉發流程會通過一系列的過濾器,而後再進行實際的轉發。

若是隻想了解其最終是如何轉的能夠直奔主題,而若是要添加你的功能,則須要編寫一些對應生命週期的過濾器。

  本來要分析zuul是如何處理請求的,可是實際上,zuul被整合到spring以後,就徹底地符合了一個springmvc的編程模型了。全部對該網關的請求會先調用 ZuulController 進行請求的接收,而後到 service處理,再到響應這麼一個過程。

  整個 ZuulController 很是地簡單:就是一個請求的委託過程!

// org.springframework.cloud.netflix.zuul.web.ZuulController
public class ZuulController extends ServletWrappingController {

    public ZuulController() {
        setServletClass(ZuulServlet.class);
        setServletName("zuul");
        setSupportedMethods((String[]) null); // Allow all
    }

    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        try {
            // We don't care about the other features of the base class, just want to
            // handle the request
            return super.handleRequestInternal(request, response);
        }
        finally {
            // @see com.netflix.zuul.context.ContextLifecycleFilter.doFilter
            RequestContext.getCurrentContext().unset();
        }
    }

}
    // org.springframework.web.servlet.mvc.ServletWrappingController#handleRequestInternal
    /**
     * Invoke the wrapped Servlet instance.
     * @see javax.servlet.Servlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
     */
    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
            throws Exception {

        Assert.state(this.servletInstance != null, "No Servlet instance");
        // 該 servletInstance 是 ZuulServlet, 整個zuul的實現框架由其控制
        this.servletInstance.service(request, response);
        return null;
    }
    // com.netflix.zuul.http.ZuulServlet#service
    @Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            // 初始化請求,由 zuulRunner 處理
            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
            // setZuulEngineRan 會旋轉一個標識: "zuulEngineRan", true
            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();
        }
    }

  以上就是整個zuul對於普通請求的處理框架部分了。邏輯仍是比較清晰的,簡單的,前置+轉發+後置處理。咱們就幾個重點部分說明一下:

 

2.1. 請求初始化

  該部分主要是將外部請求,接入到 zuul 的處理流程上,固然下面的實現主要是使用了 ThreadLocal 實現了上下文的銜接。

    // com.netflix.zuul.http.ZuulServlet#init
    /**
     * initializes request
     *
     * @param servletRequest
     * @param servletResponse
     */
    void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
        zuulRunner.init(servletRequest, servletResponse);
    }
    // com.netflix.zuul.ZuulRunner#init
    /**
     * sets HttpServlet request and HttpResponse
     *
     * @param servletRequest
     * @param servletResponse
     */
    public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
        // RequestContext 使用 ThreadLocal 進行保存,且保證有值
        // 且 RequestContext 繼承了 ConcurrentHashMap, 保證了操做的線程安全
        RequestContext ctx = RequestContext.getCurrentContext();
        if (bufferRequests) {
            ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
        } else {
            ctx.setRequest(servletRequest);
        }

        ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
    }

  以上就是一個 zuul 請求的初始化了,簡單地說就是設置好請求上下文,備用。

 

2.2. 前置處理過濾器

  前置處理過濾器主要用於標記一些請求類型,權限驗證,安全過濾等等。是不可或缺一環。具體實現自行處理!咱們來看一個總體的通用流程:

    // com.netflix.zuul.http.ZuulServlet#preRoute
    /**
     * executes "pre" filters
     *
     * @throws ZuulException
     */
    void preRoute() throws ZuulException {
        zuulRunner.preRoute();
    }
    // com.netflix.zuul.ZuulRunner#preRoute
    /**
     * executes "pre" filterType  ZuulFilters
     *
     * @throws ZuulException
     */
    public void preRoute() throws ZuulException {
        // FilterProcessor 是個單例
        FilterProcessor.getInstance().preRoute();
    }
    // com.netflix.zuul.FilterProcessor#preRoute
    /**
     * runs all "pre" filters. These filters are run before routing to the orgin.
     *
     * @throws ZuulException
     */
    public void preRoute() throws ZuulException {
        try {
            // 調用Type 爲 pre 的過濾器
            runFilters("pre");
        } catch (ZuulException e) {
            throw e;
        } catch (Throwable e) {
            throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName());
        }
    }
    // com.netflix.zuul.FilterProcessor#runFilters
    /**
     * 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;
        // 經過 FilterLoader 的單例,獲取全部註冊爲 sType 的過濾器
        // 存放 Filters 的容器天然也是線程安全的,爲 ConcurrentHashMap
        // - org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter
        // - org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter
        // - org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter
        // - org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter
        // - org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter
        List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
        if (list != null) {
            for (int i = 0; i < list.size(); i++) {
                ZuulFilter zuulFilter = list.get(i);
                // 依次處理每一個 filter
                Object result = processZuulFilter(zuulFilter);
                if (result != null && result instanceof Boolean) {
                    bResult |= ((Boolean) result);
                }
            }
        }
        return bResult;
    }
    // 獲取相應的 filters
    // com.netflix.zuul.FilterLoader#getFiltersByType
    /**
     * Returns a list of filters by the filterType specified
     *
     * @param filterType
     * @return a List<ZuulFilter>
     */
    public List<ZuulFilter> getFiltersByType(String filterType) {

        List<ZuulFilter> list = hashFiltersByType.get(filterType);
        if (list != null) return list;

        list = new ArrayList<ZuulFilter>();

        Collection<ZuulFilter> filters = filterRegistry.getAllFilters();
        for (Iterator<ZuulFilter> iterator = filters.iterator(); iterator.hasNext(); ) {
            ZuulFilter filter = iterator.next();
            if (filter.filterType().equals(filterType)) {
                list.add(filter);
            }
        }
        Collections.sort(list); // sort by priority

        hashFiltersByType.putIfAbsent(filterType, list);
        return list;
    }

    // com.netflix.zuul.FilterProcessor#processZuulFilter
    /**
     * 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();
            }
            // 調用各filter的 runFilter() 方法,觸發filter做用
            // 若是filter被禁用,則不會調用 zuul.ServletDetectionFilter.pre.disable=true, 表明禁用 pre
            // 具體實現邏輯由各 filter 決定 
            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();
                    // 使用 StringBuilder 記錄請求處理日誌
                    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;
            }
        }
    }
    // com.netflix.zuul.ZuulFilter#runFilter
    /**
     * runFilter checks !isFilterDisabled() and shouldFilter(). The run() method is invoked if both are true.
     *
     * @return the return from ZuulFilterResult
     */
    public ZuulFilterResult runFilter() {
        ZuulFilterResult zr = new ZuulFilterResult();
        // 若是被禁用則不會觸發真正地調用
        if (!isFilterDisabled()) {
            // shouldFilter() 由各filter決定,返回true時執行filter
            if (shouldFilter()) {
                Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
                try {
                    Object res = run();
                    zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
                } catch (Throwable e) {
                    t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
                    zr = new ZuulFilterResult(ExecutionStatus.FAILED);
                    zr.setException(e);
                } finally {
                    t.stopAndLog();
                }
            } else {
                // 打上跳過標識
                zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
            }
        }
        return zr;
    }
    // org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter#run
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        if (!(request instanceof HttpServletRequestWrapper) 
                && isDispatcherServletRequest(request)) {
            ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, true);
        } else {
            ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, false);
        }

        return null;
    }

  如上,就是一個preFilter的處理流程了:

    1. 從 FilterLoader 中獲取全部 pre 類型的filter;
    2. 依次調用各filter的runFilter()方法,觸發filter;
    3. 調用前先調用 shouldFilter() 進行判斷該filter對於這次請求是否有用, 各filter實現能夠從上下文中取得相應的信息,各自斷定;
    4. 計數器加1;
    5. 默認就會有多個filter可調用, 不夠知足業務場景再自行添加;

 

2.3. 正常路由處理

  zuul 的本職工做,是對路徑的轉發路由(正向代理 or 反向代理),以下處理:

    // com.netflix.zuul.http.ZuulServlet#route
    /**
     * executes "route" filters
     *
     * @throws ZuulException
     */
    void route() throws ZuulException {
        zuulRunner.route();
    }
    // com.netflix.zuul.ZuulRunner#route
    /**
     * executes "route" filterType  ZuulFilters
     *
     * @throws ZuulException
     */
    public void route() throws ZuulException {
        FilterProcessor.getInstance().route();
    }
    // com.netflix.zuul.FilterProcessor#route
    /**
     * Runs all "route" filters. These filters route calls to an origin.
     *
     * @throws ZuulException if an exception occurs.
     */
    public void route() throws ZuulException {
        try {
            // 一樣,獲取filter類型爲 route 的 filters, 進行調用處理便可
            // - org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter
            // - org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter
            // - org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter
            runFilters("route");
        } catch (ZuulException e) {
            throw e;
        } catch (Throwable e) {
            throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName());
        }
    }
    // 其中,Ribbon 的處理須要有 ribbon 組件的引入和配置
    // org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter#shouldFilter
    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        // 判斷是否有 serviceId, 且 sendZuulResponse=true 纔會進行 ribbon 處理
        return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
                && ctx.sendZuulResponse());
    }
    
    如下是普通路由轉發的實現,只要配置了相應的路由信息,則會進行相關轉發:
    // org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#shouldFilter
    @Override
    public boolean shouldFilter() {
        return RequestContext.getCurrentContext().getRouteHost() != null
                && RequestContext.getCurrentContext().sendZuulResponse();
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        // step1. 構建http請求頭信息
        HttpServletRequest request = context.getRequest();
        MultiValueMap<String, String> headers = this.helper
                .buildZuulRequestHeaders(request);
        // step2. 構建 params 信息, 如: a=111&&b=222
        MultiValueMap<String, String> params = this.helper
                .buildZuulRequestQueryParams(request);
        // 獲取請求類型, GET,POST,PUT,DELETE
        String verb = getVerb(request);
        // step3. 構建請求體信息,如文件
        InputStream requestEntity = getRequestBody(request);
        // 若是沒有 Content-Length 字段,則設置 chunkedRequestBody:true
        if (getContentLength(request) < 0) {
            context.setChunkedRequestBody();
        }
        // step4. 構建要轉發的uri地址信息
        String uri = this.helper.buildZuulRequestURI(request);
        this.helper.addIgnoredHeaders();

        try {
            // step5. 請求轉發出去,等待響應
            // 具體如何轉發請求,是在 forward 中處理的
            CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,
                    headers, params, requestEntity);
            // 將結果放到上下文中,以備後續filter處理
            setResponse(response);
        }
        catch (Exception ex) {
            throw new ZuulRuntimeException(ex);
        }
        return null;
    }

    // step1. 構建http請求頭信息
    // org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper#buildZuulRequestHeaders
    public MultiValueMap<String, String> buildZuulRequestHeaders(
            HttpServletRequest request) {
        RequestContext context = RequestContext.getCurrentContext();
        MultiValueMap<String, String> headers = new HttpHeaders();
        Enumeration<String> headerNames = request.getHeaderNames();
        // 獲取全部的 header 信息,還原到 headers 中
        if (headerNames != null) {
            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                // 排除一些特別的的頭信息
                if (isIncludedHeader(name)) {
                    Enumeration<String> values = request.getHeaders(name);
                    while (values.hasMoreElements()) {
                        String value = values.nextElement();
                        headers.add(name, value);
                    }
                }
            }
        }
        // 添加本次路由轉發新增的頭信息
        Map<String, String> zuulRequestHeaders = context.getZuulRequestHeaders();
        for (String header : zuulRequestHeaders.keySet()) {
            headers.set(header, zuulRequestHeaders.get(header));
        }
        headers.set(HttpHeaders.ACCEPT_ENCODING, "gzip");
        return headers;
    }
    
    // step2. 構建 params 信息, 如: a=111&&b=222
    // org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper#buildZuulRequestQueryParams
    public MultiValueMap<String, String> buildZuulRequestQueryParams(
            HttpServletRequest request) {
        // 解析 getQueryString 中的 a=111&b=222... 信息
        Map<String, List<String>> map = HTTPRequestUtils.getInstance().getQueryParams();
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        if (map == null) {
            return params;
        }
        for (String key : map.keySet()) {
            for (String value : map.get(key)) {
                params.add(key, value);
            }
        }
        return params;
    }
    // 解析請求url中的k=v&k2=v2 爲 map 格式
    // com.netflix.zuul.util.HTTPRequestUtils#getQueryParams
    /**
     * returns query params as a Map with String keys and Lists of Strings as values
     * @return
     */
    public Map<String, List<String>> getQueryParams() {

        Map<String, List<String>> qp = RequestContext.getCurrentContext().getRequestQueryParams();
        if (qp != null) return qp;

        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();

        qp = new LinkedHashMap<String, List<String>>();

        if (request.getQueryString() == null) return null;
        StringTokenizer st = new StringTokenizer(request.getQueryString(), "&");
        int i;

        while (st.hasMoreTokens()) {
            String s = st.nextToken();
            i = s.indexOf("=");
            if (i > 0 && s.length() >= i + 1) {
                String name = s.substring(0, i);
                String value = s.substring(i + 1);

                try {
                    name = URLDecoder.decode(name, "UTF-8");
                } catch (Exception e) {
                }
                try {
                    value = URLDecoder.decode(value, "UTF-8");
                } catch (Exception e) {
                }

                List<String> valueList = qp.get(name);
                if (valueList == null) {
                    valueList = new LinkedList<String>();
                    qp.put(name, valueList);
                }

                valueList.add(value);
            }
            else if (i == -1)
            {
                String name=s;
                String value="";
                try {
                    name = URLDecoder.decode(name, "UTF-8");
                } catch (Exception e) {
                }
               
                List<String> valueList = qp.get(name);
                if (valueList == null) {
                    valueList = new LinkedList<String>();
                    qp.put(name, valueList);
                }

                valueList.add(value);
                
            }
        }

        RequestContext.getCurrentContext().setRequestQueryParams(qp);
        return qp;
    }

    
    // step3. 構建請求體信息,如文件
    // org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#getRequestBody
    protected InputStream getRequestBody(HttpServletRequest request) {
        InputStream requestEntity = null;
        try {
            // 先向 requestEntity 中獲取輸入流,若是沒有則向 servlet 中獲取
            requestEntity = (InputStream) RequestContext.getCurrentContext().get(REQUEST_ENTITY_KEY);
            if (requestEntity == null) {
                // 向 HttpServletRequest 中獲取原始的輸入流
                requestEntity = request.getInputStream();
            }
        }
        catch (IOException ex) {
            log.error("error during getRequestBody", ex);
        }
        return requestEntity;
    }
    
    
    // step4. 構建要轉發的uri地址信息
    // org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper#buildZuulRequestURI
    public String buildZuulRequestURI(HttpServletRequest request) {
        RequestContext context = RequestContext.getCurrentContext();
        // 原始請求 uri
        String uri = request.getRequestURI();
        // 路由轉換以後的請求 uri
        String contextURI = (String) context.get(REQUEST_URI_KEY);
        if (contextURI != null) {
            try {
                // 防止亂碼,urlencode 一下
                uri = UriUtils.encodePath(contextURI, characterEncoding(request));
            }
            catch (Exception e) {
                log.debug(
                        "unable to encode uri path from context, falling back to uri from request",
                        e);
            }
        }
        return uri;
    }
    
    // step5. 請求轉發出去,等待響應
    // 具體如何轉發請求,是在 forward 中處理的
    // org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#forward
    private CloseableHttpResponse forward(CloseableHttpClient httpclient, String verb,
            String uri, HttpServletRequest request, MultiValueMap<String, String> headers,
            MultiValueMap<String, String> params, InputStream requestEntity)
            throws Exception {
        Map<String, Object> info = this.helper.debug(verb, uri, headers, params,
                requestEntity);
        // 配置的路由地址前綴
        URL host = RequestContext.getCurrentContext().getRouteHost();
        HttpHost httpHost = getHttpHost(host);
        // 取出uri
        uri = StringUtils.cleanPath((host.getPath() + uri).replaceAll("/{2,}", "/"));
        long contentLength = getContentLength(request);

        ContentType contentType = null;

        if (request.getContentType() != null) {
            contentType = ContentType.parse(request.getContentType());
        }
        // 使用InputStreamEntity封裝inputStream請求,該inputStream是從socket接入後的原始輸入流
        // 後續 httpclient 進行數據讀取時,將由其進行提供相應讀數據方法
        InputStreamEntity entity = new InputStreamEntity(requestEntity, contentLength,
                contentType);
        // 構建本次要請求的數據,關鍵
        HttpRequest httpRequest = buildHttpRequest(verb, uri, entity, headers, params,
                request);
        try {
            log.debug(httpHost.getHostName() + " " + httpHost.getPort() + " "
                    + httpHost.getSchemeName());
            // 提交給 httpclient 組件執行 http 請求,並返回結果
            CloseableHttpResponse zuulResponse = forwardRequest(httpclient, httpHost,
                    httpRequest);
            this.helper.appendDebug(info, zuulResponse.getStatusLine().getStatusCode(),
                    revertHeaders(zuulResponse.getAllHeaders()));
            return zuulResponse;
        }
        finally {
            // When HttpClient instance is no longer needed,
            // shut down the connection manager to ensure
            // immediate deallocation of all system resources
            // httpclient.getConnectionManager().shutdown();
        }
    }
    // org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#buildHttpRequest
    protected HttpRequest buildHttpRequest(String verb, String uri,
            InputStreamEntity entity, MultiValueMap<String, String> headers,
            MultiValueMap<String, String> params, HttpServletRequest request) {
        HttpRequest httpRequest;
        String uriWithQueryString = uri + (this.forceOriginalQueryStringEncoding
                ? getEncodedQueryString(request) : this.helper.getQueryString(params));
        // 根據原始請求的不一樣類型,作相應類型的轉發
        // 如下請求處理,都包含了對 文件流一類請求的邏輯
        switch (verb.toUpperCase()) {
        case "POST":
            HttpPost httpPost = new HttpPost(uriWithQueryString);
            httpRequest = httpPost;
            httpPost.setEntity(entity);
            break;
        case "PUT":
            HttpPut httpPut = new HttpPut(uriWithQueryString);
            httpRequest = httpPut;
            httpPut.setEntity(entity);
            break;
        case "PATCH":
            HttpPatch httpPatch = new HttpPatch(uriWithQueryString);
            httpRequest = httpPatch;
            httpPatch.setEntity(entity);
            break;
        case "DELETE":
            BasicHttpEntityEnclosingRequest entityRequest = new BasicHttpEntityEnclosingRequest(
                    verb, uriWithQueryString);
            httpRequest = entityRequest;
            // DELETE 時會作兩步操做
            entityRequest.setEntity(entity);
            break;
        default:
            // 除以上幾種狀況,都使用 BasicHttpRequest 進行處理便可
            httpRequest = new BasicHttpRequest(verb, uriWithQueryString);
            log.debug(uriWithQueryString);
        }
        // 統一都設置請求頭,將map轉換爲 BasicHeader
        httpRequest.setHeaders(convertHeaders(headers));
        return httpRequest;
    }
    // org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#forwardRequest
    private CloseableHttpResponse forwardRequest(CloseableHttpClient httpclient,
            HttpHost httpHost, HttpRequest httpRequest) throws IOException {
        return httpclient.execute(httpHost, httpRequest);
    }

  可見整個真正的轉發流程,主要分幾步:

    1. 解析http請求頭信息,並添加本身部分的頭信息;
    2. 解析並保留請求參數信息, 如: a=111&&b=222;
    3. 獲取原始的inputStream信息,如文件;
    4. 根據路由配置,構建要轉發的uri地址信息;
    5. 使用httpclient組件,將請求轉發出去,並等待響應,設置到 response中;

  實際上,真正的轉發仍然是依次作好相應判斷,而後還原成對應的請求,再轉發後後端服務中。

  以上,就是一個普通的服務轉發實現了。並無太多的技巧,而是最基礎的步驟:接收請求,解析參數,從新構建請求,請求後端,得到結果。

 

2.4. 後置過濾器

  後置處理器能夠作一些請求完服務端以後,對客戶端的響應數據,包括正常數據流的輸出,錯誤信息的返回等。如 SendResponseFilter, SendErrorFilter...

    // com.netflix.zuul.http.ZuulServlet#postRoute
    /**
     * executes "post" ZuulFilters
     *
     * @throws ZuulException
     */
    void postRoute() throws ZuulException {
        zuulRunner.postRoute();
    }
    
    // com.netflix.zuul.ZuulRunner#postRoute
    /**
     * executes "post" filterType  ZuulFilters
     *
     * @throws ZuulException
     */
    public void postRoute() throws ZuulException {
        FilterProcessor.getInstance().postRoute();
    }

    // com.netflix.zuul.FilterProcessor#postRoute
    /**
     * runs "post" filters which are called after "route" filters. ZuulExceptions from ZuulFilters are thrown.
     * Any other Throwables are caught and a ZuulException is thrown out with a 500 status code
     *
     * @throws ZuulException
     */
    public void postRoute() throws ZuulException {
        try {
            // 獲取類型爲 post 的 filter, 調用
            // 默認爲: org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
            runFilters("post");
        } catch (ZuulException e) {
            throw e;
        } catch (Throwable e) {
            throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_POST_FILTER_" + e.getClass().getName());
        }
    }
    // org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#shouldFilter
    @Override
    public boolean shouldFilter() {
        // 有響應的數據,就能夠進行處理
        RequestContext context = RequestContext.getCurrentContext();
        return context.getThrowable() == null
                && (!context.getZuulResponseHeaders().isEmpty()
                    || context.getResponseDataStream() != null
                    || context.getResponseBody() != null);
    }
    // org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#run
    @Override
    public Object run() {
        try {
            // 添加header信息
            addResponseHeaders();
            // 輸出數據流到請求端
            writeResponse();
        }
        catch (Exception ex) {
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }
    // org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#addResponseHeaders
    private void addResponseHeaders() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletResponse servletResponse = context.getResponse();
        if (this.zuulProperties.isIncludeDebugHeader()) {
            @SuppressWarnings("unchecked")
            List<String> rd = (List<String>) context.get(ROUTING_DEBUG_KEY);
            if (rd != null) {
                StringBuilder debugHeader = new StringBuilder();
                for (String it : rd) {
                    debugHeader.append("[[[" + it + "]]]");
                }
                servletResponse.addHeader(X_ZUUL_DEBUG_HEADER, debugHeader.toString());
            }
        }
        // 向 response 中添加header
        List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
        if (zuulResponseHeaders != null) {
            for (Pair<String, String> it : zuulResponseHeaders) {
                servletResponse.addHeader(it.first(), it.second());
            }
        }
        if (includeContentLengthHeader(context)) {
            Long contentLength = context.getOriginContentLength();
            if(useServlet31) {
                servletResponse.setContentLengthLong(contentLength);
            } else {
                //Try and set some kind of content length if we can safely convert the Long to an int
                if (isLongSafe(contentLength)) {
                    servletResponse.setContentLength(contentLength.intValue());
                }
            }
        }
    }
    // org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#writeResponse()
    private void writeResponse() throws Exception {
        RequestContext context = RequestContext.getCurrentContext();
        // there is no body to send
        if (context.getResponseBody() == null
                && context.getResponseDataStream() == null) {
            return;
        }
        HttpServletResponse servletResponse = context.getResponse();
        if (servletResponse.getCharacterEncoding() == null) { // only set if not set
            servletResponse.setCharacterEncoding("UTF-8");
        }
        
        OutputStream outStream = servletResponse.getOutputStream();
        InputStream is = null;
        try {
            if (context.getResponseBody() != null) {
                String body = context.getResponseBody();
                is = new ByteArrayInputStream(
                                body.getBytes(servletResponse.getCharacterEncoding()));
            }
            else {
                is = context.getResponseDataStream();
                if (is!=null && context.getResponseGZipped()) {
                    // if origin response is gzipped, and client has not requested gzip,
                    // decompress stream before sending to client
                    // else, stream gzip directly to client
                    if (isGzipRequested(context)) {
                        servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");
                    }
                    else {
                        is = handleGzipStream(is);
                    }
                }
            }
            
            if (is!=null) {
                writeResponse(is, outStream);
            }
        }
        finally {
            /**
            * We must ensure that the InputStream provided by our upstream pooling mechanism is ALWAYS closed
            * even in the case of wrapped streams, which are supplied by pooled sources such as Apache's
            * PoolingHttpClientConnectionManager. In that particular case, the underlying HTTP connection will
            * be returned back to the connection pool iif either close() is explicitly called, a read
            * error occurs, or the end of the underlying stream is reached. If, however a write error occurs, we will
            * end up leaking a connection from the pool without an explicit close()
            *
            * @author Johannes Edmeier
            */
            if (is != null) {
                try {
                    is.close();
                }
                catch (Exception ex) {
                    log.warn("Error while closing upstream input stream", ex);
                }
            }

            try {
                Object zuulResponse = context.get("zuulResponse");
                if (zuulResponse instanceof Closeable) {
                    ((Closeable) zuulResponse).close();
                }
                outStream.flush();
                // The container will close the stream for us
            }
            catch (IOException ex) {
                log.warn("Error while sending response to client: " + ex.getMessage());
            }
        }
    }
    // org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#writeResponse
    private void writeResponse(InputStream zin, OutputStream out) throws Exception {
        // 默認大小 8192
        byte[] bytes = buffers.get();
        int bytesRead = -1;
        // 依次向 outputStream 中寫入字節流
        while ((bytesRead = zin.read(bytes)) != -1) {
            out.write(bytes, 0, bytesRead);
        }
    }

  一樣,對客戶端的輸出,就是這麼簡單:解析出header信息,將response write() 到客戶端的socket中。即完成任務。

  以上,咱們主要看了幾個很是普通的filter的處理過程,理解了下 zuul 的運行流程,固然主要的目的分析zuul是如何轉發請求的。基本上上面全部的filter都會繼承 ZuulFilter 的抽象,它提供兩個重要的統一的方法:isFilterDisabled() 和 shouldFilter() 方法用於控制過慮器是否啓用或者是否應該使用,並統一了返回結果。

 

   zuul 總體實現也是很是簡單明瞭,基於模板方法模式 和 責任鏈模式 和 單例模式,基本搞定。只是更多的花須要應用本身去玩了。

 

3、自行實現一個業務filter

  要想作到通用的框架,這點事情是必需要作的。固然,還必需要足夠簡單,以下:一個註解加一個繼承實現便可!

// 一個註解,@Component, 成功 spring bean 組件
// 一個繼承,ZuulFilter, 使用 zuul 能夠按照規範進行filter 的接入
@Component
public class MyOneFilter extends ZuulFilter {

    private final UrlPathHelper urlPathHelper = new UrlPathHelper();

    @Autowired
    private ZuulProperties zuulProperties;

    @Autowired
    private RouteLocator routeLocator;

    public MyOneFilter() {
    }

    public MyOneFilter(ZuulProperties zuulProperties,
                       RouteLocator routeLocator) {
        this.routeLocator = routeLocator;
        this.zuulProperties = zuulProperties;
    }

    @Override
    public String filterType() {
        // 自定義過濾器的類型,知道爲何不用枚舉類嗎?嘿嘿
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 定義過濾器的出場順序,越小越牛
        return 1;
    }

    @Override
    public boolean shouldFilter() {
        // 是否能夠啓用當前filter, 按你的業務規則來講了算
        return true;
    }

    @Override
    public Object run() {
        // 若是知足了過濾條件,你想怎麼作都行,RequestContext中有你想要的一切
        RequestContext ctx = RequestContext.getCurrentContext();
        Route route = routeLocator.getMatchingRoute(
                urlPathHelper.getPathWithinApplication(ctx.getRequest()));
        System.out.println("in my one filter");
        return null;
    }

}

  至於其餘配置項什麼的,自行查看官網便可! https://www.springcloud.cc/spring-cloud-greenwich.html#_router_and_filter_zuul

相關文章
相關標籤/搜索