一、meta-data信息收集
When a client registers with Eureka, it provides meta-data about itself — such as host, port, health indicator URL, home page, and other details. Eureka receives heartbeat messages from each instance belonging to a service. If the heartbeat fails over a configurable timetable, the instance is normally removed from the registry.
通过EurekaClientAutoConfiguration配置类中的eurekaInstanceConfigBean方法,收集客户端的配置信息,包括host、port等。这些信息,会随着客户端进行注册时,传递给server端。
public EurekaInstanceConfigBean eurekaInstanceConfigBean(InetUtils inetUtils,
ManagementMetadataProvider managementMetadataProvider) {
String hostname = getProperty("eureka.instance.hostname");
boolean preferIpAddress = Boolean.parseBoolean(getProperty("eureka.instance.prefer-ip-address"));
String ipAddress = getProperty("eureka.instance.ip-address");
boolean isSecurePortEnabled = Boolean.parseBoolean(getProperty("eureka.instance.secure-port-enabled"));
String serverContextPath = env.getProperty("server.servlet.context-path", "/");
int serverPort = Integer.valueOf(env.getProperty("server.port", env.getProperty("port", "8080")));
Integer managementPort = env.getProperty("management.server.port", Integer.class); // nullable.
String managementContextPath = env.getProperty("management.server.servlet.context-path"); // nullable. should be wrapped into optional
Integer jmxPort = env.getProperty("com.sun.management.jmxremote.port", Integer.class); // nullable
EurekaInstanceConfigBean instance = new EurekaInstanceConfigBean(inetUtils);
instance.setNonSecurePort(serverPort);
instance.setInstanceId(getDefaultInstanceId(env));
instance.setPreferIpAddress(preferIpAddress);
instance.setSecurePortEnabled(isSecurePortEnabled);
if (StringUtils.hasText(ipAddress)) {
instance.setIpAddress(ipAddress);
}
if (isSecurePortEnabled) {
instance.setSecurePort(serverPort);
}
if (StringUtils.hasText(hostname)) {
instance.setHostname(hostname);
}
String statusPageUrlPath = getProperty("eureka.instance.status-page-url-path");
String healthCheckUrlPath = getProperty("eureka.instance.health-check-url-path");
if (StringUtils.hasText(statusPageUrlPath)) {
instance.setStatusPageUrlPath(statusPageUrlPath);
}
if (StringUtils.hasText(healthCheckUrlPath)) {
instance.setHealthCheckUrlPath(healthCheckUrlPath);
}
ManagementMetadata metadata = managementMetadataProvider.get(instance, serverPort,
serverContextPath, managementContextPath, managementPort);
if (metadata != null) {
instance.setStatusPageUrl(metadata.getStatusPageUrl());
instance.setHealthCheckUrl(metadata.getHealthCheckUrl());
if (instance.isSecurePortEnabled()) {
instance.setSecureHealthCheckUrl(metadata.getSecureHealthCheckUrl());
}
Map<String, String> metadataMap = instance.getMetadataMap();
metadataMap.computeIfAbsent("management.port", k -> String.valueOf(metadata.getManagementPort()));
}
else {
// without the metadata the status and health check URLs will not be set
// and the status page and health check url paths will not include the
// context path so set them here
if (StringUtils.hasText(managementContextPath)) {
instance.setHealthCheckUrlPath(
managementContextPath + instance.getHealthCheckUrlPath());
instance.setStatusPageUrlPath(
managementContextPath + instance.getStatusPageUrlPath());
}
}
setupJmxPort(instance, jmxPort);
return instance;
}
其中,针对于hostname、ip地址信息,内部先通过InetUtils帮助工具类获取。当外部通过eureka.instance.hostname和eureka.instance.ip-address指定是,会覆盖原获取到的数据信息。
public EurekaInstanceConfigBean(InetUtils inetUtils) {
this.inetUtils = inetUtils;
this.hostInfo = this.inetUtils.findFirstNonLoopbackHostInfo();
this.ipAddress = this.hostInfo.getIpAddress();
this.hostname = this.hostInfo.getHostname();
}
If your app wants to be contacted over HTTPS, you can set two flags in the EurekaInstanceConfig:
eureka.instance.[nonSecurePortEnabled]=[false]
eureka.instance.[securePortEnabled]=[true]
Doing so makes Eureka publish instance information that shows an explicit preference for secure communication. The Spring Cloud DiscoveryClient always returns a URI starting with https for a service configured this way. Similarly, when a service is configured this way, the Eureka (native) instance information has a secure health check URL.
在官方帮助手册中,存在如上描述,介绍如何开启Https传输。当使用https协议时,Spring会通过DefaultManagementMetadataProvider.get方法,重置healthCheckUrl以及statusPageUrl属性。
private String getUrl(EurekaInstanceConfigBean instance, int serverPort,
String serverContextPath, String managementContextPath,
Integer managementPort, String urlPath, boolean isSecure) {
managementContextPath = refineManagementContextPath(serverContextPath, managementContextPath, managementPort);
if (managementPort == null) {
managementPort = serverPort;
}
String scheme = isSecure ? "https" : "http";
return constructValidUrl(scheme, instance.getHostname(), managementPort, managementContextPath, urlPath);
}
二、DiscoveryClient创建。
在EurekaClientAutoConfiguration配置类中,存在RefreshableEurekaClientConfiguration内部类,在该内部类中,完成了EurekaClient Bean的创建。针对于CloudEurekaClient对象的构造方法,使用的4个入参,appManager, config这前两个入参,在EurekaClientAutoConfiguration配置类中,能够找到创建方法。而 context属性则为Spring上下文。其中只有optionalArgs这一个参数(对应于AbstractDiscoveryClientOptionalArgs),比较特殊,它用于控制基于何种方式,进行请求的发送与处理。这个属性的来源,在"三、服务注册"环节介绍。
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public EurekaClient eurekaClient(ApplicationInfoManager manager,
EurekaClientConfig config, EurekaInstanceConfig instance,
@Autowired(required = false) HealthCheckHandler healthCheckHandler) {
// If we use the proxy of the ApplicationInfoManager we could run into a problem
// when shutdown is called on the CloudEurekaClient where the
// ApplicationInfoManager bean is
// requested but wont be allowed because we are shutting down. To avoid this
// we use the object directly.
ApplicationInfoManager appManager;
if (AopUtils.isAopProxy(manager)) {
appManager = ProxyUtils.getTargetObject(manager);
}
else {
appManager = manager;
}
CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, this.optionalArgs, this.context);
cloudEurekaClient.registerHealthCheck(healthCheckHandler);
return cloudEurekaClient;
}
通过类图,来查看CloudEurekaClient和EurekaClient之间的关系。
在CloudEurekaClient构造方法处,先调用父类的构造方法,然后收集父类的eurekaTransport属性,通过eurekaTransport属性,可以获取其内部的EurekaHttpClient属性,用于后续的getInstanceInfo方法,关于DiscoveryClient的这两个属性,后续会有介绍。
public CloudEurekaClient(ApplicationInfoManager applicationInfoManager,
EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs<?> args,
ApplicationEventPublisher publisher) {
super(applicationInfoManager, config, args);
this.applicationInfoManager = applicationInfoManager;
this.publisher = publisher;
this.eurekaTransportField = ReflectionUtils.findField(DiscoveryClient.class, "eurekaTransport");
ReflectionUtils.makeAccessible(this.eurekaTransportField);
}
紧接着,Spring覆写了onCacheRefreshed方法,在该方法中,基于Spring的事件发布机制,增加了HeartbeatEvent事件的发布操作。
protected void onCacheRefreshed() {
super.onCacheRefreshed();
if (this.cacheRefreshedCount != null) { // might be called during construction and
// will be null
long newCount = this.cacheRefreshedCount.incrementAndGet();
log.trace("onCacheRefreshed called with count: " + newCount);
this.publisher.publishEvent(new HeartbeatEvent(this, newCount));
}
}
在EurekaClient体系中,最为核心的是DiscoveryEurekaClient对象。在该对象中,完成了EurekaHttpClient对象的创建(服务注册操作的基础)等操作。现在,我们主要介绍一下EurekaHttpClient对象的创建。
EurekaHttpClient作为EurekaTransport对象的一个属性存在,对应于registrationClient。
private static final class EurekaTransport {
private ClosableResolver bootstrapResolver;
private TransportClientFactory transportClientFactory;
private EurekaHttpClient registrationClient;
private EurekaHttpClientFactory registrationClientFactory;
private EurekaHttpClient queryClient;
private EurekaHttpClientFactory queryClientFactory;
……
}
EurekaTransport对象创建以及属性完善,通过DiscoveryEurekaClient构造方法处的如下两行代码完成。
eurekaTransport = new EurekaTransport();
scheduleServerEndpointTask(eurekaTransport, args);
其中截取与registrationClient属性有关的如下代码。
if (clientConfig.shouldRegisterWithEureka()) {
EurekaHttpClientFactory newRegistrationClientFactory = null;
EurekaHttpClient newRegistrationClient = null;
try {
newRegistrationClientFactory = EurekaHttpClients.registrationClientFactory(
eurekaTransport.bootstrapResolver,
eurekaTransport.transportClientFactory,
transportConfig
);
newRegistrationClient = newRegistrationClientFactory.newClient();
} catch (Exception e) {
logger.warn("Transport initialization failure", e);
}
eurekaTransport.registrationClientFactory = newRegistrationClientFactory;
eurekaTransport.registrationClient = newRegistrationClient;
}
在通过EurekaHttpClients.registrationClientFactory方法,跟踪到如下处理代码,发现首次创建的EurekaHttpClient对象,为SessionedEurekaHttpClient类型。真正用于完成服务注册的EurekaHttpClient对象,后续介绍。
static EurekaHttpClientFactory canonicalClientFactory(final String name,
final EurekaTransportConfig transportConfig,
final ClusterResolver<EurekaEndpoint> clusterResolver,
final TransportClientFactory transportClientFactory) {
return new EurekaHttpClientFactory() {
@Override
public EurekaHttpClient newClient() {
return new SessionedEurekaHttpClient(
name,
RetryableEurekaHttpClient.createFactory(
name,
transportConfig,
clusterResolver,
RedirectingEurekaHttpClient.createFactory(transportClientFactory),
ServerStatusEvaluators.legacyEvaluator()),
transportConfig.getSessionedClientReconnectIntervalSeconds() * 1000
);
}
@Override
public void shutdown() {
wrapClosable(clusterResolver).shutdown();
}
};
}
三、服务注册。
在DiscoveryEurekaClient中,通过register方法,完成服务注册操作。内部对应于EurekaHttpClient的register方法。
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
通过上述分析,我们找到了SessionedEurekaHttpClient这个EurekaHttpClient类。在EurekaHttpClient接口外,还存在EurekaHttpClientDecorator抽象类,在该抽象类中,针对于register方法进行了封装。后续操作,暴露了execute抽象方法。后续介绍的几个EurekaHttpClient实现类,都是通过实现execute方法,完成其注册逻辑。
@Override
public EurekaHttpResponse<Void> register(final InstanceInfo info) {
return execute(new RequestExecutor<Void>() {
@Override
public EurekaHttpResponse<Void> execute(EurekaHttpClient delegate) {
return delegate.register(info);
}
@Override
public RequestType getRequestType() {
return RequestType.Register;
}
});
}
针对于SessionedEurekaHttpClient类,其execute方法如下。可以发现,第一次进来的时候,由于eurekaHttpClientRef没有EurekaHttpClient对象,因此,真正的EurekaHttpClient对象,是通过clientFactory.newClient()方法,进行创建。
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
long now = System.currentTimeMillis();
long delay = now - lastReconnectTimeStamp;
if (delay >= currentSessionDurationMs) {
logger.debug("Ending a session and starting anew");
lastReconnectTimeStamp = now;
currentSessionDurationMs = randomizeSessionDuration(sessionDurationMs);
TransportUtils.shutdown(eurekaHttpClientRef.getAndSet(null));
}
EurekaHttpClient eurekaHttpClient = eurekaHttpClientRef.get();
if (eurekaHttpClient == null) {
eurekaHttpClient = TransportUtils.getOrSetAnotherClient(eurekaHttpClientRef, clientFactory.newClient());
}
return requestExecutor.execute(eurekaHttpClient);
}
在整套EurekaHttpClient体系中,其内部采用类似于责任链的设计模式,完成最终EurekaHttpClient对象的创建操作。即SessionedEurekaHttpClient,通过RetryableEurekaHttpClientFactory创建RetryableEurekaHttpClient,RetryableEurekaHttpClient又是通过RedirectingEurekaHttpClientFactory完成。刚开始查看代码的时候,比较绕,容易弄晕。多看几次,了解其内部处理运转逻辑,会好一点。整个体系的数据流转,在EurekaHttpClients.registrationClientFactory方法,完成指定。
static EurekaHttpClientFactory canonicalClientFactory(final String name,
final EurekaTransportConfig transportConfig,
final ClusterResolver<EurekaEndpoint> clusterResolver,
final TransportClientFactory transportClientFactory) {
return new EurekaHttpClientFactory() {
@Override
public EurekaHttpClient newClient() {
return new SessionedEurekaHttpClient(
name,
RetryableEurekaHttpClient.createFactory(
name,
transportConfig,
clusterResolver,
RedirectingEurekaHttpClient.createFactory(transportClientFactory),
ServerStatusEvaluators.legacyEvaluator()),
transportConfig.getSessionedClientReconnectIntervalSeconds() * 1000
);
}
@Override
public void shutdown() {
wrapClosable(clusterResolver).shutdown();
}
};
}
通过上述分析,可以发现,真正的注册操作所需的EurekaHttpClient,通过RedirectingEurekaHttpClient.createFactory(transportClientFactory)创建的factory中的transportClientFactory完成。那么这个属性又是怎么来的呢?
通过一步步倒退,发现该属性顶层来源于DiscoveryEurekaClient构造方法处。
TransportClientFactories argsTransportClientFactories = null;
if (args != null && args.getTransportClientFactories() != null) {
argsTransportClientFactories = args.getTransportClientFactories();
}
// Ignore the raw types warnings since the client filter interface changed between jersey 1/2
@SuppressWarnings("rawtypes")
TransportClientFactories transportClientFactories = argsTransportClientFactories == null
? new Jersey1TransportClientFactories()
: argsTransportClientFactories;
而其中的args,对应于构造方法处AbstractDiscoveryClientOptionalArgs类型的入参。现在来回答一下"二、DiscoveryClient创建"提到的AbstractDiscoveryClientOptionalArgs来源问题。
@Configuration
public class DiscoveryClientOptionalArgsConfiguration {
@Bean
@ConditionalOnMissingClass("com.sun.jersey.api.client.filter.ClientFilter")
@ConditionalOnMissingBean(value = AbstractDiscoveryClientOptionalArgs.class, search = SearchStrategy.CURRENT)
public RestTemplateDiscoveryClientOptionalArgs restTemplateDiscoveryClientOptionalArgs() {
return new RestTemplateDiscoveryClientOptionalArgs();
}
@Bean
@ConditionalOnClass(name = "com.sun.jersey.api.client.filter.ClientFilter")
@ConditionalOnMissingBean(value = AbstractDiscoveryClientOptionalArgs.class, search = SearchStrategy.CURRENT)
public MutableDiscoveryClientOptionalArgs discoveryClientOptionalArgs() {
return new MutableDiscoveryClientOptionalArgs();
}
}
通过DiscoveryClientOptionalArgsConfiguration配置类,发现,classpath下存在ClientFilter类时,使用MutableDiscoveryClientOptionalArgs,否则使用RestTemplateDiscoveryClientOptionalArgs。
现,我们通过RestTemplateDiscoveryClientOptionalArgs完成注册请求发送逻辑分析。
public class RestTemplateDiscoveryClientOptionalArgs
extends AbstractDiscoveryClientOptionalArgs<Void> {
public RestTemplateDiscoveryClientOptionalArgs() {
setTransportClientFactories(new RestTemplateTransportClientFactories());
}
}
在RestTemplateTransportClientFactories存在如下,代码块。顿时明白,Spring引入了RestTemplateTransportClientFactory以及RestTemplateEurekaHttpClient完成注册请求的发送操作。
@Override
public TransportClientFactory newTransportClientFactory(
EurekaClientConfig clientConfig, Collection<Void> additionalFilters,
InstanceInfo myInstanceInfo) {
return new RestTemplateTransportClientFactory();
}
在RestTemplateEurekaHttpClient的register方法,通过RestTemplate发送注册请求,从而完成服务注册操作。
@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = serviceUrl + "apps/" + info.getAppName();
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.ACCEPT_ENCODING, "gzip");
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
ResponseEntity<Void> response = restTemplate.exchange(urlPath, HttpMethod.POST,
new HttpEntity<>(info, headers), Void.class);
return anEurekaHttpResponse(response.getStatusCodeValue())
.headers(headersOf(response)).build();
}
到此,已完成Eureka注册分析。