导语
Eureka Client 是为了简化开发人员的开发工作,将很多的Eureka Server交互的工作进行了封装,在使用的时候自动完成,在应用的不同阶段来完成不同的功能实现。下面就来了解一下关于Eureka Client所完成的功能。
应用启动阶段
在应用启动阶段主要完成的工作有
- 1、读取跟Server进行交互的配置信息,将这些信息封装成EurekaClientConfig
- 2、读取吱声服务实例的配置信息,将其封装成EurekaInstanceConfig
- 3、从Server中获取注册信息并将这些信息缓存到本地中
- 4、服务注册
- 5、初始化心跳、缓存刷新(拉取注册表信息更新本地缓存)、按照需求注册服务、进行定时任务。
应用执行阶段
在应用的执行阶段,会完成如下的一些任务
- 1、定时发送心跳信息到Server中,这样保证注册信息正确响应
- 2、定时从Server中拉取注册信息、更新注册信息到本地缓存中
- 3、监控应用自身的信息变化,如果发生变化,需要重新发起注册
应用销毁阶段
在应用销毁阶段只完成一件事情
- 1、从Server中注销自身的服务实例
Eureka Client源码分析
读取自动配置信息
为了研究是如何运行的,可以在yml日志中做如下的配置。在使用IDEA启动的时候会在控制台打印出对应的DEBUG信息。
logging:
level:
org.springframework: debug
先来解决一个小问题
java.io.FileNotFoundException: class path resource [templates/] cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/Users/nihui/Documents/Maven/env/java/apache-maven-3.3.9/repo/org/springframework/cloud/spring-cloud-netflix-eureka-server/2.0.0.RELEASE/spring-cloud-netflix-eureka-server-2.0.0.RELEASE.jar!/templates/
at org.springframework.util.ResourceUtils.getFile(ResourceUtils.java:217) ~[spring-core-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.core.io.AbstractFileResolvingResource.getFile(AbstractFileResolvingResource.java:133) ~[spring-core-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.ui.freemarker.FreeMarkerConfigurationFactory.getTemplateLoaderForPath(FreeMarkerConfigurationFactory.java:346) [spring-context-support-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.ui.freemarker.FreeMarkerConfigurationFactory.createConfiguration(FreeMarkerConfigurationFactory.java:298) [spring-context-support-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer.afterPropertiesSet(FreeMarkerConfigurer.java:120) [spring-webmvc-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1769) [spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1706) [spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:583) [spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:502) [spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:312) [spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228) ~[spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:310) [spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) [spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:760) ~[spring-beans-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:868) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549) ~[spring-context-5.0.4.RELEASE.jar:5.0.4.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140) ~[spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) ~[spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:388) ~[spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:327) ~[spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1246) ~[spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1234) ~[spring-boot-2.0.0.RELEASE.jar:2.0.0.RELEASE]
at com.springcloud.eureka.MainApplication.main(MainApplication.java:17) ~[classes/:na]
在启动的时候居然报错了,怎么会报错呢,这问题是怎么来的呢?会发现在错误信息中有如下的一句代码
at org.springframework.ui.freemarker.FreeMarkerConfigurationFactory.getTemplateLoaderForPath(FreeMarkerConfigurationFactory.java:346) [spring-context-support-5.0.4.RELEASE.jar:5.0.4.RELEASE] 开始的时候想是不是在Resource路径下面没有对应的目录造成的,后来找到了如下的代码,在下面这个代码中,会看到当我们使用DEBUG模式运行的时候就会将异常信息打印出来。所以说这个出现在控制台是正常的。
/**
* Determine a FreeMarker TemplateLoader for the given path.
* <p>Default implementation creates either a FileTemplateLoader or
* a SpringTemplateLoader.
* @param templateLoaderPath the path to load templates from
* @return an appropriate TemplateLoader
* @see freemarker.cache.FileTemplateLoader
* @see SpringTemplateLoader
*/
protected TemplateLoader getTemplateLoaderForPath(String templateLoaderPath) {
if (isPreferFileSystemAccess()) {
// Try to load via the file system, fall back to SpringTemplateLoader
// (for hot detection of template changes, if possible).
try {
Resource path = getResourceLoader().getResource(templateLoaderPath);
File file = path.getFile(); // will fail if not resolvable in the file system
if (logger.isDebugEnabled()) {
logger.debug(
"Template loader path [" + path + "] resolved to file path [" + file.getAbsolutePath() + "]");
}
return new FileTemplateLoader(file);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Cannot resolve template loader path [" + templateLoaderPath +
"] to [java.io.File]: using SpringTemplateLoader as fallback", ex);
}
return new SpringTemplateLoader(getResourceLoader(), templateLoaderPath);
}
}
else {
// Always load via SpringTemplateLoader (without hot detection of template changes).
logger.debug("File system access not preferred: using SpringTemplateLoader");
return new SpringTemplateLoader(getResourceLoader(), templateLoaderPath);
}
解决完小问题之后先来看看在SpringBoot启动的时候都往容器中自动注入了那些关于Eureka的东西,会看到在启动自动注入了一些类而其中比较重要的就是以下的几个
- 1、EurekaClientAutoConfiguration : Eureka Client 自动配置类,主要是用来控制在SpringBoot中Eureka相关的Bean的配置和初始化
- 2、RibbonEurekaAutoConfiguration: Ribbon负责负载均衡相关的配置
- 3、EurekaDiscoveryClientConfiguration: 配置自动注册和应用健康检查器
对于SpringBoot自动配置原理了解同学可以知道,在SpringBoot自动配置原理中,很多的自动配置类都是以XXXAutoConfigration来进行配置的,按照这个思路,可以在源码中搜索EurekeClientAutoConfigration。
会发现在在这其中确实有一个叫做EurekaClientAutoConfiguration的自动配置类那么下面就来了解一下这个自动配置类。首先来分析一下在这个类开始的时候这些注解信息都是什么意思
@Configuration
@EnableConfigurationProperties
//表示在EurekaClientConfig存在的时候其作用
@ConditionalOnClass(EurekaClientConfig.class)
//表示在容器加载的时候通过Import的方式注入一些依赖
@Import(DiscoveryClientOptionalArgsConfiguration.class)
//注入Bean
@ConditionalOnBean(EurekaDiscoveryClientConfiguration.Marker.class)
//如果有eureka.client.enabled存在的时候
@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)
//自动配置之前
@AutoConfigureBefore({ NoopDiscoveryClientAutoConfiguration.class,
CommonsClientAutoConfiguration.class, ServiceRegistryAutoConfiguration.class })
//自动配置之后
@AutoConfigureAfter(name = {"org.springframework.cloud.autoconfigure.RefreshAutoConfiguration",
"org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration",
"org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration"})
public class EurekaClientAutoConfiguration {
从代码中可以知道,在自动化配置之前进行的操作是对NoopDiscoveryClientAutoConfiguration、CommonsClientAutoConfiguration、ServiceRegistryAutoConfiguration三个自动配置进行的自动配置操作。对于这些自动配置注入完成之后,进入到了EurekaClientAutoConfiguration 的自动配置。那么在EurekaClientAutoConfiguration的自动配置中都有那些内容呢?
在以DEBUG模式下除了上面那个异常值得注意以外还有一个位置需要注意
DEBUG 646 --- [ main] o.s.core.env.StandardEnvironment : Initialized StandardEnvironment with PropertySources [MapPropertySource@791412502 {name='systemProperties', properties={java.runtime.name=Java(TM) SE Runtime Environment, spring.output.ansi.enabled=always, sun.boot.library.path=/Library/Java/JavaVirtualMachines/jdk1.8.0_74.jdk/Contents/Home/jre/lib, java.vm.version=25.74-b02, gopherProxySet=false, java.vm.vendor=Oracle Corporation, java.vendor.url=http://java.oracle.com/, java.rmi.server.randomIDs=true, path.separator=:, java.vm.name=Java HotSpot(TM) 64-Bit Server VM, file.encoding.pkg=sun.io, user.country=CN, sun.java.launcher=SUN_STANDARD, sun.os.patch.level=unknown, PID=646, java.vm.specification.name=Java Virtual Machine Specification, user.dir=/Users/nihui/Documents/IDEAProject/NewSpringCloud/eureka, java.runtime.version=1.8.0_74-b02, java.awt.graphicsenv=sun.awt.CGraphicsEnvironment, java.endorsed.dirs=/Library/Java/JavaVirtualMachines/jdk1.8.0_74.jdk/Contents/Home/jre/lib/endorsed, os.arch=x86_64, java.io.tmpdir=/var/folders/j5/2623h9kj7235x5190hb1zs2w0000gn/T/, line.separator=
, java.vm.specification.vendor=Oracle Corporation, os.name=Mac OS X, sun.jnu.encoding=UTF-8, spring.beaninfo.ignore=true,
首先就是对于环境的配置
private ConfigurableEnvironment env;
public EurekaClientAutoConfiguration(ConfigurableEnvironment env) {
this.env = env;
}
注入一个Bean,DiscoveryClient是SpringCloud中提供服务发现的顶级接口,在Eureka中有其实现类
@Bean
public DiscoveryClient discoveryClient(EurekaInstanceConfig config, EurekaClient client) {
return new EurekaDiscoveryClient(config, client);
}
public interface DiscoveryClient {
//获取实现类的描述
String description();
//通过服务的ID获取服务实例的信息
List<ServiceInstance> getInstances(String serviceId);
// 获取所有的服务实例ID列表
List<String> getServices();
}
上面方法中实际返回的是一个EurekaDiscoveryClient,也就是说在容器中实际存在的是一个EurekaDiscoveryClient的对象。那么EurekaDiscoveryClient到底如何?
public EurekaDiscoveryClient(EurekaInstanceConfig config, EurekaClient eurekaClient) {
this.config = config;
this.eurekaClient = eurekaClient;
}
实现的getInstances方法如下
@Override
public List<ServiceInstance> getInstances(String serviceId) {
List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId,false);
List<ServiceInstance> instances = new ArrayList<>();
for (InstanceInfo info : infos) {
instances.add(new EurekaServiceInstance(info));
}
return instances;
}
上面提到了一个EurekaClient类在这个类在自动配置的时候就已经注入到容器中了。@ImplementedBy注解在之前提到过是Google提供的实现动态绑定的注解。而它所实现的DiscoveryClient是一个实现类。com.netflix.discovery.DiscoveryClient
org.springframework.cloud.client.discovery.DiscoveryClient
@ImplementedBy(DiscoveryClient.class)
public interface EurekaClient extends LookupService {
在SpringCloud中通过组合方式调用了Eureka中的服务发现方法。
服务发现客户端
在上面会看到在Spring Cloud中提供了一个DiscoveryClient,在Eureka中提供了一个DiscoveryClient,那么这两个到底是什么关系呢?首先我们知道在SpringCloud中的DiscoveryClient是一个接口,在Eureka中的是一个接口实现类,上面说了在应用启动的时候Eureka自动配置中注入了一个DiscoveryClient,它的功能是服务的发现。而在Eureka Client中真正起作用的是DiscoveryClient这实现类。
1、DiscoveryClient是干什么的?
DiscoveryClient是Eureka Client 的核心类,包括了与Eureka Server交互的关键逻辑主要功能如下
- 1、注册服务实例到Eureka Server中
- 2、发送心跳检测更新Server的租约
- 3、服务下线功能
- 4、查询在Eureka Server中注册的服务实例列表
2、DiscoveryClient的类图结构
从类图结构来看,其实最终实现的都是LookupService接口,而对于一个接口来说就是定义一些实现的规则。那么下面就来看看LookupService接口需要哪些实现规则。
public interface LookupService<T> {
//根据服务实例注册的appName来获取封装有相同appName的服务实例信息容器
Application getApplication(String appName);
//返回当前注册表中所有的服务实例信息
Applications getApplications();
//根据服务实例的ID获取服务实例信息
List<InstanceInfo> getInstancesById(String id);
//获取下一个服务
InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure);
}
Application是持有服务实例的信息,也可以理解为同一个服务集群信息,这些实例都挂载在同一个服务名appName下。InstanceInfo表示一个服务实例信息。当然在一个应用服务集群中会有很多的InstanceInfo实例,所以在操作的时候Application对于InstanceInfo是具有原子性的,也就是Application中的操作对于所有的InstanceInfo都是同步操作。
之前提到了EurekaClient继承了LookupService接口并且有如下的操作
public interface EurekaClient extends LookupService {
// ========================
// getters for InstanceInfo
// ========================
/**
* @param region the region that the Applications reside in
* @return an {@link com.netflix.discovery.shared.Applications} for the matching region. a Null value
* is treated as the local region.
*/
public Applications getApplicationsForARegion(@Nullable String region);
/**
* Get all applications registered with a specific eureka service.
*
* @param serviceUrl The string representation of the service url.
* @return The registry information containing all applications.
*/
public Applications getApplications(String serviceUrl);
/**
* Gets the list of instances matching the given VIP Address.
*
* @param vipAddress The VIP address to match the instances for.
* @param secure true if it is a secure vip address, false otherwise
* @return - The list of {@link InstanceInfo} objects matching the criteria
*/
public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure);
/**
* Gets the list of instances matching the given VIP Address in the passed region.
*
* @param vipAddress The VIP address to match the instances for.
* @param secure true if it is a secure vip address, false otherwise
* @param region region from which the instances are to be fetched. If <code>null</code> then local region is
* assumed.
*
* @return - The list of {@link InstanceInfo} objects matching the criteria, empty list if not instances found.
*/
public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure, @Nullable String region);
/**
* Gets the list of instances matching the given VIP Address and the given
* application name if both of them are not null. If one of them is null,
* then that criterion is completely ignored for matching instances.
*
* @param vipAddress The VIP address to match the instances for.
* @param appName The applicationName to match the instances for.
* @param secure true if it is a secure vip address, false otherwise.
* @return - The list of {@link InstanceInfo} objects matching the criteria.
*/
public List<InstanceInfo> getInstancesByVipAddressAndAppName(String vipAddress, String appName, boolean secure);
// ==========================
// getters for local metadata
// ==========================
/**
* @return in String form all regions (local + remote) that can be accessed by this client
*/
public Set<String> getAllKnownRegions();
/**
* @return the current self instance status as seen on the Eureka server.
*/
public InstanceInfo.InstanceStatus getInstanceRemoteStatus();
/**
* @deprecated see {@link com.netflix.discovery.endpoint.EndpointUtils} for replacement
*
* Get the list of all eureka service urls for the eureka client to talk to.
*
* @param zone the zone in which the client resides
* @return The list of all eureka service urls for the eureka client to talk to.
*/
@Deprecated
public List<String> getDiscoveryServiceUrls(String zone);
/**
* @deprecated see {@link com.netflix.discovery.endpoint.EndpointUtils} for replacement
*
* Get the list of all eureka service urls from properties file for the eureka client to talk to.
*
* @param instanceZone The zone in which the client resides
* @param preferSameZone true if we have to prefer the same zone as the client, false otherwise
* @return The list of all eureka service urls for the eureka client to talk to
*/
@Deprecated
public List<String> getServiceUrlsFromConfig(String instanceZone, boolean preferSameZone);
/**
* @deprecated see {@link com.netflix.discovery.endpoint.EndpointUtils} for replacement
*
* Get the list of all eureka service urls from DNS for the eureka client to
* talk to. The client picks up the service url from its zone and then fails over to
* other zones randomly. If there are multiple servers in the same zone, the client once
* again picks one randomly. This way the traffic will be distributed in the case of failures.
*
* @param instanceZone The zone in which the client resides.
* @param preferSameZone true if we have to prefer the same zone as the client, false otherwise.
* @return The list of all eureka service urls for the eureka client to talk to.
*/
@Deprecated
public List<String> getServiceUrlsFromDNS(String instanceZone, boolean preferSameZone);
// ===========================
// healthcheck related methods
// ===========================
/**
* @deprecated Use {@link #registerHealthCheck(com.netflix.appinfo.HealthCheckHandler)} instead.
*
* Register {@link HealthCheckCallback} with the eureka client.
*
* Once registered, the eureka client will invoke the
* {@link HealthCheckCallback} in intervals specified by
* {@link EurekaClientConfig#getInstanceInfoReplicationIntervalSeconds()}.
*
* @param callback app specific healthcheck.
*/
@Deprecated
public void registerHealthCheckCallback(HealthCheckCallback callback);
/**
* Register {@link HealthCheckHandler} with the eureka client.
*
* Once registered, the eureka client will first make an onDemand update of the
* registering instanceInfo by calling the newly registered healthcheck handler,
* and subsequently invoke the {@link HealthCheckHandler} in intervals specified
* by {@link EurekaClientConfig#getInstanceInfoReplicationIntervalSeconds()}.
*
* @param healthCheckHandler app specific healthcheck handler.
*/
public void registerHealthCheck(HealthCheckHandler healthCheckHandler);
/**
* Register {@link EurekaEventListener} with the eureka client.
*
* Once registered, the eureka client will invoke {@link EurekaEventListener#onEvent}
* whenever there is a change in eureka client's internal state. Use this instead of
* polling the client for changes.
*
* {@link EurekaEventListener#onEvent} is called from the context of an internal thread
* and must therefore return as quickly as possible without blocking.
*
* @param eventListener
*/
public void registerEventListener(EurekaEventListener eventListener);
/**
* Unregister a {@link EurekaEventListener} previous registered with {@link EurekaClient#registerEventListener}
* or injected into the constructor of {@link DiscoveryClient}
*
* @param eventListener
* @return True if removed otherwise false if the listener was never registered.
*/
public boolean unregisterEventListener(EurekaEventListener eventListener);
/**
* @return the current registered healthcheck handler
*/
public HealthCheckHandler getHealthCheckHandler();
// =============
// other methods
// =============
/**
* Shuts down Eureka Client. Also sends a deregistration request to the eureka server.
*/
public void shutdown();
/**
* @return the configuration of this eureka client
*/
public EurekaClientConfig getEurekaClientConfig();
/**
* @return the application info manager of this eureka client
*/
public ApplicationInfoManager getApplicationInfoManager();
}
&esmp; 会看到,EurekaClient在原有的LookupService基础上扩展了很多的方法,提供了很多的获取实例的方式,主要的扩展有如下几个方面
- 1、提供多种方式获取InstanceInfo
- 2、提供本地客户端数据
- 3、提供为客户端注册和获取健康检查处理器的能力
/**
* Register {@link HealthCheckHandler} with the eureka client.
*
* Once registered, the eureka client will first make an onDemand update of the
* registering instanceInfo by calling the newly registered healthcheck handler,
* and subsequently invoke the {@link HealthCheckHandler} in intervals specified
* by {@link EurekaClientConfig#getInstanceInfoReplicationIntervalSeconds()}.
*
* @param healthCheckHandler app specific healthcheck handler.
*/
public void registerHealthCheck(HealthCheckHandler healthCheckHandler);
/**
* Register {@link EurekaEventListener} with the eureka client.
*
* Once registered, the eureka client will invoke {@link EurekaEventListener#onEvent}
* whenever there is a change in eureka client's internal state. Use this instead of
* polling the client for changes.
*
* {@link EurekaEventListener#onEvent} is called from the context of an internal thread
* and must therefore return as quickly as possible without blocking.
*
* @param eventListener
*/
public void registerEventListener(EurekaEventListener eventListener);
Eureka Server 通过心跳机制来识别一个实例的状态,Eureka Clinet 中 在一个定时任务定时通过HealthCheckHandler检查当前Client的状态,如果Client的状态发生了变化之后就会触发新的注册事件。从而更新Eureka Server的注册表中该服务实例的相关信息
public interface HealthCheckHandler {
InstanceInfo.InstanceStatus getStatus(InstanceInfo.InstanceStatus currentStatus);
}
HealthCheckHandler代码如上所指,其实现类org.springframework.cloud.netflix.eureka.EurekaHealthCheckHandler,主要是通过org.springframework.boot.actuate.health.HealthIndicator和org.springframework.boot.actuate.health.HealthAggregator来实现对于SpringBoot应用状态的监控。
在Eureka中的事件模式属于观察者模式,事件监听器将监听Client的服务实例信息变化,从而触发对应的处理事件。
总结
上面的内容从Eureka自动配置入手,简单的分析了Eureka的与SpringCloud 的服务注册发现机制,解决了在使用DEBUG的方式进行自动配置操作中出现的问题。从上面了解到Spring Cloud 与Spring Cloud Eureka并不是同一个东西,Spring Cloud Eureka 与Eureka Client也不是同一个东西,可以说通过源码的分析收获很大,了解的观察者模式,为后期Spring与设计模式系列打下基础。