背景

我们上一篇介绍了feign调用的整体流程,在@FeignClient没有写死url的情况下,就会生成一个支持客户端负载均衡的LoadBalancerClient。这个LoadBalancerClient可以根据服务名,去获取服务对应的实例列表,然后再用一些客户端负载均衡算法,从这堆实例列表中选择一个实例,再进行http调用即可。

上图中,最核心的也就是2处。我们本次就从这里入手,去研究下,服务实例列表是如何获取到的,以及如何配置静态的服务实例地址。

服务实例列表相关bean初始化

在上图的2处开始执行前,有这么一行:

这里就会去查找bean,类型是LoadBalancerLifecycle.class。去哪里查找呢,spring容器,但是是各个loadbalancer自己的spring容器。

刚开始嘛,容器还没有,此时就会触发spring容器的创建和初始化。这个容器里有哪些bean呢?

主要的bean来源于LoadBalancerClientConfiguration这个配置类。里面包含了两个重要的bean,一个是loadbalancer,支持随机获取某个实例,但这个bean,可以从下面的代码看到,它的第一个构造参数,是去获取一个ServiceInstanceListSupplier类型的bean的provider,要靠这个provider提供服务实例列表。

所以,这个bean其实是依赖于ServiceInstanceListSupplier这种bean的。

下面这个则是ServiceInstanceListSupplier类型,也就是实例列表提供者。

ServiceInstanceListSupplierBuilder

  1. ServiceInstanceListSupplier.builder():
  2. static ServiceInstanceListSupplierBuilder builder() {
  3. return new ServiceInstanceListSupplierBuilder();
  4. }

这个就是普通的建造者,没有什么特别。接下来,则是给builder设置DiscoveryClient,这个就是服务发现相关的client,比如eureka、nacos这些的客户端:

  1. .withBlockingDiscoveryClient()

这里我们发现一个一个箭头函数,这个箭头函数有一个入参,名字是context,然后return了一个DiscoveryClientServiceInstanceListSupplier类型的对象。

函数最终赋值给了:

  1. private Creator baseCreator;

它的类型:

  1. Allows creating a {@link ServiceInstanceListSupplier} instance based on provided
  2. {@link ConfigurableApplicationContext}.
  3. public interface Creator extends Function<ConfigurableApplicationContext, ServiceInstanceListSupplier> {
  4. }
  5. @FunctionalInterface
  6. public interface Function<T, R> {
  7. R apply(T t);
  8. }

这个把参数带入,就是:

  1. ServiceInstanceListSupplier apply(ConfigurableApplicationContext t);

也就是接受一个spring上下文参数,返回一个ServiceInstanceListSupplier类型的对象。

所以再看下图,也就是从spring中获取DiscoveryClient类型的bean,然后new一个DiscoveryClientServiceInstanceListSupplier类型的对象返回。

接下来,builder又设置了缓存:

  1. ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withCaching()

  1. private DelegateCreator cachingCreator;
  2. public interface DelegateCreator extends
  3. BiFunction<ConfigurableApplicationContext, ServiceInstanceListSupplier, ServiceInstanceListSupplier> {
  4. }
  5. @FunctionalInterface
  6. public interface BiFunction<T, U, R> {
  7. R apply(T t, U u);
  8. }
  9. 翻译后就是:
  10. ServiceInstanceListSupplier apply(<ConfigurableApplicationContext t, ServiceInstanceListSupplier u);

这里其实不用说太细,无非是装饰器模式,又套了一层缓存。

接下来,进入最终的build环节:

可以看到,首先是执行了baseCreator,传入了spring上下文,此时就会触发之前看到的:

CompositeDiscoveryClient

我们上图中,获取到的DiscoveryClient类型的bean为CompositeDiscoveryClient。它就像它的名字一样,里面聚合了多个DiscoveryClient。

这个bean的定义在哪里呢?这是靠自动装配引入的:

这个聚合类依赖的discoveryClient哪里来的呢?

首先是nacosDiscoveryClient:

再一个是SimpleDiscoveryClient类型:

多个DiscoveryClient的顺序

在CompositeDiscoveryClient中,是用list维护各个DiscoveryClient。

  1. private final List<DiscoveryClient> discoveryClients;

谁先谁后,重要吗?看看下面的方法,是用来获取服务实例的:

这里是先获取到则直接返回,说明还是很重要的。

各个DiscoveryClient的order值怎么获取呢?

  1. public interface DiscoveryClient extends Ordered {
  2. /**
  3. * Default order of the discovery client.
  4. */
  5. int DEFAULT_ORDER = 0;
  6. ...
  7. default int getOrder() {
  8. return DEFAULT_ORDER;
  9. }
  10. }
  1. nacos中,实现了这个类,但是没有覆写getOrder,所以对于NacosDiscoveryClient,值就是0.
  2. public class NacosDiscoveryClient implements DiscoveryClient

对于SimpleDiscoveryClient来说,我们先不管它是啥,我们看其类定义:

其支持从配置文件中获取order:

  1. @Override
  2. public int getOrder() {
  3. return this.simpleDiscoveryProperties.getOrder();
  4. }

你没有显示设置这个order属性的话,默认也是0.

所以,不显式设置SimpleDiscoveryProperties的order的话,SimpleDiscoveryClient和NacosDiscoveryClient的order值相同,那谁先谁后就难讲了,这块待细挖才知道。

SimpleDiscoveryClient

这个discoveryClient是干嘛的呢,没啥存在感?

其实它是用来从配置文件中获取服务实例的。

  1. A DiscoveryClient that will use the properties file as a source of service instances.

它依赖的配置类如下:

  1. public class SimpleDiscoveryClient implements DiscoveryClient {
  2. private SimpleDiscoveryProperties simpleDiscoveryProperties;
  3. public SimpleDiscoveryClient(SimpleDiscoveryProperties simpleDiscoveryProperties) {
  4. this.simpleDiscoveryProperties = simpleDiscoveryProperties;
  5. }

它可以配置各个Feign服务的服务实例,以及我们前面提到的order(通过把这里的order改小,可以排到nacosDiscoveryClient前面,达成屏蔽nacos中的服务实例的效果)

我们可以像下面这样来配置:

  1. spring:
  2. application:
  3. discovery:
  4. client:
  5. simple:
  6. instances:
  7. echo-service-provider:
  8. - uri: http://1.1.1.1:8082
  9. metadata:
  10. my: instance1
  11. - uri: http://2.2.2.2:8082
  12. metadata:
  13. my: instance2

正常像上面这样就可以了,但是,nacos会排在它前面,导致无法生效:

所以,还得配上order:

  1. spring:
  2. application:
  3. discovery:
  4. client:
  5. simple:
  6. order: -1
  7. instances:
  8. echo-service-provider:
  9. - uri: http://1.1.1.1:8082
  10. metadata:
  11. my: instance1
  12. - uri: http://2.2.2.2:8082
  13. metadata:
  14. my: instance2

DiscoveryClientServiceInstanceListSupplier

构造好了前面的CompositeDiscoveryClient,我们就会开始创建服务实例supplier。

上图可以看到,这里有delegate.getInstances(serviceId),但后面又进行了封装,最终的类型是:

  1. private final Flux<List<ServiceInstance>> serviceInstances;

这个Flux是反应式编程相关的api,不是很懂,但内部主要就是封装了一个数据源,等到需要获取服务实例的时候,就会真正调用到:

  1. delegate.getInstances(serviceId)

届时,就会调用到:

缓存包装

这个DiscoveryClientServiceInstanceListSupplier,后续又经过cache相关包装,最终的类型是:

  1. CachingServiceInstanceListSupplier

这个bean咱们就讲到这里。

reactorServiceInstanceLoadBalancer

接下来,开始看:

第一个参数:

  1. loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class)
  2. public <T> ObjectProvider<T> getLazyProvider(String name, Class<T> type) {
  3. return new ClientFactoryObjectProvider<>(this, name, type);
  4. }
  5. ClientFactoryObjectProvider(NamedContextFactory<?> clientFactory, String name, Class<T> type) {
  6. this.clientFactory = clientFactory;
  7. this.name = name;
  8. this.type = type;
  9. }

接下来构造这个随机的loadbalancer:

  1. public RoundRobinLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId, int seedPosition) {
  2. this.serviceId = serviceId;
  3. this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
  4. this.position = new AtomicInteger(seedPosition);
  5. }

到此,就构造完成了。

此时,我们也基本完成了loadbalancer对应的整个spring容器的初始化。

loadBalancerClient.choose

完成了spring容器初始化后,接下来开始真正执行下图2处:

首先就是获取loadbalancer,就是从容器内获取ReactorServiceInstanceLoadBalancer类型的bean:

  1. @Override
  2. public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
  3. return getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class);
  4. }
  5. public <T> T getInstance(String name, Class<T> type) {
  6. AnnotationConfigApplicationContext context = getContext(name);
  7. try {
  8. return context.getBean(type);
  9. }
  10. catch (NoSuchBeanDefinitionException e) {
  11. // ignore
  12. }
  13. return null;
  14. }

容器中,这种类型的bean,就只有前面讲的RoundRobinLoadBalancer.

然后调用loadBalancer.choose(request):

  1. org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer#choose
  2. public Mono<Response<ServiceInstance>> choose(Request request) {
  3. // 1
  4. ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
  5. .getIfAvailable(NoopServiceInstanceListSupplier::new);
  6. // 2
  7. return supplier.get(request).next()
  8. .map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
  9. }

1处就从容器中获取到前面提到的CachingServiceInstanceListSupplier。

2处的supplier.get(request):

然后对这个Flux<List<ServiceInstance>>类型的对象,执行next,把当前对象变成了MonoNext类型的对象,MonoNext的注释是:Emits a single item at most from the source.

接下来是map操作,转成了一个MonoMap类型的对象:

这里还不会实际触发上面的客户端负载均衡逻辑,此时只是封装成了MonoMap:

把MonoMap丢给了如下的from函数,里面把MonoMap强转为了Mono类型:

接下来执行block操作,转为同步阻塞:

  1. Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();

这里我感觉就是,创建了一个实际的订阅者,且这个订阅者订阅了当前这个MonoMap,所以这个MonoMap就得真正开始干活了(之前只是把一堆操作给封装进去了,但没有实际做)。

此时,也会真正触发如下地方:

这里完成后呢,就真正拿到了服务实例列表,此时,就会触发之前那个map函数:

根据当前loadbalancer的算法(随机算法),进行多个服务实例中选一个的操作:

接着,我们终于拿到了一个实例了,可以进行后续调用了:

总结

反应式编程,这个真是太难看懂了,实在是劝退。

今天是大寒,马上要更冷了,不过再坚持一阵,就能春暖花开了,兄弟们

参考

https://docs.spring.io/spring-cloud-commons/docs/3.1.8/reference/html/#zone-based-load-balancing

https://mp.weixin.qq.com/s/aRpwCtgENCwubMF3idQQzQ

Feign源码解析6:如何集成discoveryClient获取服务列表的更多相关文章

  1. Feign源码解析

    1. Feign源码解析 1.1. 启动过程 1.1.1. 流程图 1.1.2. 解释说明 Feign解析过程依赖Spring的初始化,它通过实现ImportBeanDefinitionRegistr ...

  2. Feign源码解析系列-注册套路

    感谢不知名朋友的打赏,感谢你的支持! 开始 在追寻Feign源码的过程中发现了一些套路,既然是套路,就可以举一反三,所以值得关注. 这篇会详细解析Feign Client配置和初始化的方式,这些方式大 ...

  3. Feign源码解析系列-那些注解们

    开始 Feign在Spring Cloud体系中被整合进来作为web service客户端,使用HTTP请求远程服务时能就像调用本地方法,可见在未来一段时间内,大多数Spring Cloud架构的微服 ...

  4. Feign源码解析系列-最佳实践

    前几篇准备写完feign的源码,这篇直接给出Feign的最佳实践,考虑到目前网上还没有一个比较好的实践解释,对于新使用spring cloud的同学会对微服务之间的依赖产生一些迷惑,也会走一些弯路.这 ...

  5. Feign源码解析系列-核心初始化

    开始 初始化Feign客户端当然是整个过程中的核心部分,毕竟初始化完毕就等着调用了,初始化时候准备的什么,流程就走什么. 内容 从上一篇中,我们已经知道,对于扫描到的每一个有@FeignClient, ...

  6. Feign 系列(05)Spring Cloud OpenFeign 源码解析

    Feign 系列(05)Spring Cloud OpenFeign 源码解析 [TOC] Spring Cloud 系列目录(https://www.cnblogs.com/binarylei/p/ ...

  7. Feign 系列(04)Contract 源码解析

    Feign 系列(04)Contract 源码解析 [TOC] Spring Cloud 系列目录(https://www.cnblogs.com/binarylei/p/11563952.html# ...

  8. jQuery2.x源码解析(构建篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 笔者阅读了园友艾伦 Aaron的系列博客< ...

  9. Spring-cloud & Netflix 源码解析:Eureka 服务注册发现接口 ****

    http://www.idouba.net/spring-cloud-source-eureka-client-api/?utm_source=tuicool&utm_medium=refer ...

  10. Java 集合系列12之 TreeMap详细介绍(源码解析)和使用示例

    概要 这一章,我们对TreeMap进行学习.我们先对TreeMap有个整体认识,然后再学习它的源码,最后再通过实例来学会使用TreeMap.内容包括:第1部分 TreeMap介绍第2部分 TreeMa ...

随机推荐

  1. 简便实用:在 ASP.NET Core 中实现 PDF 的加载与显示

    前言 在Web应用开发中,经常需要实现PDF文件的加载和显示功能.本文小编将为您介绍如何在ASP.NET Core中实现这一功能,以便用户可以在Web应用中查看和浏览PDF文件. 实现步骤 1)在服务 ...

  2. SpringBoot对象拷贝

    目录 概述 定义实体类 Car size carInfo 造测试数据 Spring BeanUtils Apache BeanUtils Cglib BeanCopier MapStruct 性能测试 ...

  3. GPT-4多模态大型语言模型发布

    GPT-4 模型是OpenAI开发的第四代大型语言模型(LLM),它将是一个多模态模型,会提供完全不同的可能性-例如文字转图像.音乐甚至视频.GPT 全称为 Generative Pre-traine ...

  4. 解决方案 | VS2022 + AutoCAD2024 + ObjectARX2024环境搭建过程

    一.准备工具 1.vs2022 自行网络搜索,各种版本均可(比如专业版.社区版),注意使用社区版必须使用最新版,目前是17.8版本,否则最终会无法使用样板. 2.cad2024 自行网络搜索 3.Ob ...

  5. Python——第二章:字典的循环、嵌套、"解构"(解包)

    字典进阶操作 -- 循环和嵌套 字典的循环 我们先看直接打印字典的样子,会分别对每对key:value进行打印,并使用,分隔他们 dic = { "赵四": "特别能歪嘴 ...

  6. 在centos7.9中 修改docker0 的网卡默认IP地址

    docker0网卡的默认IP地址为172.17.0.1/16 因此很可能会与企业网中的业务地址冲突,为了解决这个问题,必须修改docker0的网卡配置,操作如下 1.查看网卡docker0的默认地址  ...

  7. 当创建pvc后,kubernetes组件如何协作

    本文分享自华为云社区<当创建一个pvc后,kubernetes会发生什么?>,作者:可以交个朋友. 一.背景 外部存储接入 Kubernetes 的方式主要有两种:In-Tree 和 Ou ...

  8. QNX 性能分析工具

    QNX 性能分析工具 小结 hogs:列出最占用 CPU/RAM 的进程,可以统计内存占用详细情况[可单个进程] pidin:显示进程信息.系统信息.库信息...[可单个进程] ps:显示进程信息[可 ...

  9. 一文带你了解GaussDB(DWS) 的Roach逻辑备份实现原理

    摘要:Roach工具是GaussDB(DWS)推出的一款主力的备份恢复工具,包含物理与逻辑备份两种主要能力,本文着重于讲解Roach逻辑备份的实现原理. 一.简介 在大数据时代,数据的完整和可靠性成为 ...

  10. JPEG/Exif/TIFF格式解读(4):win10照片旋转win7不识别

    xif元数据根据不同的内容分布在五个不同的IFD中. IFD0中的数据是由TIFF定义的基本图像数据,其中有些与照片无关,所以Exif只实现其中一小部分.这部份数据在Photoshop中称为TIFF元 ...