Spring Cloud原始碼分析之Eureka篇第八章:服務註冊名稱的來歷
關於服務註冊名稱
服務註冊名稱,是指Eureka client註冊到Eureka server時,用於標記自己身份的標誌,舉例說明,以下是個簡單的Eureka client配置:
server:
port: 8082
spring:
application:
name: springcloud-deep-provider
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8081/eureka/
這樣配置的應用,啟動後如果在Eureka server註冊成功,那麼Eureka server的home頁面資訊如下,紅框中就是註冊名稱:
本文目標是通過分析Eureka client原始碼來找出這個名稱是如何建立的;
關於原始碼版本
本次分析的Spring Cloud版本為Edgware.RELEASE,對應的eureka-client版本為1.7.0;
從啟動說起
-
在spring-cloud-commons庫的META-INF目錄下有spring.factories檔案,這是spring擴充套件規範的實現,這裡面配置的類會被例項化,其中就包含了HostInfoEnvironmentPostProcessor這個類,如下圖紅框所示:
-
HostInfoEnvironmentPostProcessor實現了EnvironmentPostProcessor介面,來看看
-
HostInfoEnvironmentPostProcessor原始碼如下:
public class HostInfoEnvironmentPostProcessor
implements EnvironmentPostProcessor, Ordered {
// Before ConfigFileApplicationListener
private int order = ConfigFileApplicationListener.DEFAULT_ORDER - 1;
@Override
public int getOrder() {
return this.order;
}
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
InetUtils.HostInfo hostInfo = getFirstNonLoopbackHostInfo(environment);
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("spring.cloud.client.hostname", hostInfo.getHostname());
map.put("spring.cloud.client.ipAddress", hostInfo.getIpAddress());
MapPropertySource propertySource = new MapPropertySource(
"springCloudClientHostInfo", map);
environment.getPropertySources().addLast(propertySource);
}
private HostInfo getFirstNonLoopbackHostInfo(ConfigurableEnvironment environment) {
InetUtilsProperties target = new InetUtilsProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(target,
InetUtilsProperties.PREFIX);
binder.bind(new PropertySourcesPropertyValues(environment.getPropertySources()));
try (InetUtils utils = new InetUtils(target)) {
return utils.findFirstNonLoopbackHostInfo();
}
}
}
上述程式碼有兩處需要注意: 第一,設定了兩個環境變數:spring.cloud.client.hostname和spring.cloud.client.ipAddress; 第二,getFirstNonLoopbackHostInfo方法返回的物件中,127.0.0.1這樣的IP地址是會被過濾掉的,過濾邏輯很簡單,原始碼在Inet4Address類的isLoopbackAddress方法:
public boolean isLoopbackAddress() {
/* 127.x.x.x */
byte[] byteAddr = getAddress();
return byteAddr[0] == 127;
}
小結:HostInfoEnvironmentPostProcessor的作用是把本機的hostname和IP地址設定到環境變數中;
在配置類中儲存服務名稱
- 接下來看看配置類EurekaClientAutoConfiguration,這裡面主要是和Eureka相關的配置資訊的資料和邏輯;
- 請看方法eurekaInstanceConfigBean,該方法向Spring容器環境提供EurekaInstanceConfigBean例項,注意instance.setInstanceId(getDefaultInstanceId(propertyResolver))這一行:
@Bean
@ConditionalOnMissingBean(value = EurekaInstanceConfig.class, search = SearchStrategy.CURRENT)
public EurekaInstanceConfigBean eurekaInstanceConfigBean(InetUtils inetUtils,
ManagementMetadataProvider managementMetadataProvider) throws MalformedURLException {
PropertyResolver eurekaPropertyResolver = new RelaxedPropertyResolver(this.env, "eureka.instance.");
String hostname = eurekaPropertyResolver.getProperty("hostname");
boolean preferIpAddress = Boolean.parseBoolean(eurekaPropertyResolver.getProperty("preferIpAddress"));
boolean isSecurePortEnabled = Boolean.parseBoolean(eurekaPropertyResolver.getProperty("securePortEnabled"));
String serverContextPath = propertyResolver.getProperty("server.contextPath", "/");
int serverPort = Integer.valueOf(propertyResolver.getProperty("server.port", propertyResolver.getProperty("port", "8080")));
Integer managementPort = propertyResolver.getProperty("management.port", Integer.class);// nullable. should be wrapped into optional
String managementContextPath = propertyResolver.getProperty("management.contextPath");// nullable. should be wrapped into optional
Integer jmxPort = propertyResolver.getProperty("com.sun.management.jmxremote.port", Integer.class);//nullable
EurekaInstanceConfigBean instance = new EurekaInstanceConfigBean(inetUtils);
instance.setNonSecurePort(serverPort);
//服務自身的名稱在此設定,儲存到instance後,其他地方就可以使用了
instance.setInstanceId(getDefaultInstanceId(propertyResolver));
- 順藤摸瓜,展開方法getDefaultInstanceId:
public static String getDefaultInstanceId(PropertyResolver resolver) {
RelaxedPropertyResolver relaxed = new RelaxedPropertyResolver(resolver);
String vcapInstanceId = relaxed.getProperty("vcap.application.instance_id");
if (StringUtils.hasText(vcapInstanceId)) {
return vcapInstanceId;
}
String hostname = relaxed.getProperty("spring.cloud.client.hostname");
String appName = relaxed.getProperty("spring.application.name");
String namePart = combineParts(hostname, SEPARATOR, appName);
String indexPart = relaxed.getProperty("spring.application.instance_id",
relaxed.getProperty("server.port"));
return combineParts(namePart, SEPARATOR, indexPart);
}
如上述程式碼所示,真相大白,服務註冊名稱一共有三部分:hostname、應用名稱、自定義例項ID,如果自定義例項ID沒有配置就用監聽埠代替;
此時再來回顧之前在Eureka server的home頁面上看到的服務註冊名:localhost:springcloud-deep-provider:8082,果然與原始碼一致; 4. 原始碼讀到此處,禁不住手癢,按照上面的邏輯,在應用的aplication.yml中增加配置項spring.application.instance_id,看看能否生效,改過的aplication.yml內容如下圖所示,紅框中是新增的自定義例項ID配置: 5. 重啟應用,重新註冊到Eureka server,此時再看home頁面如下圖紅框,服務註冊名稱果然已經更新:
使用配置類中的服務名稱
現在我們知道了EurekaInstanceConfigBean例項的instanceId欄位被設定為"hostname:應用名稱:自定義例項ID",接下來看該欄位如何被提交到Eureka server;
- 在EurekaClientAutoConfiguration類中有個eurekaApplicationInfoManager方法,為spring容器提供了ApplicationInfoManager例項:
@Bean
@ConditionalOnMissingBean(value = ApplicationInfoManager.class, search = SearchStrategy.CURRENT)
public ApplicationInfoManager eurekaApplicationInfoManager(
EurekaInstanceConfig config) {
//config就是前面看到的EurekaInstanceConfigBean例項,
//EurekaInstanceConfig是個介面,EurekaInstanceConfigBean是該介面的實現,
//instanceInfo例項中已經儲存了EurekaInstanceConfigBean的資訊,也包括instanceId欄位
InstanceInfo instanceInfo = new InstanceInfoFactory().create(config);
//生成ApplicationInfoManager例項,
//config和instanceInfo都被設定為ApplicationInfoManager例項的成員變數
return new ApplicationInfoManager(config, instanceInfo);
}
如上所示,spring容器中有了ApplicationInfoManager例項,就可以通過該例項獲得服務註冊名稱;
- Eureka client向Eureka server發起服務註冊的操作是在DiscoveryClient類中進行的,該類的構造方法如下:
如上圖所示,紅框中ApplicationInfoManager例項被注入,藍框中表明DiscoveryClient的成員變數instanceInfo獲得了InstanceInfo例項;
- 具體的註冊邏輯在DiscoveryClient的register方法中,可見成員變數instanceInfo被當作入參傳入了註冊邏輯的API:
/**
* Register with the eureka service by making the appropriate REST call.
*/
boolean register() throws Throwable {
logger.info(PREFIX + appPathIdentifier + ": registering service...");
EurekaHttpResponse<Void> httpResponse;
try {
//以成員變數instanceInfo作為入參進行註冊
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}
- 上述程式碼中的eurekaTransport.registrationClient.register(instanceInfo)方法,經過層層呼叫,最終呼叫了AbstractJerseyEurekaHttpClient類的register方法,如下圖所示:
上圖的紅框表明,POST請求時InstanceInfo例項被作為請求引數提交到了Eureka server;
Wireshark抓包驗證
至此,程式碼分析已經結束了,最後我們用Wireshark抓包來驗證之前的分析結果,在Eureka client所在電腦上用Wireshark2.6.3來分析註冊請求:
如上圖所示,紅框中就是註冊請求,綠框中是請求包頭的全部內容,也就是前面看到的InstanceInfo例項的內容,藍框中的內容,就是服務註冊名稱的鍵值對,值就是Eureka server收到的註冊名稱;
最後來小結一下,服務註冊名稱從誕生到提交至Eureka server的過程:
- HostInfoEnvironmentPostProcessor將本機的hostname和IP地址設定到應用環境變數中;
- 配置類EurekaClientAutoConfiguration中,建立一個EurekaInstanceConfigBean型別的bean,其instanceId欄位就是即將上報到Eureka server的自身名稱,instanceId欄位的內容由hostname、應用名稱、自定義例項ID拼接而成,其中自定義例項ID來自配置項"spring.application.instance_id",如果不存在就用服務監聽埠代替;
- ApplicationInfoManager型別的bean在建立時被注入EurekaInstanceConfigBean例項,用於建立ApplicationInfoManager的成員變數instanceInfo;
- DiscoveryClient的構造方法中注入了ApplicationInfoManager,於是DiscoveryClient的成員變數instanceInfo就被賦值為ApplicationInfoManager的成員變數instanceInfo;
- DiscoveryClient的register方法負責註冊到Eureka server的邏輯,用到的引數就是成員變數instanceInfo;
- 發起註冊網路請求的操作最終由AbstractJerseyEurekaHttpClient類的register方法完成,POST的內容就是instanceInfo例項;