前言:
Eureka作爲一個服務註冊中心,主要功能就是服務註冊與服務發現。
微服務框架中,服務註冊與發現既是基礎功能也是核心功能。
Eureka分爲服務端和客戶端。
服務端也稱爲服務註冊中心,它同其他服務註冊中心一樣,支持高可用配置。在項目中使用@EnableEurekaServer實現即可
客戶端主要處理服務的註冊與發現,每一個服務提供者和消費者都可以稱爲客戶端。在項目中使用@EnableEurekaClient實現即可。
有關於Eureka的使用可以參考筆者另一篇文章 https://blog.csdn.net/qq_26323323/article/details/78652849
本文主要介紹Eureka作爲客戶端的應用,也就是在應用添加@EnableEurekaClient註解的應用
1.@EnableEurekaClient解析
客戶端應用通過添加@EnableEurekaClient註解,再在配置文件中添加eureka相關配置即可實現服務的註冊與發現,還是頗爲神奇的,主要功能應該都幾種在@EnableEurekaClient這個註解中,下面我們來剖析一下這個註解。
1)@EnableEurekaClient源碼如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@EnableDiscoveryClient // 主要註解就是這個
public @interface EnableEurekaClient {
}
// @EnableDiscoveryClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)// 主要就是爲了導入該類
public @interface EnableDiscoveryClient {
boolean autoRegister() default true;
}
2)EnableDiscoveryClientImportSelector.java
@Order(Ordered.LOWEST_PRECEDENCE - 100)
public class EnableDiscoveryClientImportSelector
extends SpringFactoryImportSelector<EnableDiscoveryClient> {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
// 1.核心功能在這裏,獲取需要註冊到Spring的類
String[] imports = super.selectImports(metadata);// 在3)中詳解
AnnotationAttributes attributes = AnnotationAttributes.fromMap(
metadata.getAnnotationAttributes(getAnnotationClass().getName(), true));
boolean autoRegister = attributes.getBoolean("autoRegister");
// 2.autoRegister默認爲true,同時則註冊AutoServiceRegistrationConfiguration類到Spring中
if (autoRegister) {
List<String> importsList = new ArrayList<>(Arrays.asList(imports));
importsList.add("org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration");
imports = importsList.toArray(new String[0]);
}
return imports;
}
...
}
3)super.selectImports(metadata)即在SpringFactoryImportSelector.selectImports(metadata)
public abstract class SpringFactoryImportSelector<T>
implements DeferredImportSelector, BeanClassLoaderAware, EnvironmentAware {
...
@Override
public String[] selectImports(AnnotationMetadata metadata) {
// 1.默認enabled值爲true
if (!isEnabled()) {
return new String[0];
}
...
// 2.主要功能在這裏
List<String> factories = new ArrayList<>(new LinkedHashSet<>(SpringFactoriesLoader
.loadFactoryNames(this.annotationClass, this.beanClassLoader)));
...
return factories.toArray(new String[factories.size()]);
}
// SpringFactoriesLoader.loadFactoryNames(this.annotationClass, this.beanClassLoader)
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
// 1.factoryClassName值爲org.springframework.cloud.client.discovery.EnableDiscoveryClient
String factoryClassName = factoryClass.getName();
try {
// 2.獲取所有 META-INF/spring.factories文件
Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<String>();
// 3.遍歷所有spring.factories文件
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName);
// 4.獲取properties中key爲EnableDiscoveryClient對應的value值列表
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
}
...
}
注意:org.springframework.cloud.client.discovery.EnableDiscoveryClient對應的value值,可以在spring-cloud-netflix-eureka-client-1.3.1.RELEASE-sources.jar下META-INF/spring.factories文件中獲取,具體值爲
org.springframework.cloud.client.discovery.EnableDiscoveryClient=\
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration
總結:所以上述註冊到Spring中的類爲兩個:
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration
org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration
同時我們還注意到eureka-client下META-INF/spring.factories文件中還有其他內容
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration
通過筆者的另一篇文章https://blog.csdn.net/qq_26323323/article/details/81204284 可知, EnableAutoConfiguration對應的value值列表中的類會在SpringBoot項目啓動的時候註冊到Spring容器中,EurekaClient的關鍵功能就在EurekaClientConfigServerAutoConfiguration中,下面我們一起來看下這個類
2.EurekaClientConfigServerAutoConfiguration功能解析
源碼如下:
@Configuration
@EnableConfigurationProperties
@ConditionalOnClass({ EurekaInstanceConfigBean.class, EurekaClient.class,
ConfigServerProperties.class })
public class EurekaClientConfigServerAutoConfiguration {
...
@PostConstruct
public void init() {
if (this.instance == null || this.server == null) {
return;
}
String prefix = this.server.getPrefix();
if (StringUtils.hasText(prefix)) {
this.instance.getMetadataMap().put("configPath", prefix);
}
}
}
通過該類@ConditionalOnClass註解可知,EurekaClientConfigServerAutoConfiguration類的產生需要一些條件,需要EurekaInstanceConfigBean.class, EurekaClient.class,ConfigServerProperties.class這三個類先行產生。
我們重點關注下EurekaClient.class,源碼如下:
@ImplementedBy(DiscoveryClient.class)
public interface EurekaClient extends LookupService {}
主要是一個接口,並定義了默認實現類DiscoveryClient,該接口定義了Eureka客戶端的主要功能,包括獲取服務URL、註冊當前服務等功能。
注意:這裏使用了Google-Guice框架,這是一個輕量級的DI框架。具體使用讀者可參考https://blog.csdn.net/derekjiang/article/details/7231490
3.DiscoveryClient
實際沒有絕對的服務消費者和服務提供者,每一個服務提供者也可以是一個服務消費者。
消費者和提供者的主要功能點有:服務註冊、服務續約、服務下線、服務調用
下面根據功能點來對照各自的源碼,以下方法可在com.netflix.discovery.DiscoveryClient中找到
1)服務註冊(發送註冊請求到註冊中心)
boolean register() throws Throwable {
...
EurekaHttpResponse<Void> httpResponse;
try {
// 主要的註冊功能就是這句話
// 真正實現在AbstractJerseyEurekaHttpClient.register()
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
}
...
return httpResponse.getStatusCode() == 204;
}
// AbstractJerseyEurekaHttpClient.register()
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
try {
// 1.構造一個HTTP請求
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);
// 2.封裝請求類型和返回類型,將當前服務的元信息封裝爲InstanceInfo,
// 發送post請求到serviceUrl,serviceUrl即我們在配置文件中配置的defaultZone
response = resourceBuilder
.header("Accept-Encoding", "gzip")
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
.post(ClientResponse.class, info);
// 3.返回響應狀態
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
2)服務續約(本質就是發送當前應用的心跳請求到註冊中心)
boolean renew() {
EurekaHttpResponse<InstanceInfo> httpResponse;
try {
// 1.本質就是發送心跳請求
// 2.真正實現爲 AbstractJerseyEurekaHttpClient.sendHeartBeat()
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
// 2.如果請求失敗,則調用註冊服務請求
if (httpResponse.getStatusCode() == 404) {
REREGISTER_COUNTER.increment();
logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName());
return register();
}
return httpResponse.getStatusCode() == 200;
} catch (Throwable e) {
logger.error("{} - was unable to send heartbeat!", PREFIX + appPathIdentifier, e);
return false;
}
}
// AbstractJerseyEurekaHttpClient.sendHeartBeat()
public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {
String urlPath = "apps/" + appName + '/' + id;
ClientResponse response = null;
try {
// 主要就是將當前實例的元信息(InstanceInfo)以及狀態(UP)通過HTTP請求發送到serviceUrl
WebResource webResource = jerseyClient.resource(serviceUrl)
.path(urlPath)
.queryParam("status", info.getStatus().toString())
.queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());
if (overriddenStatus != null) {
webResource = webResource.queryParam("overriddenstatus", overriddenStatus.name());
}
Builder requestBuilder = webResource.getRequestBuilder();
addExtraHeaders(requestBuilder);
response = requestBuilder.put(ClientResponse.class);
EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = anEurekaHttpResponse(response.getStatus(), InstanceInfo.class).headers(headersOf(response));
if (response.hasEntity()) {
eurekaResponseBuilder.entity(response.getEntity(InstanceInfo.class));
}
return eurekaResponseBuilder.build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP PUT {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
3.服務調用(本質就是獲取調用服務名所對應的服務提供者實例信息,包括IP、port等)
public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure) {
return getInstancesByVipAddress(vipAddress, secure, instanceRegionChecker.getLocalRegion());
}
//getInstancesByVipAddress()
public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure,
@Nullable String region) {
if (vipAddress == null) {
throw new IllegalArgumentException(
"Supplied VIP Address cannot be null");
}
Applications applications;
// 1.判斷服務提供方是否當前region,若是的話直接從localRegionApps中獲取
if (instanceRegionChecker.isLocalRegion(region)) {
applications = this.localRegionApps.get();
// 2.否則的話從遠程region獲取
} else {
applications = remoteRegionVsApps.get(region);
if (null == applications) {
logger.debug("No applications are defined for region {}, so returning an empty instance list for vip "
+ "address {}.", region, vipAddress);
return Collections.emptyList();
}
}
// 3.從applications中獲取服務名稱對應的實例名稱列表
if (!secure) {
return applications.getInstancesByVirtualHostName(vipAddress);
} else {
return applications.getInstancesBySecureVirtualHostName(vipAddress);
}
}
4)服務下線(本質就是發送取消註冊的HTTP請求到註冊中心)
void unregister() {
// It can be null if shouldRegisterWithEureka == false
if(eurekaTransport != null && eurekaTransport.registrationClient != null) {
try {
logger.info("Unregistering ...");
// 重點在這裏
EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());
logger.info(PREFIX + appPathIdentifier + " - deregister status: " + httpResponse.getStatusCode());
} catch (Exception e) {
logger.error(PREFIX + appPathIdentifier + " - de-registration failed" + e.getMessage(), e);
}
}
}
// AbstractJerseyEurekaHttpClient.cancel()
public EurekaHttpResponse<Void> cancel(String appName, String id) {
String urlPath = "apps/" + appName + '/' + id;
ClientResponse response = null;
try {
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);
// 本質就是發送delete請求到註冊中心
response = resourceBuilder.delete(ClientResponse.class);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP DELETE {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
總結:通過以上分析可知,服務的註冊、下線等操作實際上就是通過發送HTTP請求到註冊中心來實現的。那麼這些操作的執行時機是什麼時候呢?是什麼時候服務註冊操作會被調用?下線操作是如何被觸發的?這些請讀者先自行思考下,下篇文章會來解密這些
參考:SpringCloud微服務實戰