SpringCloud源码解析 -- RestTemplate与@LoadBalanced

本文主要分享SpringCloud中RestTemplate与@LoadBalanced的实现原理。
源码分析基于Spring Cloud Hoxton

RestTemplate处理请求
先看一下RestTemplate是怎么处理Http请求的。熟悉RestTemplate的同学可以跳过这一部分。

RestTemplate#doExecute

protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
        @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

    Assert.notNull(url, "URI is required");
    Assert.notNull(method, "HttpMethod is required");
    ClientHttpResponse response = null;
    try {
        // #1
        ClientHttpRequest request = createRequest(url, method); 
        if (requestCallback != null) {
            // #2
            requestCallback.doWithRequest(request); 
        }
        // #3
        response = request.execute();   
        // #4
        handleResponse(url, method, response);
        return (responseExtractor != null ? responseExtractor.extractData(response) : null);
    }
    // #5
    ...
}

#1 创建一个ClientHttpRequest,ClientHttpRequest代表一个Http请求
#2 使用RequestCallback处理request,参数拼接,转化等
#3 发起Http请求
#4 处理Http请求结构,转化对象
#5 异常处理,关闭连接

#1步骤 -> HttpAccessor#createRequest -> ClientHttpRequestFactory#createRequest
这里由不同的ClientHttpRequestFactory创建Request,我们配置RestTemplate通常会指定ClientHttpRequestFactory,例如

@Bean
public RestTemplate restTemplate() {
    return new RestTemplate(new SimpleClientHttpRequestFactory());
}

默认的ClientHttpRequestFactory也是SimpleClientHttpRequestFactory。

SimpleClientHttpRequestFactory#createRequest

public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
    HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
    prepareConnection(connection, httpMethod.name());

    if (this.bufferRequestBody) {
        return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
    }
    else {
        return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
    }
}

可以看到,SimpleClientHttpRequestFactory#createRequest每次都创建一个新的连接,没有使用连接池,因而性能很差,使用RestTemplate一定要注意不要使用它。

RestTemplate#doExecute方法#3步骤 -> AbstractClientHttpRequest#execute -> AbstractClientHttpRequest#executeInternal,该方法由bstractClientHttpRequest子类实现。

RestTemplate拦截机制

InterceptingClientHttpRequest实现了ClientHttpRequest,并且支持对Http请求进行拦截操作。

AbstractBufferingClientHttpRequest#executeInternal -> InterceptingClientHttpRequest#executeInternal -> InterceptingRequestExecution#execute

public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
    // #1
    if (this.iterator.hasNext()) {
        ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
        // #2
        return nextInterceptor.intercept(request, body, this);
    }
    else {
        // #3
        HttpMethod method = request.getMethod();
        Assert.state(method != null, "No standard HTTP method");
        ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
        request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
        if (body.length > 0) {
            if (delegate instanceof StreamingHttpOutputMessage) {
                StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate;
                streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream));
            }
            else {
                StreamUtils.copy(body, delegate.getBody());
            }
        }
        return delegate.execute();
    }
}

#1 InterceptingClientHttpRequest#interceptors是一个ClientHttpRequestInterceptor集合,ClientHttpRequestInterceptor是一个拦截器接口,负责定义对Http请求的拦截操作,InterceptingRequestExecution是Http请求执行器,InterceptingRequestExecution#iterator就是拦截器集合迭代器。
#2 执行拦截操作,注意方法最后的this参数,ClientHttpRequestInterceptor#intercept方法中必须继续调用AbstractBufferingClientHttpRequest#executeInternal,才能将调用链继续。
#3 InterceptingClientHttpRequest#interceptingRequestFactory就是ClientHttpRequestFactory,这一步才构造真正的请求HttpRequest,并发起http请求。

InterceptingClientHttpRequest是在哪里构造的呢?
回到RestTemplate父类InterceptingHttpAccessor#getRequestFactory

public ClientHttpRequestFactory getRequestFactory() {
    List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
    // #1
    if (!CollectionUtils.isEmpty(interceptors)) {
        ClientHttpRequestFactory factory = this.interceptingRequestFactory;
        if (factory == null) {
            factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
            this.interceptingRequestFactory = factory;
        }
        return factory;
    }
    else {
        // #2
        return super.getRequestFactory();
    }
}

#1 如果RestTemplate中存在拦截器,这里会创建一个InterceptingClientHttpRequestFactory,该factory生成InterceptingClientHttpRequest。
注意InterceptingClientHttpRequestFactory构造方法,将原始的RequestFactory和拦截器列表interceptors作为参数,InterceptingRequestExecution#execute方法会用到这些数据。
#2 如果RestTemplate中没有interceptor,直接使用原始的RequestFactory。

@LoadBalanced的实现原理
@LoadBalanced也是通过ClientHttpRequestInterceptor实现的。
LoadBalancerAutoConfiguration$LoadBalancerInterceptorConfig类中,构造了LoadBalancerInterceptor和RestTemplateCustomizer,其中LoadBalancerInterceptor就是实现LoadBalanced功能的拦截器,而RestTemplateCustomizer负责将LoadBalancerInterceptor添加到restTemplate中。

LoadBalancerAutoConfiguration#loadBalancedRestTemplateInitializerDeprecated生成了一个SmartInitializingSingleton,用于执行RestTemplateCustomizer。
比较有趣的是LoadBalancerAutoConfiguration#restTemplates定义

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();

这里可以将SpringContext中使用了@LoadBalanced标注的RestTemplate注入进来,为什么呢?
因为@LoadBalanced注解上标注了@Qualifier注解。

Spring在处理@Autowired注解时,发现@LoadBalanced上有@Qualifier注解,就会检查引入的RestTemplate是否也有@LoadBalanced注解,具体实现在QualifierAnnotationAutowireCandidateResolver#checkQualifier中。这部分内容可以参考 @Value,@Autowired实现原理

LoadBalancerInterceptor#intercept

public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
    URI originalUri = request.getURI();
    String serviceName = originalUri.getHost();
    Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
    // #1
    return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
}

#1 通过LoadBalancerRequestFactory#createRequest方法生成构造一个LoadBalancerRequest,LoadBalancerRequest代表一个LoadBalancer请求。
而loadBalancer是一个LoadBalancerClient,他是一个loadBalancer客户端,用于执行LoadBalancerRequest。

RibbonLoadBalancerClient#execute


public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
        throws IOException {
    // #1
    ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
    // #2
    Server server = getServer(loadBalancer, hint);
    if (server == null) {
        throw new IllegalStateException("No instances available for " + serviceId);
    }
    // #3
    RibbonServer ribbonServer = new RibbonServer(serviceId, server,
            isSecure(server, serviceId),
            serverIntrospector(serviceId).getMetadata(server));
    // #4
    return execute(serviceId, ribbonServer, request);
}

#1 通过SpringClientFactory构造一个ILoadBalancer负载均衡器,SpringClientFactory类是一个用来创建客户端负载均衡器的工厂类,该工厂会为每一个serviceId生成不同的Spring上下文。
#2 通过ILoadBalancer负载均衡器选择一个Server
#3 构建一个RibbonServer,它实现了ServiceInstance接口,ServiceInstance代表一个服务实例,提供getHost/getPort等方法。
#4 执行LoadBalancerRequest

#4步骤 -> LoadBalancerRequest#apply -> LoadBalancerRequestFactory#createRequest(该方法返回了匿名的LoadBalancerRequest)

public LoadBalancerRequest<ClientHttpResponse> createRequest(
        final HttpRequest request, final byte[] body,
        final ClientHttpRequestExecution execution) {
    return instance -> {
        // #1
        HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance,
                this.loadBalancer);
        if (this.transformers != null) {
            // #2
            for (LoadBalancerRequestTransformer transformer : this.transformers) {
                serviceRequest = transformer.transformRequest(serviceRequest,
                        instance);
            }
        }
        // #3
        return execution.execute(serviceRequest, body);
    };
}

#1 ServiceRequestWrapper重写了HttpRequest的getURI方法,将serviceId转化为真正的服务实例url,有兴趣的同学可以阅读ServiceRequestWrapper源码
#2 对HttpRequest做转化处理
#3 发起Http请求

回到RibbonLoadBalancerClient#execute方法#2步骤,
RibbonLoadBalancerClient#getServer -> BaseLoadBalancer#chooseServer -> IRule#choose。
IRule代表不同的负载均衡策略,有BestAvailableRule,AvailabilityFilteringRule,RetryRule,RoundRobinRule,RandomRule等策略。

负载均衡器ILoadBalancer
ILoadBalancer是一个负载均衡器,它维护一个存储服务实例的Server列表以实现负载均衡操作,同时提供chooseServer选择一个服务实例。

BaseLoadBalancer提供了基础的负载均衡功能,维护了两个列表allServerLock,upServerLock,分别存储所有服务实例以及正常服务实例。
它还维护了一个IPing接口,该接口的isAlive方法可以检测服务实例是否可用。
而IPingStrategy则是执行ping的策略。

DynamicServerListLoadBalancer能从注册中心中获取服务实例数据,并通过ServerListFilter过滤部分不符合规则的服务实例。

DynamicServerListLoadBalancer构造方法中通过restOfInit完成部分初始化工作

void restOfInit(IClientConfig clientConfig) {
    boolean primeConnection = this.isEnablePrimingConnections();
    this.setEnablePrimingConnections(false);
    // #1
    enableAndInitLearnNewServersFeature();
    // #2
    updateListOfServers();
    if (primeConnection && this.getPrimeConnections() != null) {
        this.getPrimeConnections()
                .primeConnections(getReachableServers());
    }
    this.setEnablePrimingConnections(primeConnection);
    LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
}

#1 通过PollingServerListUpdater定时从注册中心中拉取最新的可用的服务实例数据,
PollingServerListUpdater实现了ServerListUpdater接口,ServerListUpdater接口定义定时从注册中心更新数据的执行策略。
#2 通过ServerList获取服务实例数据并缓存,ServerList是ribbon提供的接口,其中getInitialListOfServers方法可以获取初始化的服务实例列表,而getUpdatedListOfServers方法可以获取最新的服务实例列表。由具体的注册中心实现该接口,如DiscoveryEnabledNIWSServerList,ConsulServerList等。

RibbonClientConfiguration中可以看到ILoadBalancer的默认实现为ZoneAwareLoadBalancer,IRule的默认实现为ZoneAvoidanceRule

ZoneAwareLoadBalancer可以避免因为跨Zone而导致的区域性故障,从而实现了服务的高可用,并且可以按照某种策略例如Zone的服务实例数量,故障率等等来筛选掉不符合条件的Zone区域。
ZoneAvoidanceRule能够在多Zone环境下根据某些策略(如可用性),选出最佳区域的Zone,再在Zone中通过轮询选择一个Server。
zone和region是eureka引用AWS的概念。

AsyncRestTemplate在Spring5中已经Deprecated,推荐使用WebClient。等到讲解Spring Reactive时再分享它的实现原理。

Ribbon的内容还是很多的,这里只是梳理了LoadBalanced实现的整体思路,有兴趣的同学可以自行深入学习具体的实现细节。

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


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