• Spring zuul 快速入门实践 --服务转发实现解析


      zuul 作为springCloud 的全家桶组件之一,有着不可或缺的分量。它作为一个普通java API网关,自有网关的好处:

        避免将内部信息暴露给外部;
        统一服务端应用入口;
        为微服务添加额外的安全层;
        支持混合通信协议;
        降低构建微服务的复杂性;
        微服务模拟与虚拟化;

      zuul 基本上已经被springCloud 处理为一个开箱即用的一个组件了,所以基本上只需要添加相应依赖和一些必要配置,该网关就可以跑起来了。(这表面和nginx反向代理部分功能看起来是差不多的)

      让我们来快速实践一下吧!

    一、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的初衷,把所有的工作都移到幕后,让业务更简洁。

    1.2. 编写网关入口类

      如下为整个网关的入口类,实际上就是两个注解发生了化学反应。@EnableZuulProxy 是本文的主角,它会开启网关相关的服务。

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

      就是这么简单!

    1.3. 添加测试配置项

      在application.properties配置文件中添加如下配置,主要使用一个路由配置验证即可!

    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已入坑。

    二、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;
        }
        // run样例: 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可调用, 不够满足业务场景再自行添加,各业务执行方法为 run();

    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 整体实现也是非常简单明了,基于模板方法模式 和 责任链模式 和 单例模式,基本搞定。只是更多的花需要应用自己去玩了。

    三、自行实现一个业务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

    四、几点思考

          zuul 既然作为cloud的通用网关,必然会承受着比其他应用更大的流量,同时也要担起着比其他应用更高的QOC。可谓责任重大!

          然而纵观前面的实现,并没有什么牛逼的技术。相反,看到更多是为了业务的需要,需要进行反复的数据拷贝。

          很显然,网关类的服务,是非常典型的IO密集型应用,但似乎并没有看到它在这方面的努力(默认web服务器是tomcat,这就是其上限,如果换成netty又当如何)。也许,它还得需要前置网关,负载均衡,流量分发,才能够发挥其应有的作用。(把它当作普通应用就没事了,虽然它也在做负载均衡流量分发)

          对于大文件的上传,它是通过先将文件流存储到本地临时文件,再上传后端服务中,这个过程必然会导致响应缓慢以及应对异常能力的变弱。  而且,普通网关请求 zuul 中对于会从servlet中获取输入流,并转化为byte数组,也就是说他会保持全量上传数据,这对于超大文件来说,肯定是不可取的。所以,官网上也特别说你得如何小心处理大文件的上传!

          

  • 相关阅读:
    Python基础之内存管理与垃圾回收机制
    Git常用命令
    Git分支操作
    码云配置SSH公钥
    Git基本操作
    Git基本理论
    版本控制
    Python基础之Python语法
    成为一名JAVA高级工程师你需要学什么【转】
    一个java高级工程师的进阶之路【转】
  • 原文地址:https://www.cnblogs.com/yougewe/p/13062471.html
Copyright © 2020-2023  润新知