1. 程式人生 > >Spring Cloud原始碼分析之Eureka篇第八章:服務註冊名稱的來歷

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;

從啟動說起

  1. 在spring-cloud-commons庫的META-INF目錄下有spring.factories檔案,這是spring擴充套件規範的實現,這裡面配置的類會被例項化,其中就包含了HostInfoEnvironmentPostProcessor這個類,如下圖紅框所示: 在這裡插入圖片描述

  2. HostInfoEnvironmentPostProcessor實現了EnvironmentPostProcessor介面,來看看

    官方文件對EnvironmentPostProcessor的描述: 在這裡插入圖片描述 上圖紅框中說明開發者可以自定義環境變數; 上圖綠框中說明EnvironmentPostProcessor的實現類必須在spring.factories檔案中定義; 因此HostInfoEnvironmentPostProcessor類的作用已經清楚了:自定義環境變數

  3. 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.hostnamespring.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地址設定到環境變數中;

在配置類中儲存服務名稱

  1. 接下來看看配置類EurekaClientAutoConfiguration,這裡面主要是和Eureka相關的配置資訊的資料和邏輯;
  2. 請看方法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));
  1. 順藤摸瓜,展開方法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;

  1. 在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例項,就可以通過該例項獲得服務註冊名稱;

  1. Eureka client向Eureka server發起服務註冊的操作是在DiscoveryClient類中進行的,該類的構造方法如下: 在這裡插入圖片描述

如上圖所示,紅框中ApplicationInfoManager例項被注入,藍框中表明DiscoveryClient的成員變數instanceInfo獲得了InstanceInfo例項;

  1. 具體的註冊邏輯在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;
    }
  1. 上述程式碼中的eurekaTransport.registrationClient.register(instanceInfo)方法,經過層層呼叫,最終呼叫了AbstractJerseyEurekaHttpClient類的register方法,如下圖所示: 在這裡插入圖片描述

上圖的紅框表明,POST請求時InstanceInfo例項被作為請求引數提交到了Eureka server;

Wireshark抓包驗證

至此,程式碼分析已經結束了,最後我們用Wireshark抓包來驗證之前的分析結果,在Eureka client所在電腦上用Wireshark2.6.3來分析註冊請求: 在這裡插入圖片描述

如上圖所示,紅框中就是註冊請求,綠框中是請求包頭的全部內容,也就是前面看到的InstanceInfo例項的內容,藍框中的內容,就是服務註冊名稱的鍵值對,值就是Eureka server收到的註冊名稱;

最後來小結一下,服務註冊名稱從誕生到提交至Eureka server的過程:

  1. HostInfoEnvironmentPostProcessor將本機的hostname和IP地址設定到應用環境變數中;
  2. 配置類EurekaClientAutoConfiguration中,建立一個EurekaInstanceConfigBean型別的bean,其instanceId欄位就是即將上報到Eureka server的自身名稱,instanceId欄位的內容由hostname、應用名稱、自定義例項ID拼接而成,其中自定義例項ID來自配置項"spring.application.instance_id",如果不存在就用服務監聽埠代替;
  3. ApplicationInfoManager型別的bean在建立時被注入EurekaInstanceConfigBean例項,用於建立ApplicationInfoManager的成員變數instanceInfo;
  4. DiscoveryClient的構造方法中注入了ApplicationInfoManager,於是DiscoveryClient的成員變數instanceInfo就被賦值為ApplicationInfoManager的成員變數instanceInfo;
  5. DiscoveryClient的register方法負責註冊到Eureka server的邏輯,用到的引數就是成員變數instanceInfo;
  6. 發起註冊網路請求的操作最終由AbstractJerseyEurekaHttpClient類的register方法完成,POST的內容就是instanceInfo例項;