摘要: 原创出处 http://www.iocoder.cn/Eureka/eureka-client-init-first/ 「芋道源码」欢迎转载,保留摘要,谢谢!
关注 微信公众号:【芋道源码】 有福利:
本文主要分享 Eureka-Client 自身初始化的过程 ,不包含 Eureka-Client 向 Eureka-Server 的注册过程( 后面会另外文章分享 )。
Eureka-Client 自身初始化过程中,涉及到主要对象如下图:
考虑到整个初始化的过程中涉及的配置特别多,拆分成三篇文章:
下面我们来看看每个 类 的实现。
com.netflix.appinfo.EurekaInstanceConfig ,Eureka 应用实例 配置 接口 。在下文你会看到 EurekaClientConfig 接口 ,两者的区别如下:
EurekaInstanceConfig 整体类关系如下图:
点击 EurekaInstanceConfig 查看配置属性简介,已经添加中文注释,可以对照着英文注释一起理解。这里笔者摘出部分较为重要的属性:
#getLeaseRenewalIntervalInSeconds() :租约续约频率,单位:秒。应用不断通过按照该频率发送心跳给 Eureka-Server 以达到续约的作用。当 Eureka-Server 超过最大频率未收到续约(心跳),契约失效,进行应用移除。应用移除后,其他应用无法从 Eureka-Server 获取该应用。 #getLeaseExpirationDurationInSeconds() :契约过期时间,单位:秒。 #getDataCenterInfo() :数据中心信息。 com.netflix.appinfo.DataCenterInfo ,数据中心信息 接口 ,目前较为简单,标记所属数据中心名。一般情况下,我们使用 Name.MyOwn 。接口实现代码如下:
public interface DataCenterInfo{
/**
* 数据中心名枚举
*/
enum Name {
Netflix,
Amazon,
MyOwn
}
/**
* @return 归属的数据中心名
*/
Name getName();
}
#getNamespace() :配置命名空间,默认使用 eureka 。以 eureka-client.properties 举个例子:
eureka.name=eureka eureka.port=8080 eureka.vipAddress=eureka.mydomain.net
eureka 即是配置命名空间,一般情况无需修改。 TODO[0004]:健康检查
#isInstanceEnabledOnit() :应用初始化后是否开启。在详细解析。 com.netflix.appinfo.AbstractInstanceConfig ,Eureka 应用实例 配置 抽象基类 ,主要实现一些相对 通用 的配置,实现代码如下:
public abstract class AbstractInstanceConfig implements EurekaInstanceConfig{
/**
* 契约过期时间,单位:秒
*/
private static final int LEASE_EXPIRATION_DURATION_SECONDS = 90;
/**
* 租约续约频率,单位:秒。
*/
private static final int LEASE_RENEWAL_INTERVAL_SECONDS = 30;
/**
* 应用 https 端口关闭
*/
private static final boolean SECURE_PORT_ENABLED = false;
/**
* 应用 http 端口开启
*/
private static final boolean NON_SECURE_PORT_ENABLED = true;
/**
* 应用 http 端口
*/
private static final int NON_SECURE_PORT = 80;
/**
* 应用 https 端口
*/
private static final int SECURE_PORT = 443;
/**
* 应用初始化后开启
*/
private static final boolean INSTANCE_ENABLED_ON_INIT = false;
/**
* 主机信息
* key:主机 IP 地址
* value:主机名
*/
private static final Pair<String, String> hostInfo = getHostInfo();
/**
* 数据中心信息
*/
private DataCenterInfo info = new DataCenterInfo() {
@Override
public Name getName(){
return Name.MyOwn;
}
};
private static Pair<String, String> getHostInfo(){
Pair<String, String> pair;
try {
InetAddress localHost = InetAddress.getLocalHost();
pair = new Pair<String, String>(localHost.getHostAddress(), localHost.getHostName());
} catch (UnknownHostException e) {
logger.error("Cannot get host info", e);
pair = new Pair<String, String>("", "");
}
return pair;
}
// .... 省略 setting / getting 方法
}
#getHostInfo() 方法,获取本地服务器的主机名和主机 IP 地址。 如果主机有多网卡或者虚拟机网卡 ,这块要小心,解决方式如下:
hostname + etc/hosts 文件,从而映射主机名和 IP 地址。 com.netflix.appinfo.PropertiesInstanceConfig ,基于 配置文件 的 Eureka 应用实例 配置 抽象基类 ,实现代码如下:
public abstract class PropertiesInstanceConfig extends AbstractInstanceConfig implements EurekaInstanceConfig{
/**
* 命名空间
*/
protected final String namespace;
/**
* 配置文件对象
*/
protected final DynamicPropertyFactory configInstance;
/**
* 应用分组
* 从 环境变量 获取
*/
private String appGrpNameFromEnv;
public PropertiesInstanceConfig(){
this(CommonConstants.DEFAULT_CONFIG_NAMESPACE);
}
public PropertiesInstanceConfig(String namespace){
this(namespace, new DataCenterInfo() {
@Override
public Name getName(){
return Name.MyOwn;
}
});
}
public PropertiesInstanceConfig(String namespace, DataCenterInfo info){
super(info);
// 设置 namespace,为 "." 结尾
this.namespace = namespace.endsWith(".")
? namespace
: namespace + ".";
// 从 环境变量 获取 应用分组
appGrpNameFromEnv = ConfigurationManager.getConfigInstance()
.getString(FALLBACK_APP_GROUP_KEY, Values.UNKNOWN_APPLICATION);
// 初始化 配置文件对象
this.configInstance = Archaius1Utils.initConfig(CommonConstants.CONFIG_FILE_NAME);
}
@Override
public String getAppGroupName(){
return configInstance.getStringProperty(namespace + APP_GROUP_KEY, appGrpNameFromEnv).get().trim();
}
}
configInstance 属性,配置文件对象,基于 Netflix Archaius 1.x 实现配置文件的读取。在 com.netflix.appinfo.PropertyBasedInstanceConfigConstants 可以看到配置文件的每个属性 KEY 。 appGrpNameFromEnv 属性,应用分组,从 环境变量 中获取。从 #getAppGroupName() 方法中,可以看到优先还是从配置文件读取。设置方法如下:
System.setProperty(FALLBACK_APP_GROUP_KEY, "app_gropu_name");
FALLBACK_APP_GROUP_KEY ,私有静态变量,实际得使用 NETFLIX_APP_GROUP 。 com.netflix.config.ConfigurationManager 可以从 环境变量 获取到值。 调用 Archaius1Utils#initConfig(...) 方法,初始化读取的配置文件对象,实现代码如下:
public final class Archaius1Utils{
private static final Logger logger = LoggerFactory.getLogger(Archaius1Utils.class);
private static final String ARCHAIUS_DEPLOYMENT_ENVIRONMENT = "archaius.deployment.environment";
private static final String EUREKA_ENVIRONMENT = "eureka.environment";
public static DynamicPropertyFactory initConfig(String configName){
// 配置文件对象
DynamicPropertyFactory configInstance = DynamicPropertyFactory.getInstance();
// 配置文件名
DynamicStringProperty EUREKA_PROPS_FILE = configInstance.getStringProperty("eureka.client.props", configName);
// 配置文件环境
String env = ConfigurationManager.getConfigInstance().getString(EUREKA_ENVIRONMENT, "test");
ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_ENVIRONMENT, env);
// 将配置文件加载到环境变量
String eurekaPropsFile = EUREKA_PROPS_FILE.get();
try {
ConfigurationManager.loadCascadedPropertiesFromResources(eurekaPropsFile);
} catch (IOException e) {
logger.warn(
"Cannot find the properties specified : {}. This may be okay if there are other environment "
+ "specific properties or the configuration is installed with a different mechanism.",
eurekaPropsFile);
}
return configInstance;
}
}
eureka.client.props ,获取配置文件名。如果未配置,使用参数 configName ,即 CommonConstants.CONFIG_FILE_NAME ( "eureka-client" )。 eureka.environment ( EUREKA_ENVIRONMENT ),获取配置文件环境。 ConfigurationManager#loadCascadedPropertiesFromResources(...) 方法,读取配置文件到环境变量,首先读取 ${eureka.client.props} 对应的配置文件;然后读取 ${eureka.client.props}-${eureka.environment} 对应的配置文件。若有相同属性,进行覆盖。 com.netflix.appinfo.MyDataCenterInstanceConfig ,非 AWS 数据中心的 Eureka 应用实例 配置 实现类 ,实现代码如下:
public class MyDataCenterInstanceConfig extends PropertiesInstanceConfig implements EurekaInstanceConfig{
public MyDataCenterInstanceConfig(){
}
public MyDataCenterInstanceConfig(String namespace){
super(namespace);
}
public MyDataCenterInstanceConfig(String namespace, DataCenterInfo dataCenterInfo){
super(namespace, dataCenterInfo);
}
}
一般情况下,使用 MyDataCenterInstanceConfig 配置 Eureka 应用实例。
在 Spring-Cloud-Eureka 里, 直接 基于 EurekaInstanceConfig 接口重新实现了配置类,实际逻辑差别不大,在TODO[0007] :《Spring-Cloud-Eureka-Client》详细解析。
com.netflix.appinfo.InstanceInfo , 应用实例 信息。Eureka-Client 向 Eureka-Server 注册 该对象信息。注册成功后,可以被其他 Eureka-Client 发现 。
本文仅分享 InstanceInfo 的初始化。InstanceInfo 里和注册发现相关的属性和方法,暂时跳过。
com.netflix.appinfo.providers.EurekaConfigBasedInstanceInfoProvider ,基于 EurekaInstanceConfig 创建 InstanceInfo 的工厂,实现代码如下:
1: @Singleton
2: public class EurekaConfigBasedInstanceInfoProvider implements Provider<InstanceInfo>{
3: private static final Logger LOG = LoggerFactory.getLogger(EurekaConfigBasedInstanceInfoProvider.class);
4:
5: private final EurekaInstanceConfig config;
6:
7: private InstanceInfo instanceInfo;
8:
9: @Inject(optional = true)
10: private VipAddressResolver vipAddressResolver = null;
11:
12: @Inject
13: public EurekaConfigBasedInstanceInfoProvider(EurekaInstanceConfig config){
14: this.config = config;
15: }
16:
17: @Override
18: public synchronized InstanceInfo get(){
19: if (instanceInfo == null) {
20: // Build the lease information to be passed to the server based on config
21: // 创建 租约信息构建器,并设置属性
22: LeaseInfo.Builder leaseInfoBuilder = LeaseInfo.Builder.newBuilder()
23: .setRenewalIntervalInSecs(config.getLeaseRenewalIntervalInSeconds())
24: .setDurationInSecs(config.getLeaseExpirationDurationInSeconds());
25:
26: // 创建 VIP地址解析器
27: if (vipAddressResolver == null) {
28: vipAddressResolver = new Archaius1VipAddressResolver();
29: }
30:
31: // Builder the instance information to be registered with eureka server
32: // 创建 应用实例信息构建器
33: InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder(vipAddressResolver);
34:
35: // 应用实例编号
36: // set the appropriate id for the InstanceInfo, falling back to datacenter Id if applicable, else hostname
37: String instanceId = config.getInstanceId();
38: DataCenterInfo dataCenterInfo = config.getDataCenterInfo();
39: if (instanceId == null || instanceId.isEmpty()) {
40: if (dataCenterInfo instanceof UniqueIdentifier) {
41: instanceId = ((UniqueIdentifier) dataCenterInfo).getId();
42: } else {
43: instanceId = config.getHostName(false);
44: }
45: }
46:
47: // 获得 主机名
48: String defaultAddress;
49: if (config instanceof RefreshableInstanceConfig) {
50: // Refresh AWS data center info, and return up to date address
51: defaultAddress = ((RefreshableInstanceConfig) config).resolveDefaultAddress(false);
52: } else {
53: defaultAddress = config.getHostName(false);
54: }
55: // fail safe
56: if (defaultAddress == null || defaultAddress.isEmpty()) {
57: defaultAddress = config.getIpAddress();
58: }
59:
60: // 设置 应用实例信息构建器 的 属性
61: builder.setNamespace(config.getNamespace())
62: .setInstanceId(instanceId)
63: .setAppName(config.getAppname())
64: .setAppGroupName(config.getAppGroupName())
65: .setDataCenterInfo(config.getDataCenterInfo())
66: .setIPAddr(config.getIpAddress())
67: .setHostName(defaultAddress) // 主机名
68: .setPort(config.getNonSecurePort())
69: .enablePort(PortType.UNSECURE, config.isNonSecurePortEnabled())
70: .setSecurePort(config.getSecurePort())
71: .enablePort(PortType.SECURE, config.getSecurePortEnabled())
72: .setVIPAddress(config.getVirtualHostName()) // VIP 地址
73: .setSecureVIPAddress(config.getSecureVirtualHostName())
74: .setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl())
75: .setStatusPageUrl(config.getStatusPageUrlPath(), config.getStatusPageUrl())
76: .setASGName(config.getASGName())
77: .setHealthCheckUrls(config.getHealthCheckUrlPath(),
78: config.getHealthCheckUrl(), config.getSecureHealthCheckUrl());
79:
80: // 应用初始化后是否开启
81: // Start off with the STARTING state to avoid traffic
82: if (!config.isInstanceEnabledOnit()) {
83: InstanceStatus initialStatus = InstanceStatus.STARTING;
84: LOG.info("Setting initial instance status as: " + initialStatus);
85: builder.setStatus(initialStatus);
86: } else {
87: LOG.info("Setting initial instance status as: {}. This may be too early for the instance to advertise "
88: + "itself as available. You would instead want to control this via a healthcheck handler.",
89: InstanceStatus.UP);
90: }
91:
92: // 设置 应用实例信息构建器 的 元数据( Metadata )集合
93: // Add any user-specific metadata information
94: for (Map.Entry<String, String> mapEntry : config.getMetadataMap().entrySet()) {
95: String key = mapEntry.getKey();
96: String value = mapEntry.getValue();
97: builder.add(key, value);
98: }
99:
100: // 创建 应用实例信息
101: instanceInfo = builder.build();
102:
103: // 设置 应用实例信息 的 租约信息
104: instanceInfo.setLeaseInfo(leaseInfoBuilder.build());
105: }
106: return instanceInfo;
107: }
108:
109: }
javax.inject.Provider 接口,设置 InstanceInfo 的生成工厂。感兴趣的同学,可以点击 《Google-Guice入门介绍》 搜索 Provider 关键字。目前处于 试验 阶段,未完成。 EurekaConfigBasedInstanceInfoProvider(config) 构造方法,设置生成 InstanceInfo 的 EurekaInstanceConfig 配置。 调用 #get() 方法,根据 EurekaInstanceConfig 创建 InstanceInfo。InstanceInfo 的绝大数属性和 EurekaInstanceConfig 是一致的 。实现代码如下:
com.netflix.appinfo.LeaseInfo.Builder ),并设置 renewalIntervalInSecs / durationInSecs 属性。 第 26 至 29 行 :创建 VIP地址解析器( com.netflix.appinfo.providers.VipAddressResolver )。实现代码如下:
// VipAddressResolver.java
public interface VipAddressResolver{
String resolveDeploymentContextBasedVipAddresses(String vipAddressMacro);
}
public class Archaius1VipAddressResolver implements VipAddressResolver{
private static final Pattern VIP_ATTRIBUTES_PATTERN = Pattern.compile("//$//{(.*?)//}");
@Override
public String resolveDeploymentContextBasedVipAddresses(String vipAddressMacro){
if (vipAddressMacro == null) {
return null;
}
String result = vipAddressMacro;
// 替换表达式
Matcher matcher = VIP_ATTRIBUTES_PATTERN.matcher(result);
while (matcher.find()) {
String key = matcher.group(1);
String value = DynamicPropertyFactory.getInstance().getStringProperty(key, "").get();
logger.debug("att:" + matcher.group());
logger.debug(", att key:" + key);
logger.debug(", att value:" + value);
logger.debug("");
result = result.replaceAll("//$//{" + key + "//}", value);
matcher = VIP_ATTRIBUTES_PATTERN.matcher(result);
}
return result;
}
}
#resolveDeploymentContextBasedVipAddresses() 方法,将 VIP地址 里的 ${(.*?)} 查找配置文件里的键值进行替换。例如, ${eureka.env}.domain.com ,查找配置文件里的键 ${eureka.env} 对应值进行替换。TODO[0005]:调试下来,发现 Archaius 已经替换,等到找到答案修改此处。 第 32 至 33 行 :创建应用实例信息构建器( com.netflix.appinfo.InstanceInfo.Builder )。
instanceId )。 ApplicationInfoManager#setInstanceStatus(...) 方法改变应用实例状态,在 《Eureka 源码解析 —— 应用实例注册发现 (一)之注册》「2.1 应用实例信息复制器」 有详细解析。 com.netflix.appinfo.InstanceInfo )。 com.netflix.appinfo.InstanceInfo )。 com.netflix.appinfo.ApplicationInfoManager ,应用信息管理器。实现代码如下:
public class ApplicationInfoManager{
/**
* 单例
*/
private static ApplicationInfoManager instance = new ApplicationInfoManager(null, null, null);
/**
* 状态变更监听器
*/
protected final Map<String, StatusChangeListener> listeners;
/**
* 应用实例状态匹配
*/
private final InstanceStatusMapper instanceStatusMapper;
/**
* 应用实例信息
*/
private InstanceInfo instanceInfo;
/**
* 应用实例配置
*/
private EurekaInstanceConfig config;
// ... 省略其它构造方法
public ApplicationInfoManager(EurekaInstanceConfig config, InstanceInfo instanceInfo, OptionalArgs optionalArgs){
this.config = config;
this.instanceInfo = instanceInfo;
this.listeners = new ConcurrentHashMap<String, StatusChangeListener>();
if (optionalArgs != null) {
this.instanceStatusMapper = optionalArgs.getInstanceStatusMapper();
} else {
this.instanceStatusMapper = NO_OP_MAPPER;
}
// Hack to allow for getInstance() to use the DI'd ApplicationInfoManager
instance = this;
}
// ... 省略其它方法
}
listeners 属性,状态变更监听器集合。在 《Eureka 源码解析 —— 应用实例注册发现 (一)之注册》「2.1 应用实例信息复制器」 有详细解析。 instanceStatusMapper 属性,应用实例状态匹配。实现代码如下:
// ApplicationInfoManager.java
public static interface InstanceStatusMapper{
}
private static final InstanceStatusMapper NO_OP_MAPPER = new InstanceStatusMapper() {
@Override
public InstanceStatus map(InstanceStatus prev){
return prev;
}
};
#map 方法,根据传入 pre 参数,转换成对应的应用实例状态。 涉及到配置,内容初看起来会比较多,慢慢理解后,就会变得很“啰嗦”,请保持耐心。
胖友,分享一个朋友圈可好。