前言

  • 上一章节我们知道了SpringCloud集成Consul过程就是注册bean的过程,那么有了这些bean,那它们是怎样发挥作用的呢,如果要实现微服务之间的通信就是要将本地服务信息注册得到注册中心上,那SpringCloudConsul是怎么实现的,这个是我们本章要探究的。

  • 因为SpringCloudConsul自动配置的类很多,一个一个去找十分麻烦,所以我们这里先关注它是怎么将本地服务注册得到注册中心上,通过之前章节可以知道只要启动应用,应用就会自动将自己注册到注册中心上,因为Spring有很多钩子接口及一套生命周期,所以我们看看它是怎么实现的,我们可以将log日志改为debug,通过日志来找出那些关键信息

解析

  • 通过启动日志查看关键字registe我们可以定位到这行日志,这些信息就是我们在SpringCloud(二)注册中心Consul章节中介绍注册服务的信息是一一对应的,所以我们可以知道SpringCloudConsul注册服务也是通过RESTful HTTP API来注册服务的
1
o.s.c.c.s.ConsulServiceRegistry          : Registering service with consul: NewService{id='cloudUser-dev-2010', name='cloudUser', tags=[], address='10.135.95.34', port=2010, enableTagOverride=null, check=Check{script='null', interval='10s', ttl='null', http='http://10.135.95.34:2010/health', tcp='null', timeout='null', deregisterCriticalServiceAfter='null', tlsSkipVerify=null, status='null'}, checks=null}
  • 日志拆解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
id='cloudUser-dev-2010', // 服务id
name='cloudUser', // 服务名
tags=[], // 服务的tag,自定义,可以根据这个tag来区分同一个服务名的服务
address='10.135.95.34', // 服务注册到consul的IP,服务发现,发现的就是这个IP
port=2010, // 服务注册consul的PORT,发现的就是这个PORT
enableTagOverride=null,
check = Check{ // 健康检查部分
script='null',
interval='10s', // 健康检查间隔时间,每隔10s,调用一次上面的URL
ttl='null',
http='http://10.135.95.34:2010/health', // 指定健康检查的URL,调用后只要返回20X,consul都认为是健康的
tcp='null',
timeout='null',
deregisterCriticalServiceAfter='null',
tlsSkipVerify=null,
status='null'
},
checks=null
}
  • 看日志定位到org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistry,这个类是在spring-cloud-consul-discovery包下的,上一章节有涉及到
  • 通过类名字及方法名我们可以知道这个类是Consul服务的注册及下架的处理类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
* @author Spencer Gibb
*/
public class ConsulServiceRegistry implements ServiceRegistry<ConsulRegistration> {

private static Log log = LogFactory.getLog(ConsulServiceRegistry.class);

private final ConsulClient client;

private final ConsulDiscoveryProperties properties;

private final TtlScheduler ttlScheduler;

private final HeartbeatProperties heartbeatProperties;

public ConsulServiceRegistry(ConsulClient client, ConsulDiscoveryProperties properties, TtlScheduler ttlScheduler, HeartbeatProperties heartbeatProperties) {
this.client = client;
this.properties = properties;
this.ttlScheduler = ttlScheduler;
this.heartbeatProperties = heartbeatProperties;
}

@Override
public void register(ConsulRegistration reg) {
log.info("Registering service with consul: " + reg.getService());
try {
client.agentServiceRegister(reg.getService(), properties.getAclToken());
if (heartbeatProperties.isEnabled() && ttlScheduler != null) {
ttlScheduler.add(reg.getInstanceId());
}
}
catch (ConsulException e) {
if (this.properties.isFailFast()) {
log.error("Error registering service with consul: " + reg.getService(), e);
ReflectionUtils.rethrowRuntimeException(e);
}
log.warn("Failfast is false. Error registering service with consul: " + reg.getService(), e);
}
}

@Override
public void deregister(ConsulRegistration reg) {
if (ttlScheduler != null) {
ttlScheduler.remove(reg.getInstanceId());
}
if (log.isInfoEnabled()) {
log.info("Deregistering service with consul: " + reg.getInstanceId());
}
client.agentServiceDeregister(reg.getInstanceId());
}

@Override
public void close() {

}

@Override
public void setStatus(ConsulRegistration registration, String status) {
if (status.equalsIgnoreCase(OUT_OF_SERVICE.getCode())) {
client.agentServiceSetMaintenance(registration.getInstanceId(), true);
} else if (status.equalsIgnoreCase(UP.getCode())) {
client.agentServiceSetMaintenance(registration.getInstanceId(), false);
} else {
throw new IllegalArgumentException("Unknown status: "+status);
}

}

@Override
public Object getStatus(ConsulRegistration registration) {
String serviceId = registration.getServiceId();
Response<List<Check>> response = client.getHealthChecksForService(serviceId, QueryParams.DEFAULT);
List<Check> checks = response.getValue();

for (Check check : checks) {
if (check.getServiceId().equals(registration.getInstanceId())) {
if (check.getName().equalsIgnoreCase("Service Maintenance Mode")) {
return OUT_OF_SERVICE.getCode();
}
}
}

return UP.getCode();
}
}
  • 下面来看ConsulServiceRegistry是如何发挥作用的

ConsulServiceRegistry bean注册初始化

  • 点击ConsulServiceRegistry的引用可以进入到org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistryAutoConfiguration类,这个类是见名是自动配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* @author Spencer Gibb
*/
@Configuration
@ConditionalOnConsulEnabled
@ConditionalOnProperty(value = "spring.cloud.service-registry.enabled", matchIfMissing = true)
@AutoConfigureBefore(ServiceRegistryAutoConfiguration.class)
public class ConsulServiceRegistryAutoConfiguration {

@Autowired(required = false)
private TtlScheduler ttlScheduler;

@Bean
@ConditionalOnMissingBean
public ConsulServiceRegistry consulServiceRegistry(ConsulClient consulClient, ConsulDiscoveryProperties properties,
HeartbeatProperties heartbeatProperties) {
return new ConsulServiceRegistry(consulClient, properties, ttlScheduler, heartbeatProperties);
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty("spring.cloud.consul.discovery.heartbeat.enabled")
public TtlScheduler ttlScheduler(ConsulClient consulClient, HeartbeatProperties heartbeatProperties) {
return new TtlScheduler(heartbeatProperties, consulClient);
}

@Bean
@ConditionalOnMissingBean
public HeartbeatProperties heartbeatProperties() {
return new HeartbeatProperties();
}

@Bean
@ConditionalOnMissingBean
public ConsulDiscoveryProperties consulDiscoveryProperties(InetUtils inetUtils) {
return new ConsulDiscoveryProperties(inetUtils);
}

}
  • 回顾上一章节,我们知道spring.factories里面配置了这个类,所以这个类会被Spring自动注册
1
2
3
4
5
6
7
8
9
10
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.consul.discovery.RibbonConsulAutoConfiguration,\
org.springframework.cloud.consul.discovery.configclient.ConsulConfigServerAutoConfiguration,\
org.springframework.cloud.consul.serviceregistry.ConsulAutoServiceRegistrationAutoConfiguration,\
org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistryAutoConfiguration,\
org.springframework.cloud.consul.discovery.ConsulDiscoveryClientConfiguration


org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.consul.discovery.configclient.ConsulDiscoveryClientConfigServiceBootstrapConfiguration
  • 观察ConsulServiceRegistryAutoConfiguration

    • 关注@ConditionalOnProperty(value = "spring.cloud.service-registry.enabled", matchIfMissing = true)这个条件注解,这里是说需要配置了该属性ConsulServiceRegistryAutoConfiguration类才生效,这个属性名是不是很熟悉,没错就是上一章节@EnableDiscoveryClient注解自动注册的AutoServiceRegistrationConfiguration类相对应的,AutoServiceRegistrationConfiguration类就是配置了这个属性

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      /**
      * @author Spencer Gibb
      */
      @Configuration
      @EnableConfigurationProperties(AutoServiceRegistrationProperties.class)
      @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
      public class AutoServiceRegistrationConfiguration {
      }

      @ConfigurationProperties("spring.cloud.service-registry.auto-registration")
      public class AutoServiceRegistrationProperties {

      ...
      }
    • 在这里可以得出结论@EnableDiscoveryClient是开启服务自动注册类的开关,因为@SpringCloudApplication注解默认自带这个注解,所以在SpringCloud中只要添加了注册中心的依赖,就会默认开启注册中心的功能

    • TtlScheduler这个Bean,这个是个定时器
    • HeartbeatProperties 属性配置类,对应的属性前缀:spring.cloud.consul.discovery.heartbeat,这个是心跳检测的属性配置类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      @ConfigurationProperties(prefix = "spring.cloud.consul.discovery.heartbeat")
      @Data
      @CommonsLog
      @Validated
      public class HeartbeatProperties {

      // TODO: change enabled to default to true when I stop seeing messages like
      // [WARN] agent: Check 'service:testConsulApp:xtest:8080' missed TTL, is now critical
      boolean enabled = false;

      @Min(1)
      private int ttlValue = 30;

      @NotNull
      private String ttlUnit = "s";

      @DecimalMin("0.1")
      @DecimalMax("0.9")
      private double intervalRatio = 2.0 / 3.0;

      private Period heartbeatInterval;
    • ConsulDiscoveryProperties属性配置类,对应的属性前缀:spring.cloud.consul.discovery,这个是Consul的主要配置类,我们平常最常使用的

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      @ConfigurationProperties("spring.cloud.consul.discovery")
      @Data
      public class ConsulDiscoveryProperties {

      protected static final String MANAGEMENT = "management";

      @Getter(AccessLevel.PRIVATE)
      @Setter(AccessLevel.PRIVATE)
      private InetUtils.HostInfo hostInfo;

      @Value("${consul.token:${CONSUL_TOKEN:${spring.cloud.consul.token:${SPRING_CLOUD_CONSUL_TOKEN:}}}}")
      private String aclToken;

      /** Tags to use when registering service */
      private List<String> tags = new ArrayList<>();

      /** Is service discovery enabled? */
      private boolean enabled = true;

      /** Tags to use when registering management service */
      private List<String> managementTags = Arrays.asList(MANAGEMENT);

      /** Alternate server path to invoke for health checking */
      private String healthCheckPath = "/health";

      /** Custom health check url to override default */
      private String healthCheckUrl;

      /** How often to perform the health check (e.g. 10s), defaults to 10s. */
      private String healthCheckInterval = "10s";

      /** Timeout for health check (e.g. 10s). */
      private String healthCheckTimeout;

      /**
      * Timeout to deregister services critical for longer than timeout (e.g. 30m).
      * Requires consul version 7.x or higher.
      */
      private String healthCheckCriticalTimeout;

      /** IP address to use when accessing service (must also set preferIpAddress to use) */
      private String ipAddress;

      /** Hostname to use when accessing server */
      private String hostname;

      /** Port to register the service under (defaults to listening port) */
      private Integer port;

      /** Port to register the management service under (defaults to management port) */
      private Integer managementPort;

      private Lifecycle lifecycle = new Lifecycle();

      /** Use ip address rather than hostname during registration */
      private boolean preferIpAddress = false;

      /** Source of how we will determine the address to use */
      private boolean preferAgentAddress = false;

      private int catalogServicesWatchDelay = 10;

      private int catalogServicesWatchTimeout = 2;

      /** Service name */
      private String serviceName;

      /** Unique service instance id */
      private String instanceId;

      /** Service instance zone */
      private String instanceZone;

      /** Service instance group*/
      private String instanceGroup;

      /**
      * Service instance zone comes from metadata.
      * This allows changing the metadata tag name.
      */
      private String defaultZoneMetadataName = "zone";

      /** Whether to register an http or https service */
      private String scheme = "http";

      /** Suffix to use when registering management service */
      private String managementSuffix = MANAGEMENT;

      /**
      * Map of serviceId's -> tag to query for in server list.
      * This allows filtering services by a single tag.
      */
      private Map<String, String> serverListQueryTags = new HashMap<>();

      /**
      * Map of serviceId's -> datacenter to query for in server list.
      * This allows looking up services in another datacenters.
      */
      private Map<String, String> datacenters = new HashMap<>();

      /** Tag to query for in service list if one is not listed in serverListQueryTags. */
      private String defaultQueryTag;

      /**
      * Add the 'passing` parameter to /v1/health/service/serviceName.
      * This pushes health check passing to the server.
      */
      private boolean queryPassing = false;

      /** Register as a service in consul. */
      private boolean register = true;

      /** Disable automatic de-registration of service in consul. */
      private boolean deregister = true;

      /** Register health check in consul. Useful during development of a service. */
      private boolean registerHealthCheck = true;

      /**
      * Throw exceptions during service registration if true, otherwise, log
      * warnings (defaults to true).
      */
      private boolean failFast = true;

      /**
      * Skips certificate verification during service checks if true, otherwise
      * runs certificate verification.
      */
      private Boolean healthCheckTlsSkipVerify;

      @SuppressWarnings("unused")
      private ConsulDiscoveryProperties() {}

      public ConsulDiscoveryProperties(InetUtils inetUtils) {
      this.hostInfo = inetUtils.findFirstNonLoopbackHostInfo();
      this.ipAddress = this.hostInfo.getIpAddress();
      this.hostname = this.hostInfo.getHostname();
      }

      /**
      * @param serviceId The service who's filtering tag is being looked up
      * @return The tag the given service id should be filtered by, or null.
      */
      public String getQueryTagForService(String serviceId){
      String tag = serverListQueryTags.get(serviceId);
      return tag != null ? tag : defaultQueryTag;
      }

      public String getHostname() {
      return this.preferIpAddress ? this.ipAddress : this.hostname;
      }

      public void setHostname(String hostname) {
      this.hostname = hostname;
      this.hostInfo.override = true;
      }

      public void setIpAddress(String ipAddress) {
      this.ipAddress = ipAddress;
      this.hostInfo.override = true;
      }

      @Data
      public static class Lifecycle {
      private boolean enabled = true;
      }
      }

如何注册服务的

  • Bean注册完成了就要发挥作用了,我们在日志处log.info("Registering service with consul: " + reg.getService());打断点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void register(ConsulRegistration reg) {
log.info("Registering service with consul: " + reg.getService());
try {
client.agentServiceRegister(reg.getService(), properties.getAclToken());
if (heartbeatProperties.isEnabled() && ttlScheduler != null) {
ttlScheduler.add(reg.getInstanceId());
}
}
catch (ConsulException e) {
if (this.properties.isFailFast()) {
log.error("Error registering service with consul: " + reg.getService(), e);
ReflectionUtils.rethrowRuntimeException(e);
}
log.warn("Failfast is false. Error registering service with consul: " + reg.getService(), e);
}
}
  • 查看方法执行链
  • 下面来看看具体执行流程,可以看到还是老样子的refresh()方法

    • 进入org.springframework.context.support.AbstractApplicationContext#refresh()方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      /**
      * refresh()统一入口
      *
      * @throws BeansException
      * @throws IllegalStateException
      */
      @Override
      public void refresh() throws BeansException, IllegalStateException {
      synchronized (this.startupShutdownMonitor) {
      // Prepare this context for refreshing.
      // 准备刷新的上下文环境,例如对系统属性或者环境变量进行准备及验证
      prepareRefresh();

      // Tell the subclass to refresh the internal bean factory.
      // 初始化BeanFactory,并进行XML文件读取,这一步之后ClassPathXmlApplicationContext实际上就已经包含了BeanFactory所提供的功能
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

      // Prepare the bean factory for use in this context.
      // 进入prepareBeanFactory前,Spring已经完成了对配置的解析,而ApplicationContext在功能上的扩展也由此展开
      // 对BeanFactory进行各种功能组件填充 @Qualifier @Autowired这两注解功能组件就是在这步骤中增加的支持
      prepareBeanFactory(beanFactory);

      try {
      // Allows post-processing of the bean factory in context subclasses.
      // 子类覆盖方法做额外的处理
      postProcessBeanFactory(beanFactory);


      // Invoke factory processors registered as beans in the context.
      // 调用工厂后处理器 根据反射机制从BeanDefinitionRegistry中找出所有实现了BeanFactoryPostProcessor接口的bean,
      // 并调用其postProcessBeanFactory接口方法
      invokeBeanFactoryPostProcessors(beanFactory);

      // Register bean processors that intercept bean creation.
      // 注册Bean后处理器 根据反射机制从BeanDefinitionRegistry中找出所有实现了BeanPostProcessor接口的bean,
      // 并将它们注册到容器Bean后处理器的注册表中,这里只是注册,真正的调用在getBean时候
      registerBeanPostProcessors(beanFactory);

      // Initialize message source for this context.
      // 初始化消息源 初始化容器的国际化消息资源
      initMessageSource();

      // Initialize event multicaster for this context.
      // 初始化应用上下文事件广播器
      initApplicationEventMulticaster();

      // Initialize other special beans in specific context subclasses.
      // 初始化其他特殊的bean,由具体子类实现,这是个钩子方法
      onRefresh();

      // Check for listener beans and register them.
      // 注册事件监听器
      registerListeners();

      // Instantiate all remaining (non-lazy-init) singletons.
      // 重点:初始化所有单实例的Bean,使用懒加载模式的bean除外,初始化Bean后将它们放到Spring容器的缓冲池中
      finishBeanFactoryInitialization(beanFactory);

      // Last step: publish corresponding event.
      // 完成刷新并发布容器刷新事件
      finishRefresh();
      }
    • 可以看到注册中心的注册动作是放到最后一步finishRefresh()来执行的

      1
      2
      3
      4
      5
      6
      7
      8
      9
      protected void finishRefresh() {
      super.finishRefresh();
      EmbeddedServletContainer localContainer = this.startEmbeddedServletContainer();
      if (localContainer != null) {
      // 发布事件
      this.publishEvent(new EmbeddedServletContainerInitializedEvent(this, localContainer));
      }

      }
    • 我们跳入org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent()方法可以看到这里出现了ConsulAutoServiceRegistration这个类

    • 查看ConsulAutoServiceRegistration这个类,我们可以看到这个类实现了Spring的很多接口,比如继承了EventListener,这里涉及到了事件监听,还有继承了Lifecycle,可以猜测Spirng容器销毁时用这个接口来向consul注销服务,因为这个类比较复杂,所以放到下一章节详细介绍
    • 然后跳到org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistry#register)方法,这个方法打印了我们上面的日志

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      @Override
      public void register(ConsulRegistration reg) {
      log.info("Registering service with consul: " + reg.getService());
      try {
      client.agentServiceRegister(reg.getService(), properties.getAclToken());
      if (heartbeatProperties.isEnabled() && ttlScheduler != null) {
      ttlScheduler.add(reg.getInstanceId());
      }
      }
      catch (ConsulException e) {
      if (this.properties.isFailFast()) {
      log.error("Error registering service with consul: " + reg.getService(), e);
      ReflectionUtils.rethrowRuntimeException(e);
      }
      log.warn("Failfast is false. Error registering service with consul: " + reg.getService(), e);
      }
      }
    • 上面只是些执行流程,下一章节将详细介绍ConsulAutoServiceRegistration这个类

总结

  • 由于各种自动配置类的作用,SpringCloudConsul会在应用启动的时候通过RESTful HTTP APIconsul注册本地服务信息
  • 通过日志信息定位ConsulServiceRegistry类,这个类会进行注册操作,通过断点调试发现是由ConsulAutoServiceRegistration这个类来指挥的,这个类比较复杂所以放到下一章节介绍

参考