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接口中有四个实现类,分别是:

  1. NoopDiscoveryClient,已经被标记过期
  2. EurekaDiscoveryClient,主要用来当Eureka作为注册中心进行管理服务实例时使用
  3. CompositeDiscoveryClient,是一个组合器,主要用来组合多种DiscoveryClient实现,具体还是交给其他DiscoveryClient实现类进行处理
  4. 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);
	//获取所有的服务Id
	List<String> getServices();

	@Override
	default 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 {
    
    
	//实例Id
	default String getInstanceId() {
    
    
		return null;
	}
	//服务Id
	String getServiceId();
	//主机名或地址
	String getHost();
	//端口号
	int getPort();
	//是否使用安全协议HTTPS
	boolean isSecure();
	//服务的URL
	URI getUri();
	//服务实例相关的元数据
	Map<String, String> getMetadata();
	//使用方案,一般表示协议,比如http或https
	default String getScheme() {
    
    
		return null;
	}

}
3.2、DefaultServiceInstance类

  DefaultServiceInstance类,有点类似在业务系统开发中定义的实体Bean,除了定义了接口中对应方法的属性之外,然后就是提供了构造函数和对应实现的getter方法,这里不再贴出代码了,只看一个getUri()方法,稍微做了一些处理,如下:

public static URI getUri(ServiceInstance instance) {
    
    
	//根据是否开启安全协议,选择https或http
	String scheme = (instance.isSecure()) ? "https" : "http";
	//然后,构建uri
	String 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
	@Override
	public String getInstanceId() {
    
    
		return this.instance.getId();
	}
	//使用InstanceInfo 对象的appName
	@Override
	public String getServiceId() {
    
    
		return this.instance.getAppName();
	}
	//使用InstanceInfo 对象的hostName
	@Override
	public String getHost() {
    
    
		return this.instance.getHostName();
	}
	//使用InstanceInfo 对象的port,如果安全协议使用securePort端口
	@Override
	public int getPort() {
    
    
		if (isSecure()) {
    
    
			return this.instance.getSecurePort();
		}
		return this.instance.getPort();
	}
	//判断是否开启了Https协议,还是基于InstanceInfo对象判断
	@Override
	public boolean isSecure() {
    
    
		// assume if secure is enabled, that is the default
		return this.instance.isPortEnabled(SECURE);
	}
	//获取对应的uri,格式:“协议://host:port”,其中协议可选http或https
	@Override
	public URI getUri() {
    
    
		return DefaultServiceInstance.getUri(this);
	}
	//使用InstanceInfo 对象的元数据
	@Override
	public Map<String, String> getMetadata() {
    
    
		return this.instance.getMetadata();
	}
	//使用URL的模式,http或https
	@Override
	public 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实现类,然后增强了如下能力:

  1. 提供了获取InstanceInfo实例的方法
  2. 提供获取Eureka 客户端的信息,比如regions等的方法
  3. 为客户端提供注册和访问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和remote
    public Set<String> getAllKnownRegions();
	//Eureka服务端当前实例的状态
    public InstanceInfo.InstanceStatus getInstanceRemoteStatus();
	//该方法已经迁移到了EndpointUtils类中,获取当前客户端所在zone的所有可以通信的Eureka服务端的URL集合
    @Deprecated
    public List<String> getDiscoveryServiceUrls(String zone);
	//该方法已经迁移到了EndpointUtils类中,获取当前客户端所在zone的所有可以通信的Eureka服务端的URL集合(在配置文件中配置的URL)
    @Deprecated
    public List<String> getServiceUrlsFromConfig(String instanceZone, boolean preferSameZone);
	//该方法已经迁移到了EndpointUtils类中,获取当前客户端所在zone的所有可以通信的Eureka服务端的URL集合(从DNS中)
    @Deprecated
    public List<String> getServiceUrlsFromDNS(String instanceZone, boolean preferSameZone);
	//标记过期,已迁移到com.netflix.appinfo.HealthCheckHandler类,为客户端提供注册HealthCheckCallback。
    @Deprecated
    public void registerHealthCheckCallback(HealthCheckCallback callback);
	//注册HealthCheckHandler
    public void registerHealthCheck(HealthCheckHandler healthCheckHandler);
	//注册EurekaEventListener,用于监控客户端内部的状态变化
    public void registerEventListener(EurekaEventListener eventListener);
    //解除注册的EurekaEventListener
    public boolean unregisterEventListener(EurekaEventListener eventListener);
    //获取注册的HealthCheckHandler 对象
    public HealthCheckHandler getHealthCheckHandler();
	//关闭客户端
    public void shutdown();
    //获取客户端配置对象EurekaClientConfig
    public EurekaClientConfig getEurekaClientConfig();
    //获取ApplicationInfoManager对象
    public ApplicationInfoManager getApplicationInfoManager();
}
4.3、DiscoveryClient类

  DiscoveryClient类主要就是客户端用来与Eureka服务端进行交互的类,主要实现以下功能:

  1. 初始化,开启定时任务(续约、心跳等)
  2. 服务注册
  3. 服务续约
  4. 取消租约
  5. 查询注册的服务或服务实例
  6. 其他
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服务端注册信息时,处理其中的对象,一般设置为null
   if (!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 DiscoveryClient
       DiscoveryManager.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构造函数主要实现了:

  1. 首先,是完成了一些属性对象的初始化工作,比如healthCheckHandlerProvider、applicationInfoManager、eurekaTransport、scheduler、heartbeatExecutor、cacheRefreshExecutor等。
  2. 然后,根据配置文件中fetchRegistry的配置项(默认为true),通过调用fetchRegistry()方法,实现Eureka服务端注册信息的拉去,该方法中又分为全量和增量两种方式。
  3. 当通过fetchRegistry()方法拉去失败后,会调用fetchRegistryFromBackup()方法,从缓存中获取注册信息
  4. 如果通过fetchRegistryFromBackup()方法也拉去信息失败,且要求必须在初始化时进行信息的拉去(通过shouldEnforceFetchRegistryAtInit配置,默认为false),则会抛出对应的异常。
  5. 提供了注册实例前的回调入口,可以进行功能增强。主要通过实现preRegistrationHandler.beforeRegistration()方法进行。
  6. 判断是否在客户端启动时,就把当前实例注册到Eureka服务,由registerWithEureka(默认true)和shouldEnforceRegistrationAtInit(默认false)两个配置决定,所以默认配置下,不会在启动时进行服务注册。
  7. 然后,调用initScheduledTasks()方法,该方法初始化了,一系列的定时任务,包括心跳、服务发现、服务注册等。
  8. 最后,调用Monitors.registerObject(this)方法,注册监听器。
4.4、CloudEurekaClient类

  CloudEurekaClient类是有SpringCloud提供的,继承了Netflix提供的DiscoveryClient类。

  在CloudEurekaClient类中,主要增强了一下能力:

  1. 提供了获取InstanceInfo对象的方法
  2. 提供了获取EurekaHttpClient对象的方法
  3. 提供了服务实例对象状态更新和删除重写状态的方法
  4. 重写了父类的onCacheRefreshed()方法,在原来的基础上,增加了HeartbeatEvent事件的处理。

5、总结

  在这篇内容中,我们只是简单的了解DiscoveryClient接口和类的层级结构,尤其是Netflix框架提供的DiscoveryClient类是非常复杂的,我们这里只是简单的学习了它构造函数的处理逻辑,其中还涉及了很多的细节,比如服务发现、服务注册等。后续我们在学习过程中,逐步学习DiscoveryClient类中的方法。

猜你喜欢

转载自blog.csdn.net/hou_ge/article/details/111600889