SpringCloud之EurekaClient

  1. EurekaClient 确定源码分析入口

// 程序启动主函数
@EnableEurekaClient
@SpringBootApplication
public class Provider01Application {

public static void main(String[] args) {
    SpringApplication.run(Provider01Application.class, args);
}

}

// EnableEurekaClient注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface EnableEurekaClient {

}
通过@EnableEurekaClient注解开启EurekaClient,但是从该注解源码得不到有用的信息。这时候我们可以查看一下eureka-client包META-INFO文件夹下的spring-factories文件,或许可以找到帮助,这也是分析Spring系列相关源码的一个小技巧。

// spring-factories 文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration

org.springframework.cloud.bootstrap.BootstrapConfiguration=
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration
该文件中提供了与EurekaClient初始化先关的类,但是这些类的初始化加载顺序并不明确,这时候我们可以通过在这些类中加入打印信息、查看EurekaClient控制台打印信息等确定源码分析的入口。例如在EurekaClientAutoConfiguration类中加入:

@PostConstruct
public void init(){
log.info("== spring.factories 01 EurekaClientAutoConfiguration init …");
}
启动Server、Client可以看到其初始化顺序为:

== spring.factories 01 EurekaClientAutoConfiguration init …
== spring.factories 02 EurekaDiscoveryClientConfiguration init …
== spring.factories 03 EurekaDiscoveryClientConfiguration init …
接下来就以EurekaClientAutoConfiguration为入口,开始分析。

  1. EurekaClientAutoConfiguration 注解分析

@Configuration
@EnableConfigurationProperties
@ConditionalOnClass(EurekaClientConfig.class)
@Import(DiscoveryClientOptionalArgsConfiguration.class)
@ConditionalOnBean(EurekaDiscoveryClientConfiguration.Marker.class)
@ConditionalOnProperty(value = “eureka.client.enabled”, matchIfMissing = true)
@ConditionalOnDiscoveryEnabled
@AutoConfigureBefore({
NoopDiscoveryClientAutoConfiguration.class,
CommonsClientAutoConfiguration.class,
ServiceRegistryAutoConfiguration.class
})
@AutoConfigureAfter(name = {
// RefreshScope和环境变量更改(例如重新绑定日志程序级别)自动配置类
“org.springframework.cloud.autoconfigure.RefreshAutoConfiguration”,
// 提供了EurekaHealthCheckHandler、EurekaHealthCheckHandler等相关功能
“org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration”,
// 提供了AutoServiceRegistration、AutoServiceRegistrationProperties等相关功能
“org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration”
})
public class EurekaClientAutoConfiguration {
。。。省略代码
}
该类运用了大量的注解,挑选几个比较重要的注解分析:

@Import --> 引入某一个类的实例
@AutoConfigureBefore --> 主类(EurekaClientAutoConfiguration)实例化之后,再实例化该注解指定的类
@AutoConfigureAfter --> 注解指定的类实例化之后,在实例化主类(EurekaClientAutoConfiguration)
接下来就应该分析@AutoConfigureAfter注解指定的类,RefreshAutoConfiguration、EurekaDiscoveryClientConfiguration、和AutoServiceRegistrationAutoConfiguration

2.1 @AutoConfigureAfter注解相关

2.1.1 RefreshAutoConfiguration

该类为@RefreshScope注解以及与环境更改相关的功能重新加载(例如重新绑定日志程序级别)的自动配置类。

RefreshScope 该类提供了对Bean的重新加载功能,通过refresh和refreshAll方法来实现,前者会单独销毁并重新加载某一个Bean,后者会销毁所有缓存的bean并重新加载,两者都会在下次访问时强制刷新。
// RefreshScope Bean
@Bean
@ConditionalOnMissingBean(RefreshScope.class)
public static RefreshScope refreshScope() {
return new RefreshScope();
}

// 局部刷新
public boolean refresh(String name) {
if (!name.startsWith(SCOPED_TARGET_PREFIX)) {
// User wants to refresh the bean with this name but that isn’t the one in the
// cache…
name = SCOPED_TARGET_PREFIX + name;
}
// Ensure lifecycle is finished if bean was disposable
if (super.destroy(name)) {
this.context.publishEvent(new RefreshScopeRefreshedEvent(name));
return true;
}
return false;
}

// 全部刷新
public void refreshAll() {
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
日志级别刷新,通过LoggingRebinder来实现。
// LoggingRebinder Bean
@Bean
@ConditionalOnMissingBean
public static LoggingRebinder loggingRebinder() {
return new LoggingRebinder();
}

// LoggingRebinder 源码
public class LoggingRebinder implements ApplicationListener, EnvironmentAware {

private static final Bindable<Map<String, String>> STRING_STRING_MAP = Bindable.mapOf(String.class, String.class);
private final Log logger = LogFactory.getLog(getClass());
private Environment environment;

@Override
public void setEnvironment(Environment environment) {
    this.environment = environment;
}

@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
    if (this.environment == null) {
        return;
    }
    LoggingSystem system = LoggingSystem.get(LoggingSystem.class.getClassLoader());
    setLogLevels(system, this.environment);
}

protected void setLogLevels(LoggingSystem system, Environment environment) {
    Map<String, String> levels = Binder.get(environment)
            .bind("logging.level", STRING_STRING_MAP)
            .orElseGet(Collections::emptyMap);
    for (Entry<String, String> entry : levels.entrySet()) {
        setLogLevel(system, environment, entry.getKey(), entry.getValue().toString());
    }
}

private void setLogLevel(LoggingSystem system, Environment environment, String name, String level) {
    try {
        if (name.equalsIgnoreCase("root")) {
            name = null;
        }
        level = environment.resolvePlaceholders(level);
        system.setLogLevel(name, LogLevel.valueOf(level.toUpperCase()));
    }
    catch (RuntimeException ex) {
        this.logger.error("Cannot set level: " + level + " for '" + name + "'");
    }
}

}
该类实现了EnvironmentAware接口并,系统启动时会将当前系统环境信息注入进来,并通过调用/refreash方法,触发onApplicationEvent方法重新设定日志级别。关于此例demo可以参考:@ScopeRefresh

2.1.2 EurekaDiscoveryClientConfiguration

该类主要提供了RefreshScope事件监听相关的事件、以及健康监控相关的功能,比较简单,下面我们以RefreshScope为例来分析一下该类的源码。

@Configuration
@ConditionalOnClass(RefreshScopeRefreshedEvent.class)
protected static class EurekaClientConfigurationRefresher implements ApplicationListener {

@Autowired(required = false)
private EurekaClient eurekaClient;

@Autowired(required = false)
private EurekaAutoServiceRegistration autoRegistration;

/**
 * {@link org.springframework.cloud.endpoint.RefreshEndpoint}
 * 调用/refresh请求的时候会触发此事件
 */
public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
    // This will force the creation of the EurekaClient bean if not already created
    // to make sure the client will be reregistered after a refresh event
    // 这将强制创建EurekaClient(如果还没有创建),以确保在刷新事件之后重新注册客户机
    if (eurekaClient != null) {
        eurekaClient.getApplications();
    }
    if (autoRegistration != null) {
        // register in case meta data changed
        // 注册以防元数据更改
        this.autoRegistration.stop();
        this.autoRegistration.start();
    }
}

}
前文提到了调用/refresh (如:http://localhost:7001/actuator/refresh) 方法会重新加载相关bean、设定日志级别等功能。该功能的入口在org.springframework.cloud.endpoint.RefreshEndpoint中。

// RefreshEndpoint端点
// @Endpoint注解提供了对外暴露接口的功能,可以通过JMX或者HTTP进行访问。
@Endpoint(id = “refresh”)
public class RefreshEndpoint {

private ContextRefresher contextRefresher;

public RefreshEndpoint(ContextRefresher contextRefresher) {
    this.contextRefresher = contextRefresher;
}

// refresh方法
@WriteOperation
public Collection refresh() {
Set keys = this.contextRefresher.refresh();
return keys;
}

}

// refresh方法体
public synchronized Set refresh() {
//刷新体统环境
Set keys = refreshEnvironment();
//调用refreshAll方法
this.scope.refreshAll();
return keys;
}
2.2 EurekaClientAutoConfiguration内部Bean相关

EurekaClientAutoConfiguration提供了很多相关的内部Bean对于其实例化顺序,我们可以借助代码分析,也可以通过打印日志的方式进行分析。这里为了简便起见使用日志打印的方式进行分析,以便快速理清思路。我们在这些bean中加入日志,如:

// 这些顺序我已经分析过,所以标注了01、02等顺序
log.info("== 01 EurekaClientAutoConfiguration ManagementMetadataProvider init …");
另外该类中还提供了两个静态内部类EurekaClientConfiguration和RefreshableEurekaClientConfiguration,因为EurekaClientConfiguration被@ConditionalOnMissingRefreshScope注解,所以该静态内部类中的bean不会被实例化。我们只需在RefreshableEurekaClientConfiguration类的bean中记录日志即可:

// 这些顺序我已经分析过,所以标注了01、02等顺序
log.info("== static class 01 RefreshableEurekaClientConfiguration EurekaRegistration init …");
启动程序,我们可以看到如下的日志顺序(中间省略了部分其他日志):

== 01 EurekaClientAutoConfiguration ManagementMetadataProvider init …
== 02 EurekaClientAutoConfiguration EurekaInstanceConfigBean init …
== 03 EurekaClientAutoConfiguration EurekaClientConfigBean init …
== 04 EurekaClientAutoConfiguration HasFeatures init …
== 05 EurekaClientAutoConfiguration EurekaServiceRegistry init …
== 06 EurekaClientAutoConfiguration EurekaAutoServiceRegistration init …
== 07 EurekaClientAutoConfiguration DiscoveryClient init …
== static class 01 RefreshableEurekaClientConfiguration EurekaRegistration init …
== static class 02 RefreshableEurekaClientConfiguration ApplicationInfoManager init …
== static class 03 RefreshableEurekaClientConfiguration CloudEurekaClient init …
明确了bean的实例化顺序之后,就可以开始接下来的分析了。

2.2.1 ManagementMetadataProvider

该类主要提供了管理端的一些元数据信息,可以通过其提供的get方法获取:

public ManagementMetadata get(EurekaInstanceConfigBean instance, // Instance实例信息
int serverPort, // 实例端口号
String serverContextPath, // serverContextPath
String managementContextPath, // 管理端点ContextPath
Integer managementPort){ // 管理端点端口号
// managementPort != null && managementPort == 0 返回null
if (isRandom(managementPort)) {
return null;
}
// managementPort == null && (serverPort != null && serverPort == 0) 返回null
if (managementPort == null && isRandom(serverPort)) {
return null;
}

// 获取健康监测路径
String healthCheckUrl = getHealthCheckUrl(instance, serverPort, serverContextPath, managementContextPath, managementPort, false);
// 获取状态监测路径
String statusPageUrl = getStatusPageUrl(instance, serverPort, serverContextPath, managementContextPath, managementPort);
// 实例化端点监控元数据,如果managementPort==null,则默认使用serverPort作为managementPort
ManagementMetadata metadata = new ManagementMetadata(healthCheckUrl, statusPageUrl, managementPort == null ? serverPort : managementPort);

// 如果开启了安全端口通讯,则设置secureHealthCheckUrl,以https开头
if (instance.isSecurePortEnabled()) {
    metadata.setSecureHealthCheckUrl(getHealthCheckUrl(instance, serverPort, serverContextPath, managementContextPath, managementPort, true));
}
return metadata;

}
2.2.2 EurekaInstanceConfigBean

该bean会创建一个EurekaInstanceConfigBean实例,该实例包含了Eureka实例的信息。关于这些元数据的使用,大家可以参考SpringCloud的官方文档。

@Bean
@ConditionalOnMissingBean(value = EurekaInstanceConfig.class, search = SearchStrategy.CURRENT)
public EurekaInstanceConfigBean eurekaInstanceConfigBean(InetUtils inetUtils,ManagementMetadataProvider managementMetadataProvider) {

log.info("== 02 EurekaClientAutoConfiguration EurekaInstanceConfigBean init ...");
String hostname = getProperty("eureka.instance.hostname");

// eureka.instance.prefer-ip-address: 以IP地址注册到服务中心,相互注册使用IP地址
boolean preferIpAddress = Boolean.parseBoolean(getProperty("eureka.instance.prefer-ip-address"));
// eureka.instance.ip-address: 强制指定IP地址,默认会获取本机的IP地址
String ipAddress = getProperty("eureka.instance.ip-address");

// eureka.instance.secure-port-enabled: 开启安全通信的端口,就是使用https进行通信
// 与之相对的non-secure-port-enabled:开启不安全通信的端口,就是使用http进行通信
boolean isSecurePortEnabled = Boolean.parseBoolean(getProperty("eureka.instance.secure-port-enabled"));

// 获取context-path
String serverContextPath = env.getProperty("server.servlet.context-path", "/");
// 获取server.port
int serverPort = Integer.valueOf(env.getProperty("server.port", env.getProperty("port", "8080")));
Integer managementPort = env.getProperty("management.server.port", Integer.class); // nullable.

// SpringBoot管理端点context-path
String managementContextPath = env.getProperty("management.server.servlet.context-path"); // nullable.
// jmx管理端点port
Integer jmxPort = env.getProperty("com.sun.management.jmxremote.port",Integer.class); // nullable

// 创建EurekaInstanceConfigBean,并配置该bean的信息
EurekaInstanceConfigBean instance = new EurekaInstanceConfigBean(inetUtils);

instance.setNonSecurePort(serverPort);
instance.setInstanceId(getDefaultInstanceId(env));
instance.setPreferIpAddress(preferIpAddress);
instance.setSecurePortEnabled(isSecurePortEnabled);
if (StringUtils.hasText(ipAddress)) {
    instance.setIpAddress(ipAddress);
}

if (isSecurePortEnabled) {
    instance.setSecurePort(serverPort);
}

if (StringUtils.hasText(hostname)) {
    instance.setHostname(hostname);
}

// status和health监测地址
String statusPageUrlPath = getProperty("eureka.instance.status-page-url-path");
String healthCheckUrlPath = getProperty("eureka.instance.health-check-url-path");

if (StringUtils.hasText(statusPageUrlPath)) {
    instance.setStatusPageUrlPath(statusPageUrlPath);
}
if (StringUtils.hasText(healthCheckUrlPath)) {
    instance.setHealthCheckUrlPath(healthCheckUrlPath);
}

// 获取监控端点元数据信息
ManagementMetadata metadata = managementMetadataProvider.get(instance, serverPort,
        serverContextPath, managementContextPath, managementPort);

if (metadata != null) {
    // 设置监控端点的Status、Health、SecurePort等信息
    instance.setStatusPageUrl(metadata.getStatusPageUrl());
    instance.setHealthCheckUrl(metadata.getHealthCheckUrl());
    if (instance.isSecurePortEnabled()) {
        instance.setSecureHealthCheckUrl(metadata.getSecureHealthCheckUrl());
    }
    Map<String, String> metadataMap = instance.getMetadataMap();
    metadataMap.computeIfAbsent("management.port", k -> String.valueOf(metadata.getManagementPort()));
}
else {
    // 如果没有监控端点的元数据,状态和健康检查url将不会被设置,
    // 状态页面和健康检查url路径将不包括上下文路径,所以在这里设置它们

    // without the metadata the status and health check URLs will not be set
    // and the status page and health check url paths will not include the
    // context path so set them here
    if (StringUtils.hasText(managementContextPath)) {
        instance.setHealthCheckUrlPath(managementContextPath + instance.getHealthCheckUrlPath());
        instance.setStatusPageUrlPath(managementContextPath + instance.getStatusPageUrlPath());
    }
}

// 设置Jmx监控端点
setupJmxPort(instance, jmxPort);
return instance;

}
2.2.3 EurekaClientConfigBean

@Bean
@ConditionalOnMissingBean(value = EurekaClientConfig.class, search = SearchStrategy.CURRENT)
public EurekaClientConfigBean eurekaClientConfigBean(ConfigurableEnvironment env) {
log.info("== 03 EurekaClientAutoConfiguration EurekaClientConfigBean init …");
EurekaClientConfigBean client = new EurekaClientConfigBean();
if (“bootstrap”.equals(this.env.getProperty(“spring.config.name”))) {
// 默认情况下,我们不会在引导过程中注册,但是稍后会有另一个机会。
// We don’t register during bootstrap by default, but there will be another chance later.
client.setRegisterWithEureka(false);
}
return client;
}
该段代码会创建一个EurekaClientConfigBean,该bean包含了EurekaClient相关的配置信息,下面列出这些属性的说明,关于其使用,可以多结合自己的Demo调试,

@ConfigurationProperties(EurekaClientConfigBean.PREFIX)
public class EurekaClientConfigBean implements EurekaClientConfig, Ordered {

// EurekaClient配置默认前缀
public static final String PREFIX = "eureka.client";
// Eureka默认的url
public static final String DEFAULT_URL = "http://localhost:8761" + DEFAULT_PREFIX + "/";
// 如果没有基于区域解析任何可用性区域,则默认可用性区域。
public static final String DEFAULT_ZONE = "defaultZone";
private static final int MINUTES = 60;
@Autowired(required = false)
PropertyResolver propertyResolver;
// 是否启用EurekaClient,默认为true
private boolean enabled = true;
@NestedConfigurationProperty
private EurekaTransportConfig transport = new CloudEurekaTransportConfig();
//从eureka服务器注册表中获取注册信息的时间间隔(s),默认为30秒
private int registryFetchIntervalSeconds = 30;
//复制实例变化信息到eureka服务器所需要的时间间隔(s),默认为30秒
private int instanceInfoReplicationIntervalSeconds = 30;
//最初复制实例信息到eureka服务器所需的时间(s),默认为40秒
private int initialInstanceInfoReplicationIntervalSeconds = 40;
//询问Eureka服务url信息变化的时间间隔(s),默认为300秒
private int eurekaServiceUrlPollIntervalSeconds = 5 * MINUTES;
//获取eureka服务的代理主机,默认为null
private String proxyPort;
//获取eureka服务的代理端口, 默认为null
private String proxyHost;
//获取eureka服务的代理用户名,默认为null
private String proxyUserName;
//获取eureka服务的代理密码,默认为null
private String proxyPassword;
//eureka需要超时读取之前需要等待的时间,默认为8秒
private int eurekaServerReadTimeoutSeconds = 8;
//eureka需要超时连接之前需要等待的时间,默认为5秒
private int eurekaServerConnectTimeoutSeconds = 5;
/获取实现了eureka客户端在第一次启动时读取注册表的信息作为回退选项的实现名称
private String backupRegistryImpl;
//eureka客户端允许所有eureka服务器连接的总数目,默认是200
private int eurekaServerTotalConnections = 200;
//eureka客户端允许eureka服务器主机连接的总数目,默认是50
private int eurekaServerTotalConnectionsPerHost = 50;
//表示eureka注册中心的路径,如果配置为eureka,则为http://x.x.x.x:x/eureka/,

//在eureka的配置文件中加入此配置表示eureka作为客户端向注册中心注册,从而构成eureka集群。
//此配置只有在eureka服务器ip地址列表是在DNS中才会用到,默认为null
private String eurekaServerURLContext;
//获取eureka服务器的端口,此配置只有在eureka服务器ip地址列表是在DNS中才会用到。默认为null
private String eurekaServerPort;
//获取要查询的DNS名称来获得eureka服务器,此配置只有在eureka服务器ip地址列表是在DNS中才会用到。默认为null
private String eurekaServerDNSName;
//获取实例所在的地区。默认为us-east-1
private String region = “us-east-1”;
//Eureka服务的http请求关闭之前其响应的时间,默认为30 秒
private int eurekaConnectionIdleTimeoutSeconds = 30;
//此客户端只对一个单一的VIP注册表的信息感兴趣。默认为null
private String registryRefreshSingleVipAddress;
//心跳执行程序线程池的大小,默认为2
private int heartbeatExecutorThreadPoolSize = 2;
//心跳执行程序回退相关的属性,是重试延迟的最大倍数值,默认为10
private int heartbeatExecutorExponentialBackOffBound = 10;
//执行程序缓存刷新线程池的大小,默认为2
private int cacheRefreshExecutorThreadPoolSize = 2;
//执行程序指数回退刷新的相关属性,是重试延迟的最大倍数值,默认为10
private int cacheRefreshExecutorExponentialBackOffBound = 10;
// 可用的服务全限定URl集合
private Map<String, String> serviceUrl = new HashMap<>();
{
this.serviceUrl.put(DEFAULT_ZONE, DEFAULT_URL);
}
//eureka注册表的内容是否被压缩,默认为true,并且是在最好的网络流量下被压缩
private boolean gZipContent = true;
//eureka客户端是否应该使用DNS机制来获取eureka服务器的地址列表,默认为false
private boolean useDnsForFetchingServiceUrls = false;
//实例是否在eureka服务器上注册自己的信息以供其他服务发现,默认为true
private boolean registerWithEureka = true;
//实例是否使用同一zone里的eureka服务器,默认为true,理想状态下,eureka客户端与服务端是在同一zone下
private boolean preferSameZoneEureka = true;
//是否记录eureka服务器和客户端之间在注册表的信息方面的差异,默认为false
private boolean logDeltaDiff;
//指示eureka客户机是否应该禁用对delta的抓取,而应该求助于获取完整的注册表信息。
//注意,delta获取可以极大地减少流量,因为eureka服务器的更改速度通常比获取速度低得多。
//更改在运行时的下一个注册表获取周期中有效,该周期由registryFetchIntervalSeconds指定
private boolean disableDelta;
//eureka服务注册表信息里的以逗号隔开的地区名单,如果不这样返回这些地区名单,则客户端启动将会出错。默认为null
private String fetchRemoteRegionsRegistry;
//获取实例所在的地区下可用性的区域列表,用逗号隔开。
private Map<String, String> availabilityZones = new HashMap<>();
//是否获得处于开启状态的实例的应用程序过滤之后的应用程序。默认为true
private boolean filterOnlyUpInstances = true;
//此客户端是否获取eureka服务器注册表上的注册信息,默认为true
private boolean fetchRegistry = true;
//eureka服务器序列化/反序列化的信息中获取“$”符号的的替换字符串。默认为“-”
private String dollarReplacement = "
-";
//eureka服务器序列化/反序列化的信息中获取“_”符号的的替换字符串。默认为“
private String escapeCharReplacement = "
";
//指示服务器是否可以将客户机请求重定向到备份服务器/集群。
//如果设置为false,服务器将直接处理请求,
//如果设置为true,它可能向客户机发送HTTP重定向,并提供一个新的服务器位置。
private boolean allowRedirects = false;
//如果设置为true,客户端的状态更新将会点播更新到远程服务器上,默认为true
private boolean onDemandUpdateStatusChange = true;
//这是一个短暂的编码器的配置,如果最新的编码器是稳定的,则可以去除,默认为null
private String encoderName;
//获取编码器名称
private String decoderName;
//客户端数据接收
private String clientDataAccept = EurekaAccept.full.name();
//指示客户端关闭时是否应显式地从远程服务器注销自己。
private boolean shouldUnregisterOnShutdown = true;
//指示客户端是否应在初始化期间强制注册。默认为false
private boolean shouldEnforceRegistrationAtInit = false;
//“CompositeDiscoveryClient”用于对可用客户机排序的发现客户机的顺序。
private int order = 0;
…省略get/set方法
}
2.2.1 HasFeatures

该ben主要提供了一些系统相关的特性,可以通过下面的配置开启:

management.endpoints.web.exposure.include=features
然后在浏览器中通过:http://localhost:7001/actuator/features 进行访问

// EurekaClient自带的HasFeatures
@Bean
public HasFeatures eurekaFeature() {
log.info("== 04 EurekaClientAutoConfiguration HasFeatures init …");
return HasFeatures.namedFeature(“Eureka Client”, EurekaClient.class);
}
当然我们也可以自定义HasFeatures:

// 自定义HasFeatures
@Bean(name = “myHasFeatures”)
public HasFeatures eurekaFeature1() {
log.info("== 04 EurekaClientAutoConfiguration HasFeatures init …");
return HasFeatures.namedFeature(“Eureka Client Config”, EurekaClientConfigBean.class);
}
访问上面的链接,可以看到:

{
enabled: [
{
type: “com.netflix.discovery.EurekaClient”,
name: “Eureka Client”,
version: “1.9.9-SNAPSHOT”,
vendor: null
},
{
type: “org.springframework.cloud.netflix.eureka.EurekaClientConfigBean”,
name: “Eureka Client Config”,
version: null,
vendor: null
},
{
type: “org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient”,
name: “DiscoveryClient”,
version: “2.1.1.RELEASE”,
vendor: “Pivotal Software, Inc.”
},
{
type: “org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient”,
name: “LoadBalancerClient”,
version: null,
vendor: null
},
{
type: “com.netflix.ribbon.Ribbon”,
name: “Ribbon”,
version: “2.3.0”,
vendor: null
}
],
disabled: [ ]
}
2.2.5 EurekaServiceRegistry

该类实现了ServiceRegistry接口,主要用来注册、注销、设置和获取注册实例的状态。该Bean非常重要哦,关于其具体代码会在后面分析EurekaClient的时候分析。

@Bean
public EurekaServiceRegistry eurekaServiceRegistry() {
log.info("== 05 EurekaClientAutoConfiguration EurekaServiceRegistry init …");
return new EurekaServiceRegistry();
}
2.2.6 EurekaAutoServiceRegistration

该类主要用来实现服务的自动注册,关于其具体代码后面会详细分析。

@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = “spring.cloud.service-registry.auto-registration.enabled”, matchIfMissing = true)
public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(
ApplicationContext context,
EurekaServiceRegistry registry,
EurekaRegistration registration) {
log.info("== 06 EurekaClientAutoConfiguration EurekaAutoServiceRegistration init …");
return new EurekaAutoServiceRegistration(context, registry, registration);
}
2.2.7 DiscoveryClient

该bean主要提供了对EurekaClient实例信息的一些读取功能

@Bean
public DiscoveryClient discoveryClient(EurekaClient client, EurekaClientConfig clientConfig) {
log.info("== 07 EurekaClientAutoConfiguration DiscoveryClient init …");
return new EurekaDiscoveryClient(client, clientConfig);
}
2.3 EurekaClientAutoConfiguration 静态内部类相关bean

静态内部类的bean启动顺序如下:

== static class 01 RefreshableEurekaClientConfiguration EurekaRegistration init …
== static class 02 RefreshableEurekaClientConfiguration ApplicationInfoManager init …
== static class 03 RefreshableEurekaClientConfiguration CloudEurekaClient init …
2.3.1 EurekaRegistration

该类封装了自动注册所需的相关信息,如CloudEurekaInstanceConfig、EurekaClient、ApplicationInfoManager等信息

@Bean
@org.springframework.cloud.context.config.annotation.RefreshScope
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = “spring.cloud.service-registry.auto-registration.enabled”, matchIfMissing = true)
public EurekaRegistration eurekaRegistration(EurekaClient eurekaClient,
CloudEurekaInstanceConfig instanceConfig,
ApplicationInfoManager applicationInfoManager,
@Autowired(required = false) ObjectProvider healthCheckHandler) {

log.info("== static class 01 RefreshableEurekaClientConfiguration EurekaRegistration init ...");

return EurekaRegistration
        .builder(instanceConfig)
        .with(applicationInfoManager)
        .with(eurekaClient)
        .with(healthCheckHandler)
        .build();

}
2.3.2 ApplicationInfoManager

注意:该Bean是懒加载
该类封装了EurekaInstanceConfig、InstanceInfo等信息,并提供了对EurekaInstance的管理功能。

@Bean
@ConditionalOnMissingBean(value = ApplicationInfoManager.class, search = SearchStrategy.CURRENT)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public ApplicationInfoManager eurekaApplicationInfoManager(EurekaInstanceConfig config) {
log.info("== static class 02 RefreshableEurekaClientConfiguration ApplicationInfoManager init …");
InstanceInfo instanceInfo = new InstanceInfoFactory().create(config);
return new ApplicationInfoManager(config, instanceInfo);
}
InstanceInfo封装的信息如下:

public InstanceInfo create(EurekaInstanceConfig config) {
LeaseInfo.Builder leaseInfoBuilder = LeaseInfo.Builder.newBuilder()
.setRenewalIntervalInSecs(config.getLeaseRenewalIntervalInSeconds())
.setDurationInSecs(config.getLeaseExpirationDurationInSeconds());
// 构建要在eureka服务器上注册的实例信息
// Builder the instance information to be registered with eureka server
InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder();
String namespace = config.getNamespace();
if (!namespace.endsWith(".")) {
namespace = namespace + “.”;
}
builder.setNamespace(namespace).setAppName(config.getAppname())
.setInstanceId(config.getInstanceId())
.setAppGroupName(config.getAppGroupName())
.setDataCenterInfo(config.getDataCenterInfo())
.setIPAddr(config.getIpAddress()).setHostName(config.getHostName(false))
.setPort(config.getNonSecurePort())
.enablePort(InstanceInfo.PortType.UNSECURE,config.isNonSecurePortEnabled())
.setSecurePort(config.getSecurePort())
.enablePort(InstanceInfo.PortType.SECURE, config.getSecurePortEnabled())
.setVIPAddress(config.getVirtualHostName())
.setSecureVIPAddress(config.getSecureVirtualHostName())
.setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl())
.setStatusPageUrl(config.getStatusPageUrlPath(),config.getStatusPageUrl())
.setHealthCheckUrls(config.getHealthCheckUrlPath(),config.getHealthCheckUrl(), config.getSecureHealthCheckUr
.setASGName(config.getASGName());
// Start off with the STARTING state to avoid traffic
// 实例注册到Eureka上是否立刻开启通讯。有时候应用在准备好服务之前需要一些预处理。
// 如果设置了不立刻开启通讯,则将当前服务实例状态设置为STARTING
if (!config.isInstanceEnabledOnit()) {
InstanceInfo.InstanceStatus initialStatus = InstanceInfo.InstanceStatus.STARTING;
if (log.isInfoEnabled()) {
log.info("Setting initial instance status as: " + initialStatus);
}
builder.setStatus(initialStatus);
}
else {
if (log.isInfoEnabled()) {
log.info("Setting initial instance status as: "
+ InstanceInfo.InstanceStatus.UP
+ ". This may be too early for the instance to advertise itself as available. "
+ “You would instead want to control this via a healthcheck handler.”);
}
}
// Add any user-specific metadata information
// 添加用户指定的元数据信息
for (Map.Entry<String, String> mapEntry : config.getMetadataMap().entrySet()) {
String key = mapEntry.getKey();
String value = mapEntry.getValue();
// only add the metadata if the value is present
if (value != null && !value.isEmpty()) {
builder.add(key, value);
}
}
InstanceInfo instanceInfo = builder.build();
instanceInfo.setLeaseInfo(leaseInfoBuilder.build());
return instanceInfo;
}
2.3.3 EurekaClient

注意:该Bean是懒加载

// init third
@Bean(destroyMethod = “shutdown”)
@ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public EurekaClient eurekaClient(
ApplicationInfoManager manager,
EurekaClientConfig config,
EurekaInstanceConfig instance,
@Autowired(required = false) HealthCheckHandler healthCheckHandler) {
log.info("== static class 03 RefreshableEurekaClientConfiguration CloudEurekaClient init …");

/**
 * 如果我们使用ApplicationInfoManager的代理,那么当在CloudEurekaClient上调用shutdown时,
 * 我们可能会遇到一个问题,因为在CloudEurekaClient上请求了ApplicationInfoManager bean,但是不允许,
 * 因为我们正在关闭它。为了避免这种情况,我们直接使用对象。
 *
 * If we use the proxy of the ApplicationInfoManager we could run into a problem
 * when shutdown is called on the CloudEurekaClient where the ApplicationInfoManager bean is
 * requested but wont be allowed because we are shutting down. To avoid this we use the object directly.
 */
ApplicationInfoManager appManager;
if (AopUtils.isAopProxy(manager)) {
    appManager = ProxyUtils.getTargetObject(manager);
}
else {
    appManager = manager;
}
// 实例化CloudEurekaClient
CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager,config,this.optionalArgs,this.context);
cloudEurekaClient.registerHealthCheck(healthCheckHandler);
return cloudEurekaClient;

}
3. EurekaClient 实例化的时机

前文提到了两个懒加载的Bean,ApplicationInfoManager和EurekaClient,根据@Lazy注解的定义,第一次加载对应的bean时会将其实例化,那么什么时候会第一次用到该bean的实例呢?别忘了我们前文介绍的两个Bean,EurekaAutoServiceRegistration和EurekaServiceRegistry(当然,如果实在无法联想到这两个类,可以通过启动日志定位):

EurekaAutoServiceRegistration --> 提供服务自动注册功能
该类实现了Lifecycle接口,并重写了start方法,通过WebServerInitializedEvent事件触发该方法,并调用this.serviceRegistry.register(this.registration);注册服务。
/**

  • 通过WebServerInitializedEvent事件触发
    */
    @Override
    public void start() {
    // only set the port if the nonSecurePort or securePort is 0 and this.port != 0
    if (this.port.get() != 0) {
    // 如果未配置nonSecurePort,则使用server.port作为其端口
    if (this.registration.getNonSecurePort() == 0) {
    this.registration.setNonSecurePort(this.port.get());
    }

     // 如果未配置securePort,但是配置了securePortEnabled,则使用server.port作为其端口
     if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {
         this.registration.setSecurePort(this.port.get());
     }
    

    }

    // 如果running标志位未开启且nonSecurePort端口号>0
    // only initialize if nonSecurePort is greater than 0 and it isn’t already running
    // because of containerPortInitializer below
    if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
    // 注册服务
    this.serviceRegistry.register(this.registration);
    // 发布事件
    this.context.publishEvent(new InstanceRegisteredEvent<>(this,this.registration.getInstanceConfig()));
    // 将running标志位设置为true
    this.running.set(true);
    }
    }
    EurekaServiceRegistry --> 提供了服务注册、注销、关闭、设置服务状态、获取服务状态等功能
    /**

  • 注册服务。注册通常包含关于实例的信息,比如它的主机名和端口。

  • 通过EurekaAutoServiceRegistration类的start方法调用,而start方法由WebServerInitializedEvent事件触发
    /
    @Override
    public void register(EurekaRegistration reg) {
    /
    *

    • 初始化相关组件如,ApplicationInfoManager、EurekaClient
      */
      maybeInitializeClient(reg);

    if (log.isInfoEnabled()) {
    log.info("Registering application "
    + reg.getApplicationInfoManager().getInfo().getAppName()
    + " with eureka with status "
    + reg.getInstanceConfig().getInitialStatus());
    }

    reg.getApplicationInfoManager().setInstanceStatus(reg.getInstanceConfig().getInitialStatus());

    reg.getHealthCheckHandler().ifAvailable(healthCheckHandler -> reg
    .getEurekaClient().registerHealthCheck(healthCheckHandler));
    }

/**

  • 强制初始化可能作用域的代理
  • @param reg
    */
    private void maybeInitializeClient(EurekaRegistration reg) {
    // force initialization of possibly scoped proxies
    reg.getApplicationInfoManager().getInfo();
    reg.getEurekaClient().getApplications();
    }
    其中maybeInitializeClient就是初始化这两个Bean的入口。接下来分析EurekaClient的实例化过程:

// 接上文,实例化入口
CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager,config,this.optionalArgs,this.context);

// 实例化CloudEurekaClient
public CloudEurekaClient(ApplicationInfoManager applicationInfoManager,
EurekaClientConfig config,
AbstractDiscoveryClientOptionalArgs<?> args,
ApplicationEventPublisher publisher) {
super(applicationInfoManager, config, args);
this.applicationInfoManager = applicationInfoManager;
this.publisher = publisher;
this.eurekaTransportField = ReflectionUtils.findField(DiscoveryClient.class, “eurekaTransport”);
ReflectionUtils.makeAccessible(this.eurekaTransportField);
}

// 调用父类构造方法
public DiscoveryClient(ApplicationInfoManager applicationInfoManager, final EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args) {
this(applicationInfoManager, config, args, new Provider() {
private volatile BackupRegistry backupRegistryInstance;

    @Override
    public synchronized BackupRegistry get() {
        if (backupRegistryInstance == null) {
            String backupRegistryClassName = config.getBackupRegistryImpl();
            if (null != backupRegistryClassName) {
                try {
                    backupRegistryInstance = (BackupRegistry) Class.forName(backupRegistryClassName).newInstance();
                    logger.info("Enabled backup registry of type {}", backupRegistryInstance.getClass());
                } catch (InstantiationException e) {
                    logger.error("Error instantiating BackupRegistry.", e);
                } catch (IllegalAccessException e) {
                    logger.error("Error instantiating BackupRegistry.", e);
                } catch (ClassNotFoundException e) {
                    logger.error("Error instantiating BackupRegistry.", e);
                }
            }

            if (backupRegistryInstance == null) {
                logger.warn("Using default backup registry implementation which does not do anything.");
                backupRegistryInstance = new NotImplementedRegistryImpl();
            }
        }

        return backupRegistryInstance;
    }
});

}

//再次调用构造方法
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager,
EurekaClientConfig config,
AbstractDiscoveryClientOptionalArgs args,
Provider backupRegistryProvider)
{
if (args != null) {
this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
this.eventListeners.addAll(args.getEventListeners());
this.preRegistrationHandler = args.preRegistrationHandler;
} else {
this.healthCheckCallbackProvider = null;
this.healthCheckHandlerProvider = null;
this.preRegistrationHandler = null;
}

this.applicationInfoManager = applicationInfoManager;
InstanceInfo myInfo = applicationInfoManager.getInfo();

clientConfig = config;
staticClientConfig = clientConfig;
transportConfig = config.getTransportConfig();
instanceInfo = myInfo;
if (myInfo != null) {
    appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
} else {
    logger.warn("Setting instanceInfo to a passed in null value");
}

this.backupRegistryProvider = backupRegistryProvider;

this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);
localRegionApps.set(new Applications());

fetchRegistryGeneration = new AtomicLong(0);

remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));

// 此客户端是否获取eureka服务器注册表上的注册信息,默认为true
if (config.shouldFetchRegistry()) {
    this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
    this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}

// 实例是否在eureka服务器上注册自己的信息以供其他服务发现,默认为true
if (config.shouldRegisterWithEureka()) {
    this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
    this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}

logger.info("Initializing Eureka in region {}", clientConfig.getRegion());


/**
 * 即不需要注册Eureka也无需获取已注册Eureka信息
 */
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();
    logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",initTimestampMs, this.getApplications().size());
    return;  // no need to setup up an network tasks and we are done
}

try {
    // default size of 2 - 1 each for heartbeat and cacheRefresh
    // 初始化线程池相关
    // 创建schedulerService
    scheduler = Executors.newScheduledThreadPool(2,
            new ThreadFactoryBuilder()
                    .setNameFormat("DiscoveryClient-%d")
                    .setDaemon(true)
                    .build());

    // 创建心跳Executor
    heartbeatExecutor = new ThreadPoolExecutor(
            1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(),
            new ThreadFactoryBuilder()
                    .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
                    .setDaemon(true)
                    .build()
    );  // use direct handoff

    // 创建缓存刷新Executor
    cacheRefreshExecutor = new ThreadPoolExecutor(
            1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(),
            new ThreadFactoryBuilder()
                    .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                    .setDaemon(true)
                    .build()
    );  // use direct handoff

    // 初始化Eureka网络通信相关
    eurekaTransport = new EurekaTransport();
    scheduleServerEndpointTask(eurekaTransport, args);

    AzToRegionMapper azToRegionMapper;
    // eureka客户端是否应该使用DNS机制来获取eureka服务器的地址列表,默认为false
    if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {
        azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);
    } else {
        azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);
    }
    if (null != remoteRegionsToFetch.get()) {
        azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(","));
    }
    instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion());
} catch (Throwable e) {
    throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
}

// 从Eureka注册中心拉取注册信息
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
    fetchRegistryFromBackup();
}

// call and execute the pre registration handler before all background tasks (inc registration) is started
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);
    }
}

// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
// 初始化Scheduled任务
initScheduledTasks();

// 注册监控
try {
    Monitors.registerObject(this);
} catch (Throwable e) {
    logger.warn("Cannot register timers", e);
}

// 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();
logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",initTimestampMs, this.getApplications().size());

}
该方法涉及到的知识点较多,下面针对这里面比较重要的知识点逐个进行分析。

3.1 EurekaClient 获取已注册的服务器信息

在上一步Eureka实例化的过程中有这样一段代码:

// 获取已注册的服务器信息
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
    fetchRegistryFromBackup();
}

其中的fetchRegistry(false)功能即为获取已经注册的服务器信息。看似简短的一句代码,其内部执行的代码是很复杂的。

private boolean fetchRegistry(boolean forceFullRegistryFetch) {
    Stopwatch tracer = FETCH_REGISTRY_TIMER.start();

    try {
        // If the delta is disabled or if it is the first time, get all applications
        Applications applications = getApplications();

        // 全量获取注册信息
        if (clientConfig.shouldDisableDelta()
                || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
                || forceFullRegistryFetch
                || (applications == null)
                || (applications.getRegisteredApplications().size() == 0)
                || (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
        {
            logger.info("Disable delta property : {}", clientConfig.shouldDisableDelta());
            logger.info("Single vip registry refresh property : {}", clientConfig.getRegistryRefreshSingleVipAddress());
            logger.info("Force full registry fetch : {}", forceFullRegistryFetch);
            logger.info("Application is null : {}", (applications == null));
            logger.info("Registered Applications size is zero : {}",(applications.getRegisteredApplications().size() == 0));
            logger.info("Application version is -1: {}", (applications.getVersion() == -1));
            // 获取并将注册信息存储到本地
            getAndStoreFullRegistry();
        }
        // 增量获取注册信息
        else {
            getAndUpdateDelta(applications);
        }
        applications.setAppsHashCode(applications.getReconcileHashCode());
        logTotalInstances();
    } catch (Throwable e) {
        logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e);
        return false;
    } finally {
        if (tracer != null) {
            tracer.stop();
        }
    }

    // Notify about cache refresh before updating the instance remote status
    // 在更新实例远程状态之前通知缓存刷新
    onCacheRefreshed();

    // Update remote status based on refreshed data held in the cache
    // 根据缓存中刷新的数据更新远程状态
    updateInstanceRemoteStatus();

    // registry was fetched successfully, so return true
    return true;
}

该方法所做的事情如下:

  • 全量或增量获取注册信息,获取注册信息并将注册信息缓存到本地
  • 在更新实例远程状态之前通知缓存刷新
  • 根据缓存中刷新的数据更新远程状态
  • 日志信息打印
  • 返回结果

这里最重要的步骤就是全量获取注册信息

3.1.1 getAndStoreFullRegistry 全量获取注册信息

getAndStoreFullRegistry方法会获取到注册到Eureka注册中心的所有服务器的信息(全量)并缓存到本地,包括Client和Consumer,同时会将自己也注册到Eureka注册中心。

private void getAndStoreFullRegistry() throws Throwable {
    long currentUpdateGeneration = fetchRegistryGeneration.get();

    logger.info("Getting all instance registry info from the eureka server");

    // 注册的服务器信息(包含Client和Consumer)
    Applications apps = null;
    // 获取并注册服务器
    EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
            ? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
            : eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());

    // 如果与Eureka注册中心成功通信
    if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
        apps = httpResponse.getEntity();
    }
    logger.info("The response status is {}", httpResponse.getStatusCode());

    if (apps == null) {
        logger.error("The application is null for some reason. Not storing this information");
    }
    else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
        // 将获取到的注册服务器信息,对其进行过滤,值保留状态为'UP'的服务器信息,打乱其排序,并存储到本地变量中
        localRegionApps.set(this.filterAndShuffle(apps));
        logger.debug("Got full registry with apps hashcode {}", apps.getAppsHashCode());
    } else {
        logger.warn("Not updating applications as another thread is updating it already");
    }
}

这段代码的核心是eurekaTransport.queryClient.getApplications(remoteRegionsRef.get()),获取服务器信息。而且接下来的代码执行过程比较绕。这段代码的方法调用栈梳理如下:

-- EurekaHttpClientDecorator.getApplications()
  -- SessionedEurekaHttpClient.execute()
    -- RetryableEurekaHttpClient.execute()
      -- RedirectingEurekaHttpClient.execute()
        -- MetricsCollectingEurekaHttpClient.execute()
          -- AbstractJerseyEurekaHttpClient.getApplications()
          
  • SessionedEurekaHttpClient.java
// SessionedEurekaHttpClient.java
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
    // 超过 当前会话时间,关闭当前委托的 EurekaHttpClient 。
    long now = System.currentTimeMillis();
    long delay = now - lastReconnectTimeStamp;
    if (delay >= currentSessionDurationMs) {
        logger.debug("Ending a session and starting anew");
        lastReconnectTimeStamp = now;
        currentSessionDurationMs = randomizeSessionDuration(sessionDurationMs);
        TransportUtils.shutdown(eurekaHttpClientRef.getAndSet(null));
    }

    // 获得委托的 EurekaHttpClient 。若不存在,则创建新的委托的 EurekaHttpClient 。
    EurekaHttpClient eurekaHttpClient = eurekaHttpClientRef.get();
    if (eurekaHttpClient == null) {
        eurekaHttpClient = TransportUtils.getOrSetAnotherClient(eurekaHttpClientRef, clientFactory.newClient());
    }

    return requestExecutor.execute(eurekaHttpClient);
}
  • RetryableEurekaHttpClient.java
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
    // 定义候选主机
    List<EurekaEndpoint> candidateHosts = null;
    int endpointIdx = 0;
    // numberOfRetries 失败重连尝试次数,默认为3次
    for (int retry = 0; retry < numberOfRetries; retry++) {
        EurekaHttpClient currentHttpClient = delegate.get();
        EurekaEndpoint currentEndpoint = null;
        if (currentHttpClient == null) {
            if (candidateHosts == null) {
                // 获取候选主机
                candidateHosts = getHostCandidates();
                if (candidateHosts.isEmpty()) {
                    throw new TransportException("There is no known eureka server; cluster server list is empty");
                }
            }
            if (endpointIdx >= candidateHosts.size()) {
                throw new TransportException("Cannot execute request on any known server");
            }

            currentEndpoint = candidateHosts.get(endpointIdx++);
            currentHttpClient = clientFactory.newClient(currentEndpoint);
        }

        try {
            // 执行请求
            EurekaHttpResponse<R> response = requestExecutor.execute(currentHttpClient);
            // 判断是否为可接受的相应,若是,返回。
            if (serverStatusEvaluator.accept(response.getStatusCode(), requestExecutor.getRequestType())) {
                delegate.set(currentHttpClient);
                if (retry > 0) {
                    logger.info("Request execution succeeded on retry #{}", retry);
                }
                return response;
            }
            logger.warn("Request execution failure with status code {}; retrying on another server if available", response.getStatusCode());
        } catch (Exception e) {
            logger.warn("Request execution failed with message: {}", e.getMessage());  // just log message as the underlying client should log the stacktrace
        }

        // Connection error or 5xx from the server that must be retried on another server
        // 请求失败,若是 currentHttpClient ,清除 delegate
        delegate.compareAndSet(currentHttpClient, null);

        // 请求失败,将 currentEndpoint 添加到隔离集合
        if (currentEndpoint != null) {
            quarantineSet.add(currentEndpoint);
        }
    }
    throw new TransportException("Retry limit reached; giving up on completing the request");
}
  • RedirectingEurekaHttpClient.java
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
    EurekaHttpClient currentEurekaClient = delegateRef.get();
    // 未找到非 302 的 Eureka-Server
    if (currentEurekaClient == null) {
        AtomicReference<EurekaHttpClient> currentEurekaClientRef = new AtomicReference<>(factory.newClient(serviceEndpoint));
        try {
            EurekaHttpResponse<R> response = executeOnNewServer(requestExecutor, currentEurekaClientRef);
            // 关闭原有的委托 EurekaHttpClient ,并设置当前成功非 302 请求的 EurekaHttpClient
            TransportUtils.shutdown(delegateRef.getAndSet(currentEurekaClientRef.get()));
            return response;
        } catch (Exception e) {
            logger.error("Request execution error. endpoint={}", serviceEndpoint, e);
            TransportUtils.shutdown(currentEurekaClientRef.get());
            throw e;
        }
    }
    // 已经找到非 302 的 Eureka-Server
    else {
        try {
            return requestExecutor.execute(currentEurekaClient);
        } catch (Exception e) {
            logger.error("Request execution error. endpoint={}", serviceEndpoint, e);
            delegateRef.compareAndSet(currentEurekaClient, null);
            currentEurekaClient.shutdown();
            throw e;
        }
    }
}
  • MetricsCollectingEurekaHttpClient.java
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
    EurekaHttpClientRequestMetrics requestMetrics = metricsByRequestType.get(requestExecutor.getRequestType());
    Stopwatch stopwatch = requestMetrics.latencyTimer.start();
    try {
        EurekaHttpResponse<R> httpResponse = requestExecutor.execute(delegate);
        requestMetrics.countersByStatus.get(mappedStatus(httpResponse)).increment();
        return httpResponse;
    } catch (Exception e) {
        requestMetrics.connectionErrors.increment();
        exceptionsMetric.count(e);
        throw e;
    } finally {
        stopwatch.stop();
    }
}
@Override
public EurekaHttpResponse<Applications> getApplications(String... regions) {
    return getApplicationsInternal("apps/", regions);
}
    
private EurekaHttpResponse<Applications> getApplicationsInternal(String urlPath, String[] regions) {
    ClientResponse response = null;
    String regionsParamValue = null;
    try {
        // 通过Jersey-client执行请求
        WebResource webResource = jerseyClient.resource(serviceUrl).path(urlPath);
        if (regions != null && regions.length > 0) {
            regionsParamValue = StringUtil.join(regions);
            webResource = webResource.queryParam("regions", regionsParamValue);
        }
        Builder requestBuilder = webResource.getRequestBuilder();
        addExtraHeaders(requestBuilder);
        response = requestBuilder.accept(MediaType.APPLICATION_JSON_TYPE).get(ClientResponse.class);

        // 封装返回的服务器信息
        Applications applications = null;
        if (response.getStatus() == Status.OK.getStatusCode() && response.hasEntity()) {
            applications = response.getEntity(Applications.class);
        }
        return anEurekaHttpResponse(response.getStatus(), Applications.class)
                .headers(headersOf(response))
                .entity(applications)
                .build();
    } finally {
        if (logger.isDebugEnabled()) {
            logger.debug("Jersey HTTP GET {}/{}?{}; statusCode={}",
                    serviceUrl, urlPath,
                    regionsParamValue == null ? "" : "regions=" + regionsParamValue,
                    response == null ? "N/A" : response.getStatus()
            );
        }
        if (response != null) {
            response.close();
        }
    }
}

到这里EurekaClient的初始化分析基本可以告一段落,但是上面的代码少了很关键的一个步骤,EurekaClient是如何注册到Eureka注册中心的?这个问题我们留在下面的章节分析。

4 EurekaClient 定时任务线程初始化

前面我们分析了EurekaClient的初始化过程,并留了一个问题,EurekaClient是如何注册到Eureka注册中心的?答案就在EurekaClient的线程定时任务中。接前面的分析,打开DiscoveryClient类,找到initScheduledTasks方法:

/**
 * 初始化所有计划的任务。
 * Initializes all scheduled tasks.
 */
private void initScheduledTasks() {

    /**
     * 1、按指定间隔提取注册表信息的任务。
     */
    // 此客户端是否获取eureka服务器注册表上的注册信息,默认为true
    if (clientConfig.shouldFetchRegistry()) {
        // registry cache refresh timer
        // 注册表缓存刷新定时器

        // 从eureka服务器注册表中获取注册信息的时间间隔(s),默认为30秒
        int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
        // 执行程序指数回退刷新的相关属性,是重试延迟的最大倍数值,默认为10
        int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
        scheduler.schedule(
                new TimedSupervisorTask(
                        "cacheRefresh",
                        scheduler,
                        cacheRefreshExecutor,
                        registryFetchIntervalSeconds,
                        TimeUnit.SECONDS,
                        expBackOffBound,
                        // 创建拉取注册信息线程
                        new CacheRefreshThread()
                ),
                registryFetchIntervalSeconds, TimeUnit.SECONDS);
    }

    /**
     * 2、在给定间隔内更新租约的心跳任务。
     */
    // 实例是否在eureka服务器上注册自己的信息以供其他服务发现,默认为true
    if (clientConfig.shouldRegisterWithEureka()) {
        // 心跳定时任务间隔 默认 30秒
        int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
        // 心跳最大超时倍数,即 心跳最大超时时间 = renewalIntervalInSecs*expBackOffBound,默认为10
        int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
        logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);

        // Heartbeat timer
        // 创建心跳任务定任务
        scheduler.schedule(
                new TimedSupervisorTask(
                        "heartbeat",
                        scheduler,
                        heartbeatExecutor,
                        renewalIntervalInSecs, // 超时时间
                        TimeUnit.SECONDS,
                        expBackOffBound,       // 心跳最大超时倍数
                        new HeartbeatThread()  // 创建心跳任务线程
                ),
                renewalIntervalInSecs, TimeUnit.SECONDS);

        // InstanceInfo replicator
        // 创建更新和复制本地instanceinfo到远程服务器的任务
        instanceInfoReplicator = new InstanceInfoReplicator(
                this,
                instanceInfo,
                // 复制实例变化信息到eureka服务器所需要的时间间隔(s),默认为30秒
                clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                2); // burstSize


        // 创建应用实例状态变更监听器
        statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
            @Override
            public String getId() {
                return "statusChangeListener";
            }

            @Override
            public void notify(StatusChangeEvent statusChangeEvent) {
                if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
                        InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
                    // log at warn level if DOWN was involved
                    logger.warn("Saw local status change event {}", statusChangeEvent);
                } else {
                    logger.info("Saw local status change event {}", statusChangeEvent);
                }
                instanceInfoReplicator.onDemandUpdate();
            }
        };

        // 注册应用实例状态变更监听器
        if (clientConfig.shouldOnDemandUpdateStatusChange()) {
            applicationInfoManager.registerStatusChangeListener(statusChangeListener);
        }
        // 开启应用实例信息复制器
        instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    } else {
        logger.info("Not registering with Eureka server per configuration");
    }
}

这里一共初始化了三个定时任务:

  • 按指定间隔提取注册表信息的任务
  • 在给定间隔内更新租约的心跳任务
  • 创建更新和复制本地instanceinfo到远程服务器的任务
  • 创建应用实例状态变更监听器

下面对在给定间隔内更新租约的心跳任务和创建更新、复制本地instanceinfo到远程服务器、应用实例状态变更监听器做下分析:

4.1 在给定间隔内更新租约的心跳任务

续约心跳参数:

  • renewalIntervalInSecs --> 心跳定时任务间隔 默认 30秒
  • expBackOffBound --> 心跳最大超时倍数,即 心跳最大超时时间 = renewalIntervalInSecs*expBackOffBound,默认为10

续约任务和线程:

  • TimedSupervisorTask --> 心跳任务
  • HeartbeatThread --> 心跳执行线程
4.1.1 TimedSupervisorTask

TimedSupervisorTask的构造函数如下,都是一些赋值操作,这里有一个最大超时时间的设置:maxDelay = timeoutMillis * expBackOffBound,即前文我们提到的expBackOffBound。

public TimedSupervisorTask(String name, ScheduledExecutorService scheduler, ThreadPoolExecutor executor,
                           int timeout, TimeUnit timeUnit, int expBackOffBound, Runnable task) {
    this.scheduler = scheduler;
    this.executor = executor;
    this.timeoutMillis = timeUnit.toMillis(timeout);
    this.task = task;
    this.delay = new AtomicLong(timeoutMillis);
    // 计算最大超时时间
    this.maxDelay = timeoutMillis * expBackOffBound;

    // Initialize the counters and register.
    successCounter = Monitors.newCounter("success");
    timeoutCounter = Monitors.newCounter("timeouts");
    rejectedCounter = Monitors.newCounter("rejectedExecutions");
    throwableCounter = Monitors.newCounter("throwables");
    threadPoolLevelGauge = new LongGauge(MonitorConfig.builder("threadPoolUsed").build());
    Monitors.registerObject(name, this);
}

该类继承了TimerTask,所以继续看其run方法:

public void run() {
    Future<?> future = null;
    try {
        // 提交任务
        future = executor.submit(task);
        threadPoolLevelGauge.set((long) executor.getActiveCount());
        // 获取任务结果,这里将阻塞,直至任务完成或者超时
        future.get(timeoutMillis, TimeUnit.MILLISECONDS);  // block until done or timeout
        delay.set(timeoutMillis);
        threadPoolLevelGauge.set((long) executor.getActiveCount());
        successCounter.increment();
    } catch (TimeoutException e) {
        logger.warn("task supervisor timed out", e);
        timeoutCounter.increment();

        /**
         * 超时请求阶梯重试:
         * 当发生超时异常时,需要重新计算下一次的请求超时时间,计算方法为
         * time1 = currentDelay * 2
         * time2 = timeoutMillis * expBackOffBound
         *
         * time1与time2取两者最小值作为下一次请求的超时时间
         */
        long currentDelay = delay.get();
        long newDelay = Math.min(maxDelay, currentDelay * 2);
        delay.compareAndSet(currentDelay, newDelay);
    } catch (RejectedExecutionException e) {
        if (executor.isShutdown() || scheduler.isShutdown()) {
            logger.warn("task supervisor shutting down, reject the task", e);
        } else {
            logger.warn("task supervisor rejected the task", e);
        }

        rejectedCounter.increment();
    } catch (Throwable e) {
        if (executor.isShutdown() || scheduler.isShutdown()) {
            logger.warn("task supervisor shutting down, can't accept the task");
        } else {
            logger.warn("task supervisor threw an exception", e);
        }

        throwableCounter.increment();
    } finally {
        if (future != null) {
            future.cancel(true);
        }

        if (!scheduler.isShutdown()) {
            // delay.get() --> 设置下一次请求的超时时间
            scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
        }
    }
}

关于这里的Future等不多介绍,这里有一个值得我们借鉴的点,就是超时阶梯重试机制:
当请求发生超时时,下一次请求并不是继续沿用之前的设置的超时时间,而是通过结算得出一个阶梯时间,以减少网络请求。其计算方式已经在注释中写明,不在赘述。

4.1.2 HeartbeatThread
/**
 * 在给定间隔内更新租约的心跳任务。
 * The heartbeat task that renews the lease in the given intervals.
 */
private class HeartbeatThread implements Runnable {
    public void run() {
        if (renew()) {
            lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
        }
    }
}

/**
 * 通过REST请求实现续租
 * Renew with the eureka service by making the appropriate REST call
 */
boolean renew() {
    EurekaHttpResponse<InstanceInfo> httpResponse;
    try {
        // 发送心跳请求
        httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
        logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());
        // 404 Not Found ,则尝试重新执行注册操作
        if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
            REREGISTER_COUNTER.increment();
            logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());
            long timestamp = instanceInfo.setIsDirtyWithTime();
            boolean success = register();
            if (success) {
                instanceInfo.unsetIsDirty(timestamp);
            }
            return success;
        }
        // 返回心跳请求是否成功
        return httpResponse.getStatusCode() == Status.OK.getStatusCode();
    } catch (Throwable e) {
        logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);
        return false;
    }
}

续约任务主要通过两个操作来完成:

  • 正常发送续约心跳
/**
 * 发送心跳请求
 */
@Override
public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {
    String urlPath = "apps/" + appName + '/' + id;
    ClientResponse response = null;

    logger.info("== sendHeartBeat serviceUrl:" +serviceUrl+ " urlPath:" +urlPath);

    try {
        WebResource webResource = jerseyClient.resource(serviceUrl)
                .path(urlPath)
                .queryParam("status", info.getStatus().toString())
                .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());
        if (overriddenStatus != null) {
            webResource = webResource.queryParam("overriddenstatus", overriddenStatus.name());
        }
        Builder requestBuilder = webResource.getRequestBuilder();
        addExtraHeaders(requestBuilder);
        response = requestBuilder.put(ClientResponse.class);
        EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = anEurekaHttpResponse(response.getStatus(), InstanceInfo.class).headers(headersOf(response));
        if (response.hasEntity()) {
            eurekaResponseBuilder.entity(response.getEntity(InstanceInfo.class));
        }
        return eurekaResponseBuilder.build();
    } finally {
        if (logger.isDebugEnabled()) {
            logger.debug("Jersey HTTP PUT {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
        }
        if (response != null) {
            response.close();
        }
    }
}
  • 当请求404时,尝试执行注册
/**
 * 通过REST请求注册Eureka服务
 * Register with the eureka service by making the appropriate REST call.
 */
boolean register() throws Throwable {
    logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
    EurekaHttpResponse<Void> httpResponse;
    try {
        // 执行注册
        httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
    } catch (Exception e) {
        logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
        throw e;
    }
    if (logger.isInfoEnabled()) {
        logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
    }
    return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}

/**
 * 执行注册
 */
@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
    String urlPath = "apps/" + info.getAppName();
    ClientResponse response = null;

    logger.info("== register serviceUrl:" +serviceUrl +" urlPath:" +urlPath);

    try {
        Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
        addExtraHeaders(resourceBuilder);
        response = resourceBuilder
                .header("Accept-Encoding", "gzip")
                .type(MediaType.APPLICATION_JSON_TYPE)
                .accept(MediaType.APPLICATION_JSON)
                .post(ClientResponse.class, info);
        return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
    } finally {
        if (logger.isDebugEnabled()) {
            logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
                    response == null ? "N/A" : response.getStatus());
        }
        if (response != null) {
            response.close();
        }
    }
}
4.2 创建更新和复制本地instanceinfo到远程服务器的任务

该任务会创建InstanceInfoReplicator实例,并通过手动调用start方法开启任务。EurekaClient实例的首次注册,即在该方法中完成相关设置。

public void start(int initialDelayMs) {
    if (started.compareAndSet(false, true)) {
        // 首次注册,设置应用实例数据信息不一致
        instanceInfo.setIsDirty(); // for initial register
        // 提交任务,并设置该任务的 Future
        Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
    }
}

关于instanceInfo.setIsDirty()的作用,看下面的代码:

public void run() {
    try {
        // 刷新应用实例信息
        discoveryClient.refreshInstanceInfo();
        // 判断应用实例信息是否数据不一致
        Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
        if (dirtyTimestamp != null) {
            // 发起注册
            discoveryClient.register();
            // 设置应用实例信息数据一致
            instanceInfo.unsetIsDirty(dirtyTimestamp);
        }
    } catch (Throwable t) {
        logger.warn("There was a problem with the instance info replicator", t);
    } finally {
        Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
    }
}

InstanceInfoReplicator实现了Runnable,在其run方法中做了以下事情:

  • 刷新应用实例信息
  • 判断应用实例信息是否数据不一致
  • 如不一致,则发起注册,并将应用实例信息是否数据设置为一致

当EurekaClient实例化并首次调用该任务时,应用实例信息肯定没有发生变化,那么就有必要手动设置应用实例信息数据不一致,以便完成首次注册。接下来我们再看看refreshDataCenterInfoIfRequired方法都刷新了哪些信息。

void refreshInstanceInfo() {
    // 刷新DataCenter
    applicationInfoManager.refreshDataCenterInfoIfRequired();
    // 刷新租约信息
    applicationInfoManager.refreshLeaseInfoIfRequired();

    InstanceStatus status;
    try {
        status = getHealthCheckHandler().getStatus(instanceInfo.getStatus());
    } catch (Exception e) {
        logger.warn("Exception from healthcheckHandler.getStatus, setting status to DOWN", e);
        status = InstanceStatus.DOWN;
    }

    if (null != status) {
        applicationInfoManager.setInstanceStatus(status);
    }
}
  • 刷新DataCenter
public void refreshDataCenterInfoIfRequired() {
    String existingAddress = instanceInfo.getHostName();

    String existingSpotInstanceAction = null;
    if (instanceInfo.getDataCenterInfo() instanceof AmazonInfo) {
        existingSpotInstanceAction = ((AmazonInfo) instanceInfo.getDataCenterInfo()).get(AmazonInfo.MetaDataKey.spotInstanceAction);
    }

    // 获取最新的主机名、IP地址
    String newAddress;
    if (config instanceof RefreshableInstanceConfig) {
        // Refresh data center info, and return up to date address
        // 刷新数据中心信息,并返回最新地址
        newAddress = ((RefreshableInstanceConfig) config).resolveDefaultAddress(true);
    } else {
        newAddress = config.getHostName(true);
    }
    String newIp = config.getIpAddress();

    // hostName发生变化,更新主机名和IP地址
    if (newAddress != null && !newAddress.equals(existingAddress)) {
        logger.info("The address changed from : {} => {}", existingAddress, newAddress);
        logger.info("The address changed from : {} => {}", existingAddress, newAddress);
        updateInstanceInfo(newAddress, newIp);
    }

    if (config.getDataCenterInfo() instanceof AmazonInfo) {
        String newSpotInstanceAction = ((AmazonInfo) config.getDataCenterInfo()).get(AmazonInfo.MetaDataKey.spotInstanceAction);
        if (newSpotInstanceAction != null && !newSpotInstanceAction.equals(existingSpotInstanceAction)) {
            logger.info(String.format("The spot instance termination action changed from: %s => %s",
                    existingSpotInstanceAction,
                    newSpotInstanceAction));
            updateInstanceInfo(null , null );
        }
    }
}

private void updateInstanceInfo(String newAddress, String newIp) {

    // :( in the legacy code here the builder is acting as a mutator.
    // This is hard to fix as this same instanceInfo instance is referenced elsewhere.
    // We will most likely re-write the client at sometime so not fixing for now.

    InstanceInfo.Builder builder = new InstanceInfo.Builder(instanceInfo);
    if (newAddress != null) {
        builder.setHostName(newAddress);
    }
    if (newIp != null) {
        builder.setIPAddr(newIp);
    }
    builder.setDataCenterInfo(config.getDataCenterInfo());
    // 设置应用实例信息数据不一致
    instanceInfo.setIsDirty();
}

刷新DataCenter完成了对主机名、IP地址的刷新工作。

  • 刷新租约信息
public void refreshLeaseInfoIfRequired() {
    // 获取租约信息
    LeaseInfo leaseInfo = instanceInfo.getLeaseInfo();
    if (leaseInfo == null) {
        return;
    }
    // 契约超时时间,超过currentLeaseDuration秒后没有续约,Server就认为他不可用了,随之就会将其剔除。
    int currentLeaseDuration = config.getLeaseExpirationDurationInSeconds();
    // 续约间隔时间(单位:秒)
    int currentLeaseRenewal = config.getLeaseRenewalIntervalInSeconds();

    /**
     * 如果租约信息的契约超时时间和续约间隔时间不等于配置中的时间,则更新着两个时间
     * 并将DirtyFlag设置为true,以便在下一次心跳时将实例信息传递到 discovery server
     */
    if (leaseInfo.getDurationInSecs() != currentLeaseDuration || leaseInfo.getRenewalIntervalInSecs() != currentLeaseRenewal) {
        logger.info("==The LeaseInfo changed from : {} => {}", leaseInfo.getDurationInSecs(),currentLeaseDuration);
        logger.info("==The LeaseInfo changed from : {} => {}", leaseInfo.getRenewalIntervalInSecs(),currentLeaseRenewal);
        LeaseInfo newLeaseInfo = LeaseInfo.Builder.newBuilder()
                .setRenewalIntervalInSecs(currentLeaseRenewal)
                .setDurationInSecs(currentLeaseDuration)
                .build();
        // 设置租约信息
        instanceInfo.setLeaseInfo(newLeaseInfo);
        // 设置应用实例数据信息不一致
        instanceInfo.setIsDirty();
    }
}

刷新租约信息刷新了契约超时时间、续约间隔时间

4.3 应用实例状态变更监听器

改监听器创建了StatusChangeListener接口的实例,并重写了notify方法,通过手动方式注册到applicationInfoManager,当发生StatusChangeEvent事件时,将触发notify方法。

前文介绍过refreshInstanceInfo方法,在该方法中有设置应用实例状态的一句代码:applicationInfoManager.setInstanceStatus(status);这句代码并不仅仅简单的设置了应用实例状态,而且发布了StatusChangeEvent通知

public synchronized void setInstanceStatus(InstanceStatus status) {
    InstanceStatus next = instanceStatusMapper.map(status);
    if (next == null) {
        return;
    }

    InstanceStatus prev = instanceInfo.setStatus(next);
    if (prev != null) {
        for (StatusChangeListener listener : listeners.values()) {
            try {
                // 发布StatusChangeEvent通知
                listener.notify(new StatusChangeEvent(prev, next));
            } catch (Exception e) {
                logger.warn("failed to notify listener: {}", listener.getId(), e);
            }
        }
    }
}

发布了StatusChangeEvent后,会调用InstanceInfoReplicator的onDemandUpdate方法:

public boolean onDemandUpdate() {
    if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {
        if (!scheduler.isShutdown()) {
            scheduler.submit(new Runnable() {
                @Override
                public void run() {
                    logger.debug("Executing on-demand update of local InstanceInfo");
                    Future latestPeriodic = scheduledPeriodicRef.get();
                    // 取消最近一次任务
                    if (latestPeriodic != null && !latestPeriodic.isDone()) {
                        logger.debug("Canceling the latest scheduled update, it will be rescheduled at the end of on demand update");
                        latestPeriodic.cancel(false);
                    }
                    // 再次执行任务,因为这里用到了ScheduledExecutorService,所以如果取消任务后,会导致整个定时任务被取消。
                    InstanceInfoReplicator.this.run();
                }
            });
            return true;
        } else {
            logger.warn("Ignoring onDemand update due to stopped scheduler");
            return false;
        }
    } else {
        logger.warn("Ignoring onDemand update due to rate limiter");
        return false;
    }
}

该方法中只是判断了一下Future的状态,并取消了最后一次任务,以防止错误的应用信息实例状态注册到Eureka注册中心。这里用到了ScheduledExecutorService来执行定时任务,所以在调用了Future的cancel方法之后,要重新执行任务,并在run方法中重新开启定时任务。

EurekaClient实例化过程中相关的知识点就分析到这里。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闲来也无事

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值