当前位置: 移动技术网 > IT编程>开发语言>Java > 为何一个@LoadBalanced注解就能让RestTemplate拥有负载均衡的能力?【享学Spring Cloud】

为何一个@LoadBalanced注解就能让RestTemplate拥有负载均衡的能力?【享学Spring Cloud】

2019年09月17日  | 移动技术网IT编程  | 我要评论

每篇一句

你应该思考:为什么往往完成比完美更重要?

前言

spring cloud微服务应用体系中,远程调用都应负载均衡。我们在使用resttemplate作为远程调用客户端的时候,开启负载均衡极其简单:一个@loadbalanced注解就搞定了
相信大家大都使用过ribbonclient端的负载均衡,也许你有和我一样的感受:ribbon虽强大但不是特别的好用。我研究了一番,其实根源还是我们对它内部的原理不够了解,导致对一些现象无法给出合理解释,同时也影响了我们对它的定制和扩展。本文就针对此做出梳理,希望大家通过本文也能够对ribbon有一个较为清晰的理解(本文只解释它@loadbalanced这一小块内容)。

开启客户端负载均衡只需要一个注解即可,形如这样:

@loadbalanced // 标注此注解后,resttemplate就具有了客户端负载均衡能力
@bean
public resttemplate resttemplate(){
    return new resttemplate();
}

spring是java界最优秀、最杰出的重复发明轮子作品一点都不为过。本文就代领你一探究竟,为何开启resttemplate的负载均衡如此简单。

说明:本文建立在你已经熟练使用resttemplate,并且了解resttemplate它相关组件的原理的基础上分析。若对这部分还比较模糊,强行推荐你参看我前面这篇文章:resttemplate的使用和原理你都烂熟于胸了吗?【享学spring mvc】

ribbonautoconfiguration

这是spring boot/cloud启动ribbon的入口自动配置类,需要先有个大概的了解:

@configuration
// 类路径存在com.netflix.client.iclient、resttemplate等时生效
@conditional(ribbonautoconfiguration.ribbonclassesconditions.class) 
// // 允许在单个类中使用多个@ribbonclient
@ribbonclients 
// 若有eureka,那就在eureka配置好后再配置它~~~(如果是别的注册中心呢,ribbon还能玩吗?)
@autoconfigureafter(name = "org.springframework.cloud.netflix.eureka.eurekaclientautoconfiguration")
@autoconfigurebefore({ loadbalancerautoconfiguration.class, asyncloadbalancerautoconfiguration.class })
// 加载配置:ribbon.eager-load --> true的话,那么项目启动的时候就会把client初始化好,避免第一次惩罚
@enableconfigurationproperties({ ribboneagerloadproperties.class, serverintrospectorproperties.class })
public class ribbonautoconfiguration {

    @autowired
    private ribboneagerloadproperties ribboneagerloadproperties;
    // ribbon的配置文件们~~~~~~~(复杂且重要)
    @autowired(required = false)
    private list<ribbonclientspecification> configurations = new arraylist<>();

    // 特征,featuresendpoint这个端点(`/actuator/features`)会使用它org.springframework.cloud.client.actuator.hasfeatures
    @bean
    public hasfeatures ribbonfeature() {
        return hasfeatures.namedfeature("ribbon", ribbon.class);
    }


    // 它是最为重要的,是一个org.springframework.cloud.context.named.namedcontextfactory  此工厂用于创建命名的spring容器
    // 这里传入配置文件,每个不同命名空间就会创建一个新的容器(和feign特别像) 设置当前容器为父容器
    @bean
    public springclientfactory springclientfactory() {
        springclientfactory factory = new springclientfactory();
        factory.setconfigurations(this.configurations);
        return factory;
    }

    // 这个bean是关键,若你没定义,就用系统默认提供的client了~~~
    // 内部使用和持有了springclientfactory。。。
    @bean
    @conditionalonmissingbean(loadbalancerclient.class)
    public loadbalancerclient loadbalancerclient() {
        return new ribbonloadbalancerclient(springclientfactory());
    }
    ...
}

这个配置类最重要的是完成了ribbon相关组件的自动配置,有了loadbalancerclient才能做负载均衡(这里使用的是它的唯一实现类ribbonloadbalancerclient


@loadbalanced

注解本身及其简单(一个属性都木有):

// 所在包是org.springframework.cloud.client.loadbalancer
// 能标注在字段、方法参数、方法上
// javadoc上说得很清楚:它只能标注在resttemplate上才有效
@target({ elementtype.field, elementtype.parameter, elementtype.method })
@retention(retentionpolicy.runtime)
@documented
@inherited
@qualifier
public @interface loadbalanced {
}

它最大的特点:头上标注有@qualifier注解,这是它生效的最重要因素之一,本文后半啦我花了大篇幅介绍它的生效时机。
关于@loadbalanced自动生效的配置,我们需要来到这个自动配置类:loadbalancerautoconfiguration

loadbalancerautoconfiguration

// auto-configuration for ribbon (client-side load balancing).
// 它的负载均衡技术依赖于的是ribbon组件~
// 它所在的包是:org.springframework.cloud.client.loadbalancer
@configuration
@conditionalonclass(resttemplate.class) //可见它只对resttemplate生效
@conditionalonbean(loadbalancerclient.class) // spring容器内必须存在这个接口的bean才会生效(参见:ribbonautoconfiguration)
@enableconfigurationproperties(loadbalancerretryproperties.class) // retry的配置文件
public class loadbalancerautoconfiguration {
    
    // 拿到容器内所有的标注有@loadbalanced注解的bean们
    // 注意:必须标注有@loadbalanced注解的才行
    @loadbalanced
    @autowired(required = false)
    private list<resttemplate> resttemplates = collections.emptylist(); 
    // loadbalancerrequesttransformer接口:允许使用者把request + serviceinstance --> 改造一下
    // spring内部默认是没有提供任何实现类的(匿名的都木有)
    @autowired(required = false)
    private list<loadbalancerrequesttransformer> transformers = collections.emptylist();

    // 配置一个匿名的smartinitializingsingleton 此接口我们应该是熟悉的
    // 它的aftersingletonsinstantiated()方法会在所有的单例bean初始化完成之后,再调用一个一个的处理beanname~
    // 本处:使用配置好的所有的resttemplatecustomizer定制器们,对所有的`resttemplate`定制处理
    // resttemplatecustomizer下面有个lambda的实现。若调用者有需要可以书写然后扔进容器里既生效
    // 这种定制器:若你项目中有多个resttempalte,需要统一处理的话。写一个定制器是个不错的选择
    // (比如统一要放置一个请求拦截器:输出日志之类的)
    @bean
    public smartinitializingsingleton loadbalancedresttemplateinitializerdeprecated(final objectprovider<list<resttemplatecustomizer>> resttemplatecustomizers) {
        return () -> resttemplatecustomizers.ifavailable(customizers -> {
            for (resttemplate resttemplate : loadbalancerautoconfiguration.this.resttemplates) {
                for (resttemplatecustomizer customizer : customizers) {
                    customizer.customize(resttemplate);
                }
            }
        });
    }
    
    // 这个工厂用于createrequest()创建出一个loadbalancerrequest
    // 这个请求里面是包含loadbalancerclient以及httprequest request的
    @bean
    @conditionalonmissingbean
    public loadbalancerrequestfactory loadbalancerrequestfactory(loadbalancerclient loadbalancerclient) {
        return new loadbalancerrequestfactory(loadbalancerclient, this.transformers);
    }
    
    // =========到目前为止还和负载均衡没啥关系==========
    // =========接下来的配置才和负载均衡有关(当然上面是基础项)==========

    // 若有retry的包,就是另外一份配置,和这差不多~~
    @configuration
    @conditionalonmissingclass("org.springframework.retry.support.retrytemplate")
    static class loadbalancerinterceptorconfig {、
    
        // 这个bean的名称叫`loadbalancerclient`,我个人觉得叫`loadbalancerinterceptor`更合适吧(虽然ribbon是唯一实现)
        // 这里直接使用的是requestfactory和client构建一个拦截器对象
        // loadbalancerinterceptor可是`clienthttprequestinterceptor`,它会介入到http.client里面去
        // loadbalancerinterceptor也是实现负载均衡的入口,下面详解
        // tips:这里可没有@conditionalonmissingbean哦~~~~
        @bean
        public loadbalancerinterceptor ribboninterceptor(loadbalancerclient loadbalancerclient, loadbalancerrequestfactory requestfactory) {
            return new loadbalancerinterceptor(loadbalancerclient, requestfactory);
        }
    
        
        // 向容器内放入一个resttemplatecustomizer 定制器
        // 这个定制器的作用上面已经说了:在resttemplate初始化完成后,应用此定制化器在**所有的实例上**
        // 这个匿名实现的逻辑超级简单:向所有的resttemplate都塞入一个loadbalancerinterceptor 让其具备有负载均衡的能力
        
        // tips:此处有注解@conditionalonmissingbean。也就是说如果调用者自己定义过resttemplatecustomizer类型的bean,此处是不会执行的
        // 请务必注意这点:容易让你的负载均衡不生效哦~~~~
        @bean
        @conditionalonmissingbean
        public resttemplatecustomizer resttemplatecustomizer(final loadbalancerinterceptor loadbalancerinterceptor) {
            return resttemplate -> {
                list<clienthttprequestinterceptor> list = new arraylist<>(resttemplate.getinterceptors());
                list.add(loadbalancerinterceptor);
                resttemplate.setinterceptors(list);
            };
        }
    }
    ...
}

这段配置代码稍微有点长,我把流程总结为如下几步:

  1. loadbalancerautoconfiguration要想生效类路径必须有resttemplate,以及spring容器内必须有loadbalancerclient的实现bean
    1. loadbalancerclient的唯一实现类是:org.springframework.cloud.netflix.ribbon.ribbonloadbalancerclient
  2. loadbalancerinterceptor是个clienthttprequestinterceptor客户端请求拦截器。它的作用是在客户端发起请求之前拦截,进而实现客户端的负载均衡
  3. resttemplatecustomizer()返回的匿名定制器resttemplatecustomizer它用来给所有的resttemplate加上负载均衡拦截器(需要注意它的@conditionalonmissingbean注解~)

不难发现,负载均衡实现的核心就是一个拦截器,就是这个拦截器让一个普通的resttemplate逆袭成为了一个具有负载均衡功能的请求器

loadbalancerinterceptor

该类唯一被使用的地方就是loadbalancerautoconfiguration里配置上去~

public class loadbalancerinterceptor implements clienthttprequestinterceptor {

    // 这个命名都不叫client了,而叫loadbalancer~~~
    private loadbalancerclient loadbalancer;
    // 用于构建出一个request
    private loadbalancerrequestfactory requestfactory;
    ... // 省略构造函数(给这两个属性赋值)

    @override
    public clienthttpresponse intercept(final httprequest request, final byte[] body, final clienthttprequestexecution execution) throws ioexception {
        final uri originaluri = request.geturi();
        string servicename = originaluri.gethost();
        assert.state(servicename != null, "request uri does not contain a valid hostname: " + originaluri);
        return this.loadbalancer.execute(servicename, this.requestfactory.createrequest(request, body, execution));
    }
}

此拦截器拦截请求后把它的servicename委托给了loadbalancerclient去执行,根据servicename可能对应n多个实际的server,因此就可以从众多的server中运用均衡算法,挑选出一个最为合适的server做最终的请求(它持有真正的请求执行器clienthttprequestexecution)。


loadbalancerclient

请求被拦截后,最终都是委托给了loadbalancerclient处理。

// 由使用负载平衡器选择要向其发送请求的服务器的类实现
public interface serviceinstancechooser {

    // 从负载平衡器中为指定的服务选择service服务实例。
    // 也就是根据调用者传入的serviceid,负载均衡的选择出一个具体的实例出来
    serviceinstance choose(string serviceid);
}

// 它自己定义了三个方法
public interface loadbalancerclient extends serviceinstancechooser {
    
    // 执行请求
    <t> t execute(string serviceid, loadbalancerrequest<t> request) throws ioexception;
    <t> t execute(string serviceid, serviceinstance serviceinstance, loadbalancerrequest<t> request) throws ioexception;
    
    // 重新构造url:把url中原来写的服务名 换掉 换成实际的
    uri reconstructuri(serviceinstance instance, uri original);
}

它只有一个实现类ribbonloadbalancerclientserviceinstancechooser是有多个实现类的~)。

ribbonloadbalancerclient

首先我们应当关注它的choose()方法:

public class ribbonloadbalancerclient implements loadbalancerclient {
    
    @override
    public serviceinstance choose(string serviceid) {
        return choose(serviceid, null);
    }
    // hint:你可以理解成分组。若指定了,只会在这个偏好的分组里面去均衡选择
    // 得到一个server后,使用ribbonserver把server适配起来~~~
    // 这样一个实例就选好了~~~真正请求会落在这个实例上~
    public serviceinstance choose(string serviceid, object hint) {
        server server = getserver(getloadbalancer(serviceid), hint);
        if (server == null) {
            return null;
        }
        return new ribbonserver(serviceid, server, issecure(server, serviceid),
                serverintrospector(serviceid).getmetadata(server));
    }

    // 根据serviceid去找到一个属于它的负载均衡器
    protected iloadbalancer getloadbalancer(string serviceid) {
        return this.clientfactory.getloadbalancer(serviceid);
    }

}

choose方法:传入serviceid,然后通过springclientfactory获取负载均衡器com.netflix.loadbalancer.iloadbalancer,最终委托给它的chooseserver()方法选取到一个com.netflix.loadbalancer.server实例,也就是说真正完成server选取的是iloadbalancer

iloadbalancer以及它相关的类是一个较为庞大的体系,本文不做更多的展开,而是只聚焦在我们的流程上

loadbalancerinterceptor执行的时候是直接委托执行的loadbalancer.execute()这个方法:

ribbonloadbalancerclient:

    // hint此处传值为null:一视同仁
    // 说明:loadbalancerrequest是通过loadbalancerrequestfactory.createrequest(request, body, execution)创建出来的
    // 它实现loadbalancerrequest接口是用的一个匿名内部类,泛型类型是clienthttpresponse
    // 因为最终执行的显然还是执行器:clienthttprequestexecution.execute()
    @override
    public <t> t execute(string serviceid, loadbalancerrequest<t> request) throws ioexception {
        return execute(serviceid, request, null);
    }
    // public方法(非接口方法)
    public <t> t execute(string serviceid, loadbalancerrequest<t> request, object hint) throws ioexception {
        // 同上:拿到负载均衡器,然后拿到一个serverinstance实例
        iloadbalancer loadbalancer = getloadbalancer(serviceid);
        server server = getserver(loadbalancer, hint);
        if (server == null) { // 若没找到就直接抛出异常。这里使用的是illegalstateexception这个异常
            throw new illegalstateexception("no instances available for " + serviceid);
        }

        // 把server适配为ribbonserver  issecure:客户端是否安全
        // serverintrospector内省  参考配置文件:serverintrospectorproperties
        ribbonserver ribbonserver = new ribbonserver(serviceid, server,
                issecure(server, serviceid), serverintrospector(serviceid).getmetadata(server));

        //调用本类的重载接口方法~~~~~
        return execute(serviceid, ribbonserver, request);
    }

    // 接口方法:它的参数是serviceinstance --> 已经确定了唯一的server实例~~~
    @override
    public <t> t execute(string serviceid, serviceinstance serviceinstance, loadbalancerrequest<t> request) throws ioexception {
    
        // 拿到server)(说白了,ribbonserver是execute时的唯一实现)
        server server = null;
        if (serviceinstance instanceof ribbonserver) {
            server = ((ribbonserver) serviceinstance).getserver();
        }
        if (server == null) {
            throw new illegalstateexception("no instances available for " + serviceid);
        }

        // 说明:执行的上下文是和serviceid绑定的
        ribbonloadbalancercontext context = this.clientfactory.getloadbalancercontext(serviceid);
        ... 
        // 真正的向server发送请求,得到返回值
        // 因为有拦截器,所以这里肯定说执行的是interceptingrequestexecution.execute()方法
        // so会调用servicerequestwrapper.geturi(),从而就会调用reconstructuri()方法
            t returnval = request.apply(serviceinstance);
            return returnval;
        ... // 异常处理
    }

returnval是一个clienthttpresponse,最后交给handleresponse()方法来处理异常情况(若存在的话),若无异常就交给提取器提值:responseextractor.extractdata(response),这样整个请求就算全部完成了。

使用细节

针对@loadbalanced下的resttemplate的使用,我总结如下细节供以参考:

  1. 传入的string类型的url必须是绝对路径(http://...),否则抛出异常:java.lang.illegalargumentexception: uri is not absolute
  2. serviceid不区分大小写(http://user/...效果同http://user/...
  3. serviceid后请不要跟port端口号了~~~

最后,需要特别指出的是:标注有@loadbalancedresttemplate只能书写serviceid而不能再写ip地址/域名去发送请求了。若你的项目中两种case都有需要,请定义多个resttemplate分别应对不同的使用场景~

本地测试

了解了它的执行流程后,若需要本地测试(不依赖于注册中心),可以这么来做:

// 因为自动配置头上有@conditionalonmissingbean注解,所以自定义一个覆盖它的行为即可
// 此处复写它的getserver()方法,返回一个固定的(访问百度首页)即可,方便测试
@bean
public loadbalancerclient loadbalancerclient(springclientfactory factory) {
    return new ribbonloadbalancerclient(factory) {
        @override
        protected server getserver(iloadbalancer loadbalancer, object hint) {
            return new server("www.baidu.com", 80);
        }
    };
}

这么一来,下面这个访问结果就是百度首页的html内容喽。

@test
public void contextloads() {
    string obj = resttemplate.getforobject("http://my-serviceid", string.class);
    system.out.println(obj);
}

此处my-serviceid肯定是不存在的,但得益于我上面自定义配置的loadbalancerclient

什么,写死return一个server实例不优雅?确实,总不能每次上线前还把这部分代码给注释掉吧,若有多个实例呢?还得自己写负载均衡算法吗?很显然spring cloud早早就为我们考虑到了这一点:脱离eureka使用配置listofservers进行客户端负载均衡调度(<clientname>.<namespace>.listofservers=<comma delimited hostname:port strings>

对于上例我只需要在主配置文件里这么配置一下:

# ribbon.eureka.enabled=false # 若没用euraka,此配置可省略。否则不可以
my-serviceid.ribbon.listofservers=www.baidu.com # 若有多个实例请用逗号分隔

效果完全同上。

tips:这种配置法不需要是完整的绝对路径,http://是可以省略的(new server()方式亦可)

自己添加一个记录请求日志的拦截器可行吗?

显然是可行的,我给出示例如下:

@loadbalanced
@bean
public resttemplate resttemplate() {
    resttemplate resttemplate = new resttemplate();
    list<clienthttprequestinterceptor> list = new arraylist<>();
    list.add((request, body, execution) -> {
        system.out.println("当前请求的url是:" + request.geturi().tostring());
        return execution.execute(request, body);
    });
    resttemplate.setinterceptors(list);
    return resttemplate;
}

这样每次客户端的请求都会打印这句话:当前请求的uri是:http://my-serviceid,一般情况(缺省情况)自定义的拦截器都会在负载均衡拦截器前面执行(因为它要执行最终的请求)。若你有必要定义多个拦截器且要控制顺序,可通过ordered系列接口来实现~


最后的最后,我抛出一个非常非常重要的问题:

    @loadbalanced
    @autowired(required = false)
    private list<resttemplate> resttemplates = collections.emptylist();

@autowired + @loadbalanced能把你配置的resttemplate自动注入进来拿来定制呢???核心原理是什么?

> 提示:本原理内容属于spring framwork核心技术,建议深入思考而不囫囵吞枣。有疑问的可以给我留言,我也将会在下篇文章给出详细解答(建议先思考)

推荐阅读

resttemplate的使用和原理你都烂熟于胸了吗?【享学spring mvc】
@qualifier高级应用---按类别批量依赖注入【享学spring】

总结

本文以大家熟悉的@loadbalancedresttemplate为切入点介绍了ribbon实现负载均衡的执行流程,当然此部分对ribbon整个的核心负载体系知识来说知识冰山一角,但它作为敲门砖还是很有意义的,希望本文能勾起你对ribbon体系的兴趣,深入了解它~

== 若对spring、springboot、mybatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一起飞 ==
== 若对spring、springboot、mybatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一起飞 ==

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网