在SpringCloud框架中,有一个DiscoveryClient接口和一个同名的DiscoveryClient类,其中:DiscoveryClient类是Netflix开源框架提供的,主要用于与Eureka服务端(即注册中心)进行交互;DiscoveryClient接口是SpringCloud框架提供的,主要为了扩展Netflix提供的Eureka客户端而提供的,该接口的实现类通过组合的方式引入了Netflix提供的DiscoveryClient类,然后进行了进一步封装,让开发者更加容易使用SpringBoot进行基于Eureka的开发。
在SpringCloud提供的DiscoveryClient接口中有四个实现类,分别是:
在Netflix提供的DiscoveryClient类的层级结构中,其中,LookupService接口、EurekaClient接口和DiscoveryClient类是Netflix提供的,而最后一个CloudEurekaClient类是SpringCloud基于DiscoveryClient类的扩展,方便与SpringBoot更好的集成。
我们这里主要学习DiscoveryClient接口和其对应Eureka服务的实现类EurekaDiscoveryClient。
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;
}
}
EurekaDiscoveryClient实现类就是基于Eureka实现了DiscoveryClient接口。在该实现类中,通过构造函数注入了EurekaClient和EurekaClientConfig属性,这两个属性就是专门用来进行与Eureka服务进行交互的组件,是由Netflix提供的。
该方法中,首先通过调用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;
}
该方法主要用来获取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;
}
DiscoveryClient接口的getInstances()方法会返回一个ServiceInstance集合,而ServiceInstance是SpringCloud定义的一个在服务发现中表示服务实例的接口,DefaultServiceInstance是该接口的一个默认实现;而EurekaServiceInstance就是表示基于Eureka进行服务发现的服务实例的实现。
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;
}
}
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);
}
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();
}
//……省略
}
根据前面类的层级结构图,我们可以知道DiscoveryClient类是Netflix框架提供的服务发现的最终的组件了(Spring Cloud又提供了一个DiscoveryClient类的子类CloudEurekaClient),而DiscoveryClient类实现了EurekaClient接口,EurekaClient接口又继承了LookupService接口。
LookupService
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);
}
EurekaClient接口扩展了服务发现接口LookupService,该接口提供了用于Eureka1.x到Eureka2.x的迁移的方法。默认使用了DiscoveryClient实现类,然后增强了如下能力:
@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();
}
DiscoveryClient类主要就是客户端用来与Eureka服务端进行交互的类,主要实现以下功能:
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构造函数主要实现了:
CloudEurekaClient类是有SpringCloud提供的,继承了Netflix提供的DiscoveryClient类。
在CloudEurekaClient类中,主要增强了一下能力:
在这篇内容中,我们只是简单的了解DiscoveryClient接口和类的层级结构,尤其是Netflix框架提供的DiscoveryClient类是非常复杂的,我们这里只是简单的学习了它构造函数的处理逻辑,其中还涉及了很多的细节,比如服务发现、服务注册等。后续我们在学习过程中,逐步学习DiscoveryClient类中的方法。