写点什么

Spring Cloud 源码分析之 Eureka 篇第八章:服务注册名称的来历

作者:程序员欣宸
  • 2022 年 7 月 11 日
  • 本文字数:4910 字

    阅读完需:约 16 分钟

Spring Cloud源码分析之Eureka篇第八章:服务注册名称的来历

欢迎访问我的 GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

关于服务注册名称

  • 服务注册名称,是指 Eureka client 注册到 Eureka server 时,用于标记自己身份的标志,举例说明,以下是个简单的 Eureka client 配置:


server:  port: 8082spring:  application:    name: springcloud-deep-providereureka:  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 接口,来看看官方文档对 EnvironmentPostProcessor 的描述:



  • 上图红框中说明开发者可以自定义环境变量;

  • 上图绿框中说明 EnvironmentPostProcessor 的实现类必须在 spring.factories 文件中定义;

  • 因此 HostInfoEnvironmentPostProcessor 类的作用已经清楚了:自定义环境变量

  • 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,果然与源码一致;

  • 源码读到此处,禁不住手痒,按照上面的逻辑,在应用的 aplication.yml 中增加配置项 spring.application.instance_id,看看能否生效,改过的 aplication.yml 内容如下图所示,红框中是新增的自定义实例 ID 配置:



  • 重启应用,重新注册到 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 的过程:


  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 实例;

欢迎关注 InfoQ:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...

发布于: 刚刚阅读数: 4
用户头像

搜索"程序员欣宸",一起畅游Java宇宙 2018.04.19 加入

前腾讯、前阿里员工,从事Java后台工作,对Docker和Kubernetes充满热爱,所有文章均为作者原创,个人Github:https://github.com/zq2599/blog_demos

评论

发布
暂无评论
Spring Cloud源码分析之Eureka篇第八章:服务注册名称的来历_Java_程序员欣宸_InfoQ写作社区