第一次在简书上进行写作,最近准备对spring cloud的netflix组件进行一些代码的分析和总结,首先从ribbon开始吧。
通过阅读sping cloud文档, 我们知道需要引入ribbon的话只需要引入一下依赖:
在观察这个依赖的pom文件之后,我们可以找到以下配置:
而最终我们在这个core中的spring.factories中找到了下列代码:
这个RibbonAutoConfiguration类就是启动ribbon配置的auto configuration类。
为了分析方便,我们在我们测试的spring boot项目中加入了如下配置:
根据文档我们知道,这个是生命了一个clientName为test的ribbon client,并声明了它的server list为www.baidu.com和www.163.com,并且还声明了它为eagar load,即spring上下文启动时就应该被加载。我们接下来将进入RibbonAutoConfiguration类中一探究竟。
第一个红框是声明的一个springClientFactory的定义,它引起我们的注意主要有两点,第一点是它将RibbonClientsSpecification这个list设置为了configuration属性,我们可以简单的分析一下@RibbonClient这个注解就能够知道,这个注解实际上是在上下文中引入了一个RibbonClientsSpecification的Bean Definition,通过硬编码的方式进行Ribbon Client的配置,因此我们感觉到这个SpringClientFactory肯定跟Ribbon有关系,第二点是我们看到中间那个红框返回的是LoadBalancerClient类,也是代理给springClientFactory实现的,而我们知道通过查看spring cloud文档,我们可以通过注入LoadBalancerClient来引入ribbon client,这更印证了之前的判断。
这里值得注意的是,new RibbonLoadBalancerClient的时候传入的是一个springClientFactory方法调用,这里引起了我的怀疑,这样还能保证与第一个红框中@Bean注解返回的会是同一个对象么?如果不同,怎么能保证client的统一呢?通过我单步进入到这个方法调用内部我明白了,spring对于@Configuration注解装饰的类都通过cglib的生成动态代理的方式生成了一份subclass,这样做的原因就是要代理@Configuration实例中的每一个@Bean方法,而我们调用的实际上是这个subclass中的方法,而在代理的方法中通过一些逻辑判断来首先保证返回当前spring context中已经生成好的实例,这样就保证了单例。具体大家可以下来看spring文档或者自行单步进入了解。
最后一个红框就是我们eager-load的实现,它通过在当前的这个spring-context中注入了一个RibbonApplicationContextInitializer类来完成初始化load。它的实现同样需要借助这个springClientFactory,我们进入这个类中可以看到:
它实际上是一个ApplicationListener,在上下文Ready的时候对声明要eager-load的client进行初始化操作,而且初始化的方法名也比较特别,叫getContext,这又是获取的什么上下文呢?
我们可以看到他直接调用了super的方法,我们可以借这个机会现看看这个类:
它继承的是一个叫NamedContextFactory的类,并在构造函数中传入了一个叫RibbonClientConfiguration的类,这个类我们后面会接触到。通过注释我们可以知道,这个Factory类的职责是创建client、load balancer和configuration对象,并且会针对每个client创建一个spring application context,并且把client以及其需要的类防止在这个子上下文来存放和获取。这又是为什么呢?
我们进入NamedContextFactory类中再看看:
这个类的注释明确说明它会创建一个子上下文的集合,并且保证注入的Specification能够在各自的子上下文中定义bean,我们可以看到它自己的成员变量中有一个ConcurrentHashMap存放创建好的AnnotationConfigApplicationContext,并且key为clientName,同时还有个defaultConfigType
通过构造函数我们知道这个defaultConfigType就是我们之前看到的RibbonClientConfiguration,而在我们之前看到的createContext方法中,代码如下:
它首先会创建一个AnnotationConfigApplicationContext类作为子上下文,并且如果在之前定义好的Specification中存在对应的client的config的话就会把这个configuration注入到这个子上下文中去,然后是注入default configuration,接着是注入Property以及我们的defaultConfigType(RibbonClientConfiguration),并且注入了一个propertySource,内容是clientName,最终调用子上下文的refresh方法初始化上下文。
对于我们通过properties文件定义的client来说,不存在他的specification,因此我们的重点有落在了RibbonClientConfiguration中,看来这个是重点。
我们进入到RibbonClientConfiguration中可以看到:
这个Configuration勒种首先会定义一个IClientConfig实例,这个类是读取properties的关键,我们知道netflix本身是通过一个archiaus的工具读取的properties,而spring cloud在自己的spring system environment和auchiaus之间做了一个bridge,来读取存在于spring environment中的配置,这里就不多做展开,感兴趣的同学可以自行查看代码和文档。在创建了IClientConfig配置之后,我们可以看到基于这个IClientConfig实例实现了其他的Ribbon有关的配置:
包括IRule,ILoadBalancer,IPing,ServerList等等一系列组件,这些组件的构成和各自在Ribbon中的作用我会在后面的文章中进行讲解。通过观察这些配置其实我们可以比较好的理清Ribbon各个组件各自的一些关系。通过观察其中的代码我们可以发现(以IRule Bean创建为例),这个创建方法首先会有一个@ConditionOnMissingBean注解,表示如果当前上下文中没有申明任何该类型组件,我们就将注册对应的@Bean方法进行创建,然后就在PropertyFactory里面查找(实际上是在当前Environment中查找)是否有定义相关的类型,如我们在application.yml中添加如下配置:
那么在创建IRule这个bean的时候就会优先使用我们生命的RoundRobinRule来进行负载均衡规则的创建,如果都没有,那么就会创建一个默认的实现,即代码中提示的ZoneAvoidanceRule类。整个创建过程就以这样的顺序进行。那这里我们就会有另外一个疑问,什么时候@ConditionOnMissingBean这个注解会生效呢?答案就在spring cloud的文档中。我们先前也提到,我们可以通过声明@RibbonClient注解的@Configuration类来实现硬编码的ribbon client配置,而这个注解会注入一个ribbon的RibbonClientSpecification类到parent spring context中,而我们之前代码已经分析过,这最终会导致这个@Configuration会加入到新创建的这个client对应的子上下文中来。
当这个@Configuration被注入到子上下文中之后,在refresh的时候会先将这个@Configuration中所生命的Bean Definition放入,再处理我们刚看到的RibbonClientConfiguration类中的定义,因此让这个@ConditionOnMissingBean生效。
可能我整个过程说的比较复杂,让我们通过代码和单步来测试一下:
首先我们新开了一个client package并且创建了一个新的@Configuration类,并且在这个类中声明了一个IRule的bean 定义,并且在我们的MyClass类中加上了@RibbonClient声明,将这个@Configuration引入。然后我们再单步创建的过程,可以看到,首先RibbonClientConfiguration中的IRule Bean创建方法是不会进入了,然后在创建ILoadBalancer的方法中,我们可以看到:
它所传入的IRule对象不再是以前咱们在yaml文件中声明的RoundRobinRule,而是我们在ClientConfiguration中声明的一个这个内部类了。因此我还发现了spring cloud文档中的一个有问题的地方:
它描述的是在properties中描述的class要比我在@RibbonClient中声明的class和默认实现由更高的优先级,而正确的情况是@RibbonClient的优先级大于properties,properties则大于默认实现。
这里观众可能需要有个小疑问了,为什么我们需要将我们的ribbon client的@Configuration所在的包进行这样的组织?为什么不能直接放在Application类所在的同级package包下?
首先我们可以阅读spring文档,有这么一句话:
他所描述的也是我刚才说的这种包组织结构,不能被main spring context所扫描到。大家想一下,如果我们让这个@Configuration被main spring context扫描到,根据spring上下文的继承关系,在每个client对应的子spring context创建的时候,每个client对应的@Bean方法上的@ConditionalOnMissingBean的判断都会触发,从而覆盖掉本不应该影响的其他client的配置。为了验证我刚刚的阐述,我们通过对代码进行适当的修改来单步:
我们将这个@Configuration纳入到main context的扫描中,然后我们声明一个新的test2 client,并且将它定义为eager-load,接着修改我们之前ClientConfiguration的IRule定义,返回WeightedResponseTimeRule。
这些准备工作做好之后,当我们在单步到test2的创建时发现了下面的事情:
它在创建ILoadBalancer的时候使用的时我们在ClientConfiguration中定义的WeightedResponseTimeRule,而不是我们在yaml文件中定义的RoundRobinRule。这个配置本来只应该对于test生效,因此我之前所做出的结论得到了印证。
对于为什么每个client都要创建一个独立的context,我的理解和猜测是想通过context的隔离和@ConditionOnMissingBean注解来做到优雅的动态配置和默认配置,并保证各个client之间不会造成干扰。当然这也是有一些规则必须遵守的(例如之前的@Configuration扫描路径规则)。
spring cloud netflix ribbon 加载过程第一步大致分析结束,我们可以参照spring cloud的实例代码进行测试:
这个LoadBalancerClient我们在最初的几次分析中就已经看到,它实际上也是代理给了SpringClientFactory到对应的client的上下文中进行bean的获取和操作。我们可以通过请求到这个方法来获取到返回的Host,记住我们这里使用的时WeightedResponseTimeRule,他是通过响应时间来做的一定量的比例抽样,连续访问结果如下:
可以看到在我家这个网下面,response time没有明显差别的情况下,他的表现基本跟RoundRobinRule一致。
这篇文章基本上对spring cloud netflix ribbon的家在过程进行了一些代码上的分析,大致理清了他的内部逻辑,后面我会对ribbon本身的实现和结构进行一些分析,大家有兴趣麻烦关注一波,谢谢!