SpringCloud源码学习笔记之Eureka客户端——DiscoveryClient接口的层级结构
1、DiscoveryClient接口和类
在SpringCloud框架中,有一个DiscoveryClient接口和一个同名的DiscoveryClient类,其中:DiscoveryClient类是Netflix开源框架提供的,主要用于与Eureka服务端(即注册中心)进行交互;DiscoveryClient接口是SpringCloud框架提供的,主要为了扩展Netflix提供的Eureka客户端而提供的,该接口的实现类通过组合的方式引入了Netflix提供的DiscoveryClient类,然后进行了进一步封装,让开发者更加容易使用SpringBoot进行基于Eureka的开发。
1.1、SpringCloud提供的DiscoveryClient接口
在SpringCloud提供的DiscoveryClient接口中有四个实现类,分别是:
- NoopDiscoveryClient,已经被标记过期
- EurekaDiscoveryClient,主要用来当Eureka作为注册中心进行管理服务实例时使用
- CompositeDiscoveryClient,是一个组合器,主要用来组合多种DiscoveryClient实现,具体还是交给其他DiscoveryClient实现类进行处理
- SimpleDiscoveryClient,主要用来当使用属性文件管理服务实例的场景
1.2、Netflix提供的DiscoveryClient类
在Netflix提供的DiscoveryClient类的层级结构中,其中,LookupService接口、EurekaClient接口和DiscoveryClient类是Netflix提供的,而最后一个CloudEurekaClient类是SpringCloud基于DiscoveryClient类的扩展,方便与SpringBoot更好的集成。
2、SpringCloud中的DiscoveryClient接口及其实现
我们这里主要学习DiscoveryClient接口和其对应Eureka服务的实现类EurekaDiscoveryClient。
2.1、DiscoveryClient接口
DiscoveryClient接口是SpringCloud提供的用来进行服务发现的通用接口,一般可以用于Eureka或consul等,主要提供了读取实例的方法,如下所示:
public interface DiscoveryClient extends Ordered {int DEFAULT_ORDER = 0;//实现类的功能描述,一般用在HealthIndicator的打印日志中String description();//根据服务Id获取对应的服务实例集合List<ServiceInstance> getInstances(String serviceId);//获取所有的服务IdList<String> getServices();@Overridedefault int getOrder() {return DEFAULT_ORDER;}}
2.2、EurekaDiscoveryClient实现类
EurekaDiscoveryClient实现类就是基于Eureka实现了DiscoveryClient接口。在该实现类中,通过构造函数注入了EurekaClient和EurekaClientConfig属性,这两个属性就是专门用来进行与Eureka服务进行交互的组件,是由Netflix提供的。
2.2.1、getInstances()方法
该方法中,首先通过调用eurekaClient的getInstancesByVipAddress()方法获取对应的实例集合,然后再遍历集合构造成EurekaServiceInstance集合。
@Override
public List<ServiceInstance> getInstances(String serviceId) {List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId,false);List<ServiceInstance> instances = new ArrayList<>();for (InstanceInfo info : infos) {instances.add(new EurekaServiceInstance(info));}return instances;
}
2.2.2、getServices()方法
该方法主要用来获取Eureka服务端所有的服务实例的,还是通过eurekaClient实例与Eureka服务进行交互,具体实现如下:
@Override
public List<String> getServices() {//获取Eureka服务端服务集合,一般客户端请求会返回该对象Applications applications = this.eurekaClient.getApplications();if (applications == null) {return Collections.emptyList();}//从Applications中获取服务集合Application,每一个Application表示一个服务集合,其中会包含多个InstanceInfo实例List<Application> registered = applications.getRegisteredApplications();List<String> names = new ArrayList<>();//遍历,构建成一个String集合并返回for (Application app : registered) {if (app.getInstances().isEmpty()) {continue;}names.add(app.getName().toLowerCase());}return names;
}
3、ServiceInstance接口及其实现类
DiscoveryClient接口的getInstances()方法会返回一个ServiceInstance集合,而ServiceInstance是SpringCloud定义的一个在服务发现中表示服务实例的接口,DefaultServiceInstance是该接口的一个默认实现;而EurekaServiceInstance就是表示基于Eureka进行服务发现的服务实例的实现。
3.1、ServiceInstance 接口
public interface ServiceInstance {//实例Iddefault String getInstanceId() {return null;}//服务IdString getServiceId();//主机名或地址String getHost();//端口号int getPort();//是否使用安全协议HTTPSboolean isSecure();//服务的URLURI getUri();//服务实例相关的元数据Map<String, String> getMetadata();//使用方案,一般表示协议,比如http或httpsdefault String getScheme() {return null;}}
3.2、DefaultServiceInstance类
DefaultServiceInstance类,有点类似在业务系统开发中定义的实体Bean,除了定义了接口中对应方法的属性之外,然后就是提供了构造函数和对应实现的getter方法,这里不再贴出代码了,只看一个getUri()方法,稍微做了一些处理,如下:
public static URI getUri(ServiceInstance instance) {//根据是否开启安全协议,选择https或httpString scheme = (instance.isSecure()) ? "https" : "http";//然后,构建uriString uri = String.format("%s://%s:%s", scheme, instance.getHost(),instance.getPort());return URI.create(uri);
}
3.3、EurekaServiceInstance类
EurekaServiceInstance类就是表示基于Eureka注册中心进行服务发现的服务实例,其中持有一个InstanceInfo对象表示Eureka注册中心中所使用的的服务实例,然后再扩展在SpringCloud需要的一些属性。
public class EurekaServiceInstance implements ServiceInstance {//InstanceInfo 是Eureka注册中心中使用的服务实例对象private InstanceInfo instance;//通过构造函数注入InstanceInfo 对象public EurekaServiceInstance(InstanceInfo instance) {Assert.notNull(instance, "Service instance required");this.instance = instance;}public InstanceInfo getInstanceInfo() {return instance;}//使用InstanceInfo 对象的id@Overridepublic String getInstanceId() {return this.instance.getId();}//使用InstanceInfo 对象的appName@Overridepublic String getServiceId() {return this.instance.getAppName();}//使用InstanceInfo 对象的hostName@Overridepublic String getHost() {return this.instance.getHostName();}//使用InstanceInfo 对象的port,如果安全协议使用securePort端口@Overridepublic int getPort() {if (isSecure()) {return this.instance.getSecurePort();}return this.instance.getPort();}//判断是否开启了Https协议,还是基于InstanceInfo对象判断@Overridepublic boolean isSecure() {// assume if secure is enabled, that is the defaultreturn this.instance.isPortEnabled(SECURE);}//获取对应的uri,格式:“协议://host:port”,其中协议可选http或https@Overridepublic URI getUri() {return DefaultServiceInstance.getUri(this);}//使用InstanceInfo 对象的元数据@Overridepublic Map<String, String> getMetadata() {return this.instance.getMetadata();}//使用URL的模式,http或https@Overridepublic String getScheme() {return getUri().getScheme();}//……省略
}
4、Netflix提供的DiscoveryClient类的层级结构
根据前面类的层级结构图,我们可以知道DiscoveryClient类是Netflix框架提供的服务发现的最终的组件了(Spring Cloud又提供了一个DiscoveryClient类的子类CloudEurekaClient),而DiscoveryClient类实现了EurekaClient接口,EurekaClient接口又继承了LookupService接口。
4.1、LookupService接口
LookupService<T>接口主要用来进行服务发现,即查找活动的服务实例。
public interface LookupService<T> {//根据appName查找对应的Application对象,该对象维护了一个服务实例列表,即Application对象维护了一个指定应用的服务实例列表的容器。Application getApplication(String appName);//包装了Eureka服务返回的全部注册信息,其中维护了一个Application对象的集合Applications getApplications();//根据实例Id,查询对应的服务实例列表List<InstanceInfo> getInstancesById(String id);//获取下一个用于处理请求的服务实例(只返回UP状态的服务实例,可以通过重写EurekaClientConfig#shouldFilterOnlyUpInstances()方法进行修改)InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure);
}
4.2、EurekaClient接口
EurekaClient接口扩展了服务发现接口LookupService,该接口提供了用于Eureka1.x到Eureka2.x的迁移的方法。默认使用了DiscoveryClient实现类,然后增强了如下能力:
- 提供了获取InstanceInfo实例的方法
- 提供获取Eureka 客户端的信息,比如regions等的方法
- 为客户端提供注册和访问healthcheck处理器的能力
@ImplementedBy(DiscoveryClient.class)
public interface EurekaClient extends LookupService {//获取指定region下的Applications对象public Applications getApplicationsForARegion(@Nullable String region);//根据serviceUrl获取Applications 对象public Applications getApplications(String serviceUrl);//获取匹配VIP地址的InstanceInfo集合public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure);//获取匹配VIP地址、region的InstanceInfo集合public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure, @Nullable String region);//获取匹配VIP地址、appName的InstanceInfo集合public List<InstanceInfo> getInstancesByVipAddressAndAppName(String vipAddress, String appName, boolean secure);//获取能够被当前客户端访问的所有region,包括local和remotepublic Set<String> getAllKnownRegions();//Eureka服务端当前实例的状态public InstanceInfo.InstanceStatus getInstanceRemoteStatus();//该方法已经迁移到了EndpointUtils类中,获取当前客户端所在zone的所有可以通信的Eureka服务端的URL集合@Deprecatedpublic List<String> getDiscoveryServiceUrls(String zone);//该方法已经迁移到了EndpointUtils类中,获取当前客户端所在zone的所有可以通信的Eureka服务端的URL集合(在配置文件中配置的URL)@Deprecatedpublic List<String> getServiceUrlsFromConfig(String instanceZone, boolean preferSameZone);//该方法已经迁移到了EndpointUtils类中,获取当前客户端所在zone的所有可以通信的Eureka服务端的URL集合(从DNS中)@Deprecatedpublic List<String> getServiceUrlsFromDNS(String instanceZone, boolean preferSameZone);//标记过期,已迁移到com.netflix.appinfo.HealthCheckHandler类,为客户端提供注册HealthCheckCallback。@Deprecatedpublic void registerHealthCheckCallback(HealthCheckCallback callback);//注册HealthCheckHandlerpublic void registerHealthCheck(HealthCheckHandler healthCheckHandler);//注册EurekaEventListener,用于监控客户端内部的状态变化public void registerEventListener(EurekaEventListener eventListener);//解除注册的EurekaEventListenerpublic boolean unregisterEventListener(EurekaEventListener eventListener);//获取注册的HealthCheckHandler 对象public HealthCheckHandler getHealthCheckHandler();//关闭客户端public void shutdown();//获取客户端配置对象EurekaClientConfigpublic EurekaClientConfig getEurekaClientConfig();//获取ApplicationInfoManager对象public ApplicationInfoManager getApplicationInfoManager();
}
4.3、DiscoveryClient类
DiscoveryClient类主要就是客户端用来与Eureka服务端进行交互的类,主要实现以下功能:
- 初始化,开启定时任务(续约、心跳等)
- 服务注册
- 服务续约
- 取消租约
- 查询注册的服务或服务实例
- 其他
4.3.1、DiscoveryClient构造函数
DiscoveryClient类有很多个构造函数,而且还有一些已经被打上弃用标签,如下所示,我们这里选择最后一个进行分析和学习,其他构造函数最终都是调用了这个构造函数。
DiscoveryClient构造函数的部分代码:
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {//省略 变量赋值……logger.info("Initializing Eureka in region {}", clientConfig.getRegion());//当客户端不需要注册自身到服务端和获取Eureka服务端注册信息时,处理其中的对象,一般设置为nullif (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {logger.info("Client configured to neither register nor query for data.");scheduler = null;heartbeatExecutor = null;cacheRefreshExecutor = null;eurekaTransport = null;instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()// to work with DI'd DiscoveryClientDiscoveryManager.getInstance().setDiscoveryClient(this);DiscoveryManager.getInstance().setEurekaClientConfig(config);initTimestampMs = System.currentTimeMillis();initRegistrySize = this.getApplications().size();registrySize = initRegistrySize;logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",initTimestampMs, initRegistrySize);return; // no need to setup up an network tasks and we are done}try {// 创建一个定长的线程池,而且支持周期性的任务执行scheduler = Executors.newScheduledThreadPool(2,new ThreadFactoryBuilder().setNameFormat("DiscoveryClient-%d").setDaemon(true).build());//创建用于心跳发送的线程池,创建核心线程数为1,最大线程池为2,空闲时间0,单位秒,等待队列,创建工厂heartbeatExecutor = new ThreadPoolExecutor(1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,new SynchronousQueue<Runnable>(),new ThreadFactoryBuilder().setNameFormat("DiscoveryClient-HeartbeatExecutor-%d").setDaemon(true).build()); // use direct handoff//创建用于缓存刷新的线程池cacheRefreshExecutor = new ThreadPoolExecutor(1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,new SynchronousQueue<Runnable>(),new ThreadFactoryBuilder().setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d").setDaemon(true).build()); // use direct handoff//构建EurekaTransport对象,它是Eureka 服务端与客户端进行http交互的jersey组件eurekaTransport = new EurekaTransport();//初始化 EurekaTransport对象scheduleServerEndpointTask(eurekaTransport, args);AzToRegionMapper azToRegionMapper;//主要用于 AWS,处理Region相关内容if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);} else {azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);}if (null != remoteRegionsToFetch.get()) {azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(","));}//应用实例信息区域( region )校验instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion());} catch (Throwable e) {throw new RuntimeException("Failed to initialize DiscoveryClient!", e);}//首先,判断是否允许拉去Eureka服务端的注册数据if (clientConfig.shouldFetchRegistry()) {try {//获取Eureka服务端的注册数据boolean primaryFetchRegistryResult = fetchRegistry(false);if (!primaryFetchRegistryResult) {logger.info("Initial registry fetch from primary servers failed");}boolean backupFetchRegistryResult = true;//从备份中获取Eureka服务端的注册数据if (!primaryFetchRegistryResult && !fetchRegistryFromBackup()) {backupFetchRegistryResult = false;logger.info("Initial registry fetch from backup servers failed");}//当没有获取到注册数据,且要求启动时进行初始化,这个时候就会跑出异常if (!primaryFetchRegistryResult && !backupFetchRegistryResult && clientConfig.shouldEnforceFetchRegistryAtInit()) {throw new IllegalStateException("Fetch registry error at startup. Initial fetch failed.");}} catch (Throwable th) {logger.error("Fetch registry error at startup: {}", th.getMessage());throw new IllegalStateException(th);}}// 回调,用于扩展if (this.preRegistrationHandler != null) {this.preRegistrationHandler.beforeRegistration();}//判断是否在初始化时,进行注册if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {try {if (!register() ) {throw new IllegalStateException("Registration error at startup. Invalid server response.");}} catch (Throwable th) {logger.error("Registration error at startup: {}", th.getMessage());throw new IllegalStateException(th);}}// 初始化定时任务,用于心跳、节点数据同步、获取Eureka服务端数据等。initScheduledTasks();//省略 ……}
上述的DiscoveryClient构造函数主要实现了:
- 首先,是完成了一些属性对象的初始化工作,比如healthCheckHandlerProvider、applicationInfoManager、eurekaTransport、scheduler、heartbeatExecutor、cacheRefreshExecutor等。
- 然后,根据配置文件中fetchRegistry的配置项(默认为true),通过调用fetchRegistry()方法,实现Eureka服务端注册信息的拉去,该方法中又分为全量和增量两种方式。
- 当通过fetchRegistry()方法拉去失败后,会调用fetchRegistryFromBackup()方法,从缓存中获取注册信息
- 如果通过fetchRegistryFromBackup()方法也拉去信息失败,且要求必须在初始化时进行信息的拉去(通过shouldEnforceFetchRegistryAtInit配置,默认为false),则会抛出对应的异常。
- 提供了注册实例前的回调入口,可以进行功能增强。主要通过实现preRegistrationHandler.beforeRegistration()方法进行。
- 判断是否在客户端启动时,就把当前实例注册到Eureka服务,由registerWithEureka(默认true)和shouldEnforceRegistrationAtInit(默认false)两个配置决定,所以默认配置下,不会在启动时进行服务注册。
- 然后,调用initScheduledTasks()方法,该方法初始化了,一系列的定时任务,包括心跳、服务发现、服务注册等。
- 最后,调用Monitors.registerObject(this)方法,注册监听器。
4.4、CloudEurekaClient类
CloudEurekaClient类是有SpringCloud提供的,继承了Netflix提供的DiscoveryClient类。
在CloudEurekaClient类中,主要增强了一下能力:
- 提供了获取InstanceInfo对象的方法
- 提供了获取EurekaHttpClient对象的方法
- 提供了服务实例对象状态更新和删除重写状态的方法
- 重写了父类的onCacheRefreshed()方法,在原来的基础上,增加了HeartbeatEvent事件的处理。
5、总结
在这篇内容中,我们只是简单的了解DiscoveryClient接口和类的层级结构,尤其是Netflix框架提供的DiscoveryClient类是非常复杂的,我们这里只是简单的学习了它构造函数的处理逻辑,其中还涉及了很多的细节,比如服务发现、服务注册等。后续我们在学习过程中,逐步学习DiscoveryClient类中的方法。
SpringCloud源码学习笔记之Eureka客户端——DiscoveryClient接口的层级结构相关推荐
- 雷神FFMpeg源码学习笔记
雷神FFMpeg源码学习笔记 文章目录 雷神FFMpeg源码学习笔记 读取编码并依据编码初始化内容结构 每一帧的视频解码处理 读取编码并依据编码初始化内容结构 在开始编解码视频的时候首先第一步需要注册 ...
- RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的?
RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的? 文章目录 RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的? 前言 项目 ...
- Java多线程之JUC包:Semaphore源码学习笔记
若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/go2sea/p/5625536.html Semaphore是JUC ...
- Vuex 4源码学习笔记 - 通过Vuex源码学习E2E测试(十一)
在上一篇笔记中:Vuex 4源码学习笔记 - 做好changelog更新日志很重要(十) 我们学到了通过conventional-changelog来生成项目的Changelog更新日志,通过更新日志 ...
- Vuex 4源码学习笔记 - Vuex是怎么与Vue结合?(三)
在上一篇笔记中:Vuex源码学习笔记 - Vuex开发运行流程(二) 我们通过运行npm run dev命令来启动webpack,来开发Vuex,并在Vuex的createStore函数中添加了第一个 ...
- jquery源码学习笔记三:jQuery工厂剖析
jquery源码学习笔记二:jQuery工厂 jquery源码学习笔记一:总体结构 上两篇说过,query的核心是一个jQuery工厂.其代码如下 function( window, noGlobal ...
- Apache log4j-1.2.17源码学习笔记
(1)Apache log4j-1.2.17源码学习笔记 http://blog.csdn.net/zilong_zilong/article/details/78715500 (2)Apache l ...
- PHP Yac cache 源码学习笔记
YAC 源码学习笔记 本文地址 http://blog.csdn.net/fanhengguang_php/article/details/54863955 config.m4 检测系统共享内存支持情 ...
- Vuex 4源码学习笔记 - 通过dispatch一步步来掌握Vuex整个数据流(五)
在上一篇笔记中:Vuex 4源码学习笔记 - Store 构造函数都干了什么(四) 我们通过查看Store 构造函数的源代码可以看到主要做了三件事情: 初始化一些内部变量以外 执行installMod ...
最新文章
- SQL Server审计功能入门:CDC(Change Data Capture)
- Android使用SQLite数据库(1)
- C++ Primer 第9章 顺序容器 第一次学习笔记
- Anaconda 安装操作及遇到的坑
- 支付宝打年兽神器 v1.4.5
- Cookie工具类-cookieUtil.js
- Windows Phone开发(18):变形金刚第九季——变换 转:http://blog.csdn.net/tcjiaan/article/details/7385056...
- Form.close与Application.Exit()的区别
- 【产品介绍】“弹性裸金属服务器”到底有那些特性?
- wpewebkit在ubuntu18.04上编译配置
- 世界500强面试题(经典)
- 5, Swift tableView点击收起展开的效果
- 基于MIPS指令集的单周期处理器设计
- 苹果mac系统在文件栏添加文件夹的方法
- swift网络请求封装(Moya)
- log4j2.xsml配置文件详细
- 【go 科学计算】用于统计、机器学习和数据操作
- 原生js的JSONP跨域请求
- 2019年最新的一波手机APP分享,每一个都是大家的菜哦!
- 为什么p元素内不能嵌套Div元素(随笔)
热门文章
- oracle特殊字符转码,URL转码方法及不能被转码的特殊字符
- Matlab中readtable用法
- Setup和Hold(Max/Min)分析
- HomeIDC的核心:购买顶级域名+开源自动域名解析+公网映射的保姆级教程
- [PHP] pow指数运算函数与二进制
- C++ 排序函数 sort(),qsort()的用法
- 使用PACKET_MMAP + PF_PACKET实现ZERO COPY抓包和发包
- 移动端网页实现拨打电话功能的几种方法
- 回归系数t检验公式_如何用分组回归检验调节作用?
- 戴尔PowerEdge R730服务器通过U盘启动的方式安装Windows Server 2016