SpringCloud源码解析 -- Zuul实现原理

本文主要分享Zuul的使用和原理。

因为工作需要,我第一个深入了解的SpringCloud组件其实是Zuul,希望这篇文章能说清楚Zuul的相关实现原理。

Zuul通过ZuulFilter对请求进行拦截,过滤,转发等操作。ZuulFilter也是提供给我们扩展的接口。
ZuulFilter有四种类型
pre:在请求被路由之前调用,主要负责过滤,request请求处理等工作
route:负责请求路由,转发工作
post:负责发送响应到客户端
error:上面流程发生错误时被调用,做一些异常善后工作

Zuul的整体流程在ZuulServlet或ZuulServletFilter,这两个类功能基本一样,默认使用的是ZuulServlet,在ZuulServerAutoConfiguration初始化。
ZuulServlet#service

public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
    try {
        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

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

整体流程如下

pre -->  route   --> post --> 客户端
 |         |          |  
 |         |          | 
error     error      error
 |         |
 |         |
post      post

注意:ZuulServlet#init -> ZuulRunner#init,该方法会为当前线程构造一个RequestContext,并设置Request,Response。

我们可以添加新的ZuulFilter实现我们需要的功能,不过了解Zuul自带的ZuulFilter可以帮助我们更深入了解Zuul

过滤器 order 类型 描述
ServletDetectionFilter -3 pre 检测请求是使用DispatcherServlet还是ZuulServlet
Servlet30WrapperFilter -2 pre 在Servlet 3.0下,转化RequestWrapper,Zull默认使用Servlet 2.5的RequestWrapper
FormBodyWrapperFilter -1 pre 将request转化为FormBodyRequestWrapper,它可以解析表单数据
SendErrorFilter 0 error 处理流程中出现的错误
DebugFilter 1 pre 设置请求过程是否开启debug
PreDecorationFilter 5 pre 根据请求uri决定调用哪一个route过滤器
RibbonRoutingFilter 10 route 如果通过ServiceId转发请求,则使用这个route过滤器
SimpleHostRoutingFilter 100 route 如果通过url转发请求,则用这个route过滤
SendForwardFilter 500 route 如果使用forward转发请求,则用这个route过滤
LocationRewriteFilter 900 post 重写Http的Location头部到Zuul的URL
SendResponseFilter 1000 post 发送响应数据到客户端

Zuul中支持三种转发配置

# serviceId转发
zuul.routes.goods-service.path=/goods-service/**
zuul.routes.goods-service.serviceId=goods-service
# url转发
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.url=http://localhost:9002/
# forward转发
zuul.routes.config.path=/config/**
zuul.routes.config.url=forward:/config

分别由RibbonRoutingFilter,SimpleHostRoutingFilter,SendForwardFilter处理。

下面看一下核心ZuulFilter的实现。
先看一下PreDecorationFilter。
PreDecorationFilter#run -> CompositeRouteLocator#getMatchingRoute -> SimpleRouteLocator#getMatchingRoute

protected Route getSimpleMatchingRoute(final String path) {
    ...
    String adjustedPath = adjustPath(path);
    // #1
    ZuulRoute route = getZuulRoute(adjustedPath);
    // #2
    return getRoute(route, adjustedPath);
}

#1 SimpleRouteLocator#routes是一个Map引用,键值对为上面配置中的path和ZuulRoute,ZuulRoute中包含了serviceId,url,stripPrefix等配置信息
getZuulRoute方法中使用AntPathMatcher匹配请求url与配置path。
#2 使用ZuulRoute#getRoute构造对应的Route
我们也可以继承SimpleRouteLocator并重写getRoute做一些个性化处理。

RibbonRoutingFilter负责处理serviceId转发,它集成了Ribbon和hystrix组件,提供负载均衡和熔断等功能。
RibbonRoutingFilter#run -> forward

protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
    Map<String, Object> info = this.helper.debug(context.getMethod(),
            context.getUri(), context.getHeaders(), context.getParams(),
            context.getRequestEntity());
    // #1
    RibbonCommand command = this.ribbonCommandFactory.create(context);
    try {
        // #2
        ClientHttpResponse response = command.execute();
        this.helper.appendDebug(info, response.getRawStatusCode(),
                response.getHeaders());
        return response;
    }
    catch (HystrixRuntimeException ex) {
        return handleException(info, ex);
    }
}

#1 RibbonCommand继承了HystrixExecutable接口,有RestClientRibbonCommand,OkHttpRibbonCommand,HttpClientRibbonCommand实现类,都继承于AbstractRibbonCommand。
RibbonCommandFactory是工厂类,对应实现类为RestClientRibbonCommandFactory,OkHttpRibbonCommandFactory,HttpClientRibbonCommandFactory,分别构造对应的RibbonCommand,都继承于AbstractRibbonCommandFactory,默认使用HttpClientRibbonCommandFactory,在RibbonCommandFactoryConfiguration中初始化。
#2 执行RibbonCommand

HttpClientRibbonCommandFactory#create

public HttpClientRibbonCommand create(final RibbonCommandContext context) {
    FallbackProvider zuulFallbackProvider = getFallbackProvider(
            context.getServiceId());
    final String serviceId = context.getServiceId();
    // #1
    final RibbonLoadBalancingHttpClient client = this.clientFactory
            .getClient(serviceId, RibbonLoadBalancingHttpClient.class);
    client.setLoadBalancer(this.clientFactory.getLoadBalancer(serviceId));
    // #2
    return new HttpClientRibbonCommand(serviceId, client, context, zuulProperties,
            zuulFallbackProvider, clientFactory.getClientConfig(serviceId));
}

#1 构造一个RibbonLoadBalancingHttpClient,实现了IClient接口,负责真正发起请求的操作,有对应子类OkHttpLoadBalancingClient,RestClient,RibbonLoadBalancingHttpClient,都继承于AbstractLoadBalancerAwareClient。 默认使用的是RibbonLoadBalancingHttpClient,在HttpClientRibbonConfiguration初始化。
RibbonCommand通过实现HystrixExecutable实现熔断,而负载均衡功能则是通过AbstractLoadBalancerAwareClient实现的。

注意client是与serviceId绑定的。所以ribbon.ConnectTimeoutribbon.ReadTimeout可以配置在一个serviceId上,如goods-service.ribbon.ReadTimeout

#2 构造一个HttpClientRibbonCommand
注意这里使用serviceId作为hystrix的commandkey,也就是说Zuul支持对应用级别做熔断,但不支持url级别的熔断。

回到RibbonRoutingFilter#forward方法#2步骤,HystrixExecutable#execute -> AbstractRibbonCommand#run -> AbstractLoadBalancerAwareClient#executeWithLoadBalancer

public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
    LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);
    try {
        // #1
        return command.submit(
            new ServerOperation<T>() {
                public Observable<T> call(Server server) {
                    // #2
                    URI finalUri = reconstructURIWithServer(server, request.getUri());
                    S requestForServer = (S) request.replaceUri(finalUri);
                    try {
                        // #3
                        return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                    } 
                    catch (Exception e) {
                        return Observable.error(e);
                    }
                }
            })
            .toBlocking()
            .single();
    } 
    ...
}

#1 LoadBalancerCommand#submit生成一个Observable,它是RxJava提供的类,表示一个可观察对象,它可以产生数据, 最后执行toBlocking().single()会阻塞直到产生第一个结果才返回。
#2 通过server,获取真正请求的url
#3 通过IClient#execute调用下游服务

LoadBalancerCommand#submit

public Observable<T> submit(final ServerOperation<T> operation) {
    ...
    // #1
    Observable<T> o = 
            (server == null ? selectServer() : Observable.just(server))
            .concatMap(new Func1<Server, Observable<T>>() {
                public Observable<T> call(Server server) {
                    context.setServer(server);
                    final ServerStats stats = loadBalancerContext.getServerStats(server);
                    // #2
                    Observable<T> o = Observable
                            .just(server)
                            .concatMap(new Func1<Server, Observable<T>>() {
                                public Observable<T> call(final Server server) {
                                    ...
                                    // #3
                                    return operation.call(server).doOnEach(new Observer<T>() {
                                        ...
                                    });
                                }
                            });
                    
                    if (maxRetrysSame > 0) 
                        o = o.retry(retryPolicy(maxRetrysSame, true));
                    return o;
                }
            });
            
    if (maxRetrysNext > 0 && server == null) 
        o = o.retry(retryPolicy(maxRetrysNext, false));
        
    return o.onErrorResumeNext(new Func1<Throwable, Observable<T>>() {
            ...
        });
}

#1 这里生成一个Observable,这个Observable每次重试都使用selectServer方法重新选择下游的一个服务实例,再发起请求。
#2 这里也生成一个Observable,这个Observable每次重试都在同一个Server内发起请求。
#3 operation是AbstractLoadBalancerAwareClient#executeWithLoadBalancer方法#1步骤中submit方法传递的匿名类,这里获取到server后便可通过该匿名类发起Http请求

Observable.just(...).concatMap(...)也是RxJava提供的语法,just方法生成只有一个数据的Observable,concatMap方法对该Observable数据进行转化,返回另一个Observable,有兴趣的同学也可以了解一下RxJava的知识。

最后看一下如何通过url转发
SimpleHostRoutingFilter#run -> forward

private CloseableHttpResponse forward(CloseableHttpClient httpclient, String verb,
        String uri, HttpServletRequest request, MultiValueMap<String, String> headers,
        MultiValueMap<String, String> params, InputStream requestEntity)
        throws Exception {
    // #1
    ...
    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());
        // #2       
        CloseableHttpResponse zuulResponse = forwardRequest(httpclient, httpHost,
                httpRequest);
        this.helper.appendDebug(info, zuulResponse.getStatusLine().getStatusCode(),
                revertHeaders(zuulResponse.getAllHeaders()));
        return zuulResponse;
    }
    finally {
    }
}

#1 对url,contentType做一些处理,构造一个新的HttpRequest
注意,这里需要读取原request的InputStream,如果在该步骤前已经读取了InputStream,这里就读取不到了,会导致转发的http body为空。
#2 通过CloseableHttpClient(httpclient)转发请求
CloseableHttpClient通过newClient方法构造,会设置timeout等配置。

可以看到,serviceId,url的转发机制不同,所以对应超时等配置也不同。

Zuul的解析就说到这里。Spring Cloud Gateway是Spring提供的新一代网关,基于webflux实现异步请求,后面分享Spring Reactive时再写文章解析Spring Cloud Gateway。

如果您觉得本文不错,欢迎关注我的微信公众号,您的关注是我坚持的动力!


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335