SpringCloud Netflix Eureka 源碼深入剖析(下)

、大致介紹

1、鑑於一些朋友的提問並提議講解下eureka的源碼分析,由此應運而產生的本章節的內容;2、所以我站在自我的理解角度試著整理了這篇Eureka源碼的分析,希望對大家有所幫助;3、由於篇幅太長不能在一篇裡面發佈出來,所以拆分了上下篇; 

二、基本原理

1、Eureka Server 提供服務註冊服務,各個節點啟動後,會在Eureka Server中進行註冊,這樣Eureka Server中的服務註冊表中將會存儲所有可用服務節點的信息,服務節點的信息可以在界面中直觀的看到。2、Eureka Client 是一個Java 客戶端,用於簡化與Eureka Server的交互,客戶端同時也具備一個內置的、使用輪詢負載算法的負載均衡器。3、在應用啟動後,將會向Eureka Server發送心跳(默認週期為30秒),如果Eureka Server在多個心跳週期沒有收到某個節點的心跳,Eureka Server 將會從服務註冊表中把這個服務節點移除(默認90秒)。4、Eureka Server之間將會通過複製的方式完成數據的同步;5、Eureka Client具有緩存的機制,即使所有的Eureka Server 都掛掉的話,客戶端依然可以利用緩存中的信息消費其它服務的API;

三、EurekaServer 啟動流程分析

詳見 SpringCloud(第 049 篇)Netflix Eureka 源碼深入剖析(上)

四、EurekaServer 處理服務註冊、集群數據複製

詳見 SpringCloud(第 049 篇)Netflix Eureka 源碼深入剖析(上)

五、EurekaClient 啟動流程分析

5.1 調換運行模式,Run運行 springms-discovery-eureka 服務,Debug 運行 springms-provider-user 服務,先觀察日誌先;

2017-10-23 19:43:07.688 INFO 1488 --- [ main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 02017-10-23 19:43:07.694 INFO 1488 --- [ main] o.s.c.n.eureka.InstanceInfoFactory : Setting initial instance status as: STARTING2017-10-23 19:43:07.874 INFO 1488 --- [ main] c.n.d.provider.DiscoveryJerseyProvider : Using JSON encoding codec LegacyJacksonJson2017-10-23 19:43:07.874 INFO 1488 --- [ main] c.n.d.provider.DiscoveryJerseyProvider : Using JSON decoding codec LegacyJacksonJson2017-10-23 19:43:07.971 INFO 1488 --- [ main] c.n.d.provider.DiscoveryJerseyProvider : Using XML encoding codec XStreamXml2017-10-23 19:43:07.971 INFO 1488 --- [ main] c.n.d.provider.DiscoveryJerseyProvider : Using XML decoding codec XStreamXml2017-10-23 19:43:08.134 INFO 1488 --- [ main] c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration2017-10-23 19:43:08.344 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Disable delta property : false2017-10-23 19:43:08.344 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Single vip registry refresh property : null2017-10-23 19:43:08.344 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Force full registry fetch : false2017-10-23 19:43:08.344 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Application is null : false2017-10-23 19:43:08.344 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Registered Applications size is zero : true2017-10-23 19:43:08.344 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Application version is -1: true2017-10-23 19:43:08.345 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Getting all instance registry info from the eureka server2017-10-23 19:43:08.630 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : The response status is 2002017-10-23 19:43:08.631 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Starting heartbeat executor: renew interval is: 302017-10-23 19:43:08.634 INFO 1488 --- [ main] c.n.discovery.InstanceInfoReplicator : InstanceInfoReplicator onDemand update allowed rate per min is 42017-10-23 19:43:08.637 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Discovery Client initialized at timestamp 1508758988637 with initial instances count: 02017-10-23 19:43:08.657 INFO 1488 --- [ main] c.n.e.EurekaDiscoveryClientConfiguration : Registering application springms-provider-user with eureka with status UP2017-10-23 19:43:08.658 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Saw local status change event StatusChangeEvent [timestamp=1508758988658, current=UP, previous=STARTING]2017-10-23 19:43:08.659 INFO 1488 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_SPRINGMS-PROVIDER-USER/springms-provider-user:192.168.3.101:7900: registering service...2017-10-23 19:43:08.768 INFO 1488 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 7900 (http)2017-10-23 19:43:08.768 INFO 1488 --- [ main] c.n.e.EurekaDiscoveryClientConfiguration : Updating port to 79002017-10-23 19:43:08.773 INFO 1488 --- [ main] c.s.cloud.MsProviderUserApplication : Started MsProviderUserApplication in 9.694 seconds (JVM running for 10.398)【【【【【【 用戶微服務 】】】】】】已啟動.【分析一】:根據日誌粗粒度看,大多數日誌都是在 DiscoveryClient 打印出來的,由此我們先不妨將這些打印日誌的地方都打上斷點,為了後序斷點查看調用堆棧信息。【分析二】:仔細查看下日誌,先是 DefaultLifecycleProcessor 類處理了一些 bean,然後接下來肯定會調用一些實現 SmartLifecycle 類的start 方法;【分析三】: 接著初始化設置了EurekaClient的狀態為 STARTING,初始化編碼使用的格式,哪些用JSON,哪些用XML;【分析四】: 緊接著打印了強制獲取註冊信息狀態為false,已註冊的應用大小為0,客戶端發送心跳續約,心跳續約間隔為30秒,最後打印Client初始化完成;【分析五】:帶著這些通過日誌查看出來的端倪,然後我們還得吸取分析EurekaServer的教訓,我們得先去 @EnableEurekaClient 註解瞧瞧。

5.2 有目的性的先去 MsProviderUserApplication 看看,鏈接點進 EnableEurekaClient 瞧瞧。

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@EnableDiscoveryClientpublic @interface EnableEurekaClient {}【分析一】:我們會發現,@EnableEurekaClient 註解類竟然也使用了註解 @EnableDiscoveryClient,那麼我們有必要去這個註解類看看。@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@Import(EnableDiscoveryClientImportSelector.class)public @interface EnableDiscoveryClient {}【分析二】:我們看到的是 @EnableDiscoveryClient 註解類有個比較特殊的註解 @Import,由此我們猜想,這裡的大多數邏輯是不是都寫在這個 EnableDiscoveryClientImportSelector 類呢?

5.3 進入 EnableDiscoveryClientImportSelector 看看到底做了些啥?

@Order(Ordered.LOWEST_PRECEDENCE - 100)public class EnableDiscoveryClientImportSelectorextends SpringFactoryImportSelector {@Overrideprotected boolean isEnabled() {return new RelaxedPropertyResolver(getEnvironment()).getProperty("spring.cloud.discovery.enabled", Boolean.class, Boolean.TRUE);}@Overrideprotected boolean hasDefaultFactory() {return true;}}【分析一】:EnableDiscoveryClientImportSelector 類集成了 SpringFactoryImportSelector 類,但是重寫了一個 isEnabled() 方法,默認值返回 true,為什麼會返回true,也得有個說法吧,於是我們進入父類 EnableDiscoveryClientImportSelector 看看。/*** Select and return the names of which class(es) should be imported based on* the {@link AnnotationMetadata} of the importing @{@link Configuration} class.*/@Overridepublic String[] selectImports(AnnotationMetadata metadata) {if (!isEnabled()) { // 打上斷點return new String[0];}AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(this.annotationClass.getName(), true));Assert.notNull(attributes, "No " + getSimpleName() + " attributes found. Is "+ metadata.getClassName() + " annotated with @" + getSimpleName() + "?");// Find all possible auto configuration classes, filtering duplicatesList 
factories = new ArrayList<>(new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(this.annotationClass, this.beanClassLoader)));if (factories.isEmpty() && !hasDefaultFactory()) {throw new IllegalStateException("Annotation @" + getSimpleName()+ " found, but there are no implementations. Did you forget to include a starter?");}if (factories.size() > 1) {// there should only ever be one DiscoveryClient, but there might be more than// one factorylog.warn("More than one implementation " + "of @" + getSimpleName()+ " (now relying on @Conditionals to pick one): " + factories);}return factories.toArray(new String[factories.size()]);}【分析二】:發現父類有這麼一個 selectImports 方法使用了 isEnabled() 方法,這個方法幹了些啥事情呢?我們細看下 selectImports 方法上面的英文註釋,大致意思是:選擇並且返回需要導入經過註解配置的類,由此我們猜想這個導入的類肯定對我們此次客戶端分析有莫大的幫助,於是我們現在這個方法打上斷點先。於是我們現在該乾的事情也幹了,沒有頭緒的時候,我們現在才Run運行EurekaServer,Debug運行springms-provider-user。

5.4 EnableDiscoveryClientImportSelector.selectImports 這個方法果然進斷點了。

【分析一】:既然進了斷點,我們看看這個方法,首先通過註解獲取了一些屬性,然後加載了一些類名稱,於是我們進入 loadFactoryNames 方法看看。public static List loadFactoryNames(Class> factoryClass, ClassLoader classLoader) {String factoryClassName = factoryClass.getName();try {// 註釋:public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";// 註釋:這個 jar 包下的一個配置文件Enumeration urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));List result = new ArrayList();while (urls.hasMoreElements()) {URL url = urls.nextElement();Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));String factoryClassNames = properties.getProperty(factoryClassName);result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));}return result;}catch (IOException ex) {throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +"] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);}}【分析二】:加載了一個配置文件,配置文件裡面寫了啥呢?打開SpringFactoryImportSelector該文件所在的jar包的spring.factories文件一看。# AutoConfigurationorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframework.cloud.client.CommonsClientAutoConfiguration,\org.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration,\org.springframework.cloud.client.hypermedia.CloudHypermediaAutoConfiguration,\org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration,\org.springframework.cloud.commons.util.UtilAutoConfiguration# Environment Post Processorsorg.springframework.boot.env.EnvironmentPostProcessor=\org.springframework.cloud.client.HostInfoEnvironmentPostProcessor【分析三】:看名稱,都是一些 Configuration 後綴的類名,所以這些都是加載的一堆堆的配置文件類。於是我們繼續斷點往下走,發現factories 對象裡面只有一個類名路徑為 org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration 。看這個名字就應該知道這是我們分析EurekaClient的一個重要的配置類,先不管三七二十一,找到該類先。 

5.5 進入 EurekaDiscoveryClientConfiguration 看看,這個配置類有哪些重要的方法?

@Configuration@EnableConfigurationProperties@ConditionalOnClass(EurekaClientConfig.class)@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)@CommonsLogpublic class EurekaDiscoveryClientConfiguration implements SmartLifecycle, Ordered {@Overridepublic void start() {// only set the port if the nonSecurePort is 0 and this.port != 0if (this.port.get() != 0 && this.instanceConfig.getNonSecurePort() == 0) {this.instanceConfig.setNonSecurePort(this.port.get());}// only initialize if nonSecurePort is greater than 0 and it isn't already running// because of containerPortInitializer belowif (!this.running.get() && this.instanceConfig.getNonSecurePort() > 0) {maybeInitializeClient();if (log.isInfoEnabled()) {log.info("Registering application " + this.instanceConfig.getAppname()+ " with eureka with status "+ this.instanceConfig.getInitialStatus());}this.applicationInfoManager.setInstanceStatus(this.instanceConfig.getInitialStatus());if (this.healthCheckHandler != null) {this.eurekaClient.registerHealthCheck(this.healthCheckHandler);}this.context.publishEvent(new InstanceRegisteredEvent<>(this, this.instanceConfig));this.running.set(true);}}。。。 其它省略了}【分析一】:進入這個類,首先看到該類實現了 SmartLifecycle 接口,那麼就肯定會實現 start 方法,而且這個 start 方法感覺應在 “步驟5.1之分析二” 會被加載執行的。【分析二】:因為 start 這段代碼不多,所以我就索性將 start 方法中的每段代碼都點進去看了看,發現 this.applicationInfoManager.setInstanceStatus(this.instanceConfig.getInitialStatus()) 這段代碼有一個觀察者模式的回調存在。// ApplicationInfoManager.setInstanceStatus 的方法public synchronized void setInstanceStatus(InstanceStatus status) {// 打上斷點InstanceStatus prev = instanceInfo.setStatus(status); if (prev != null) { for (StatusChangeListener listener : listeners.values()) { try {listener.notify(new StatusChangeEvent(prev, status));} catch (Exception e) {logger.warn("failed to notify listener: {}", listener.getId(), e);}}}}【分析三】:這個方法會因為狀態的改變而回調所有實現 StatusChangeListener 這個類的地方,前提得先註冊到 listeners 中去才行。【分析四】:於是乎,我們斷定,若想要回調,那麼就必須有地方先註冊這個事件,而且這個註冊還必須提前執行在 start 方法前執行,於是我們得先在 ApplicationInfoManager 這個類中找到註冊到 listeners 的這個方法。public void registerStatusChangeListener(StatusChangeListener listener) {// 打上斷點listeners.put(listener.getId(), listener);}【分析五】:沒錯,就是這個方法,肯定有地方調用這個方法,不然的話,那調用 setInstanceStatus 這個方法的意義就什麼用了。於是我們逆向找下 registerStatusChangeListener 被調用的地方。【分析六】:很不巧的是,盡然只有1個地方被調用,這個地方就是 DiscoveryClient.initScheduledTasks 方法,而且 initScheduledTasks方法又是在 DiscoveryClient 的構造函數里面調用的,同時我們也對 initScheduledTasks 以及 initScheduledTasks 被調用的構造方法地方打上斷點。

5.6 由於翻閱代碼時間有點久了,因此我們關閉 springms-provider-user 微服務,重新 Debug 運行一下。

【分析一】:果不其然,EurekaDiscoveryClientConfiguration.start 方法被調用了,緊接著 this.applicationInfoManager.setInstanceStatus(this.instanceConfig.getInitialStatus()) 也進入斷點,然後在往下走,又進入的DiscoveryClient.initScheduledTasks 方法中的 notify 回調處。【分析二】:看著斷點依次經過我們上述分析的地方,然後也符合日誌打印的順序,所以我們現在應該是有必要好好看看 DiscoveryClient.initScheduledTasks 這個方法究竟幹了什麼偉大的事情。然而又想了想,還不如看看 initScheduledTasks 被調用的構造方法。

5.7 進入 DiscoveryClient 經過 @Inject 註解過的構造方法。

@InjectDiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, DiscoveryClientOptionalArgs args, Provider backupRegistryProvider) { if (args != null) { this.healthCheckHandlerProvider = args.healthCheckHandlerProvider; this.healthCheckCallbackProvider = args.healthCheckCallbackProvider; this.eventListeners.addAll(args.getEventListeners());} else { this.healthCheckCallbackProvider = null; this.healthCheckHandlerProvider = null;} this.applicationInfoManager = applicationInfoManager;InstanceInfo myInfo = applicationInfoManager.getInfo();clientConfig = config;staticClientConfig = clientConfig;transportConfig = config.getTransportConfig();instanceInfo = myInfo; if (myInfo != null) {appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();} else {logger.warn("Setting instanceInfo to a passed in null value");} this.backupRegistryProvider = backupRegistryProvider; this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);localRegionApps.set(new Applications());fetchRegistryGeneration = new AtomicLong(0);remoteRegionsToFetch = new AtomicReference(clientConfig.fetchRegistryForRemoteRegions());remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(",")); if (config.shouldFetchRegistry()) { this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});} else { this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;} if (config.shouldRegisterWithEureka()) { this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});} else { this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;} if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {logger.info("Client configured to neither register nor query for data.");scheduler = null;heartbeatExecutor = null;cacheRefreshExecutor = null;eurekaTransport = null;instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion()); // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()// to work with DI'd DiscoveryClientDiscoveryManager.getInstance().setDiscoveryClient(this);DiscoveryManager.getInstance().setEurekaClientConfig(config);initTimestampMs = System.currentTimeMillis();logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",initTimestampMs, this.getApplications().size()); return; // no need to setup up an network tasks and we are done} try {// 註釋:定時任務調度準備scheduler = Executors.newScheduledThreadPool(3, new ThreadFactoryBuilder().setNameFormat("DiscoveryClient-%d").setDaemon(true).build());// 註釋:實例化心跳定時任務線程池heartbeatExecutor = new ThreadPoolExecutor( 1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS, new SynchronousQueue(), new ThreadFactoryBuilder().setNameFormat("DiscoveryClient-HeartbeatExecutor-%d").setDaemon(true).build()); // use direct handoff// 註釋:實例化緩存刷新定時任務線程池cacheRefreshExecutor = new ThreadPoolExecutor( 1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS, new SynchronousQueue(), new ThreadFactoryBuilder().setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d").setDaemon(true).build()); // use direct handoffeurekaTransport = new EurekaTransport();scheduleServerEndpointTask(eurekaTransport, args);AzToRegionMapper azToRegionMapper; if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);} else {azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);} if (null != remoteRegionsToFetch.get()) {azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(","));}instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion());} catch (Throwable e) { throw new RuntimeException("Failed to initialize DiscoveryClient!", e);} if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {fetchRegistryFromBackup();}// 註釋:初始化調度任務initScheduledTasks(); try {Monitors.registerObject(this);} catch (Throwable e) {logger.warn("Cannot register timers", e);} // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()// to work with DI'd DiscoveryClientDiscoveryManager.getInstance().setDiscoveryClient(this);DiscoveryManager.getInstance().setEurekaClientConfig(config);initTimestampMs = System.currentTimeMillis();logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",initTimestampMs, this.getApplications().size());}【分析一】:從往下看,initScheduledTasks 這個方法顧名思義就是初始化調度任務,所以這裡面的內容應該就是重頭戲,進入看看。private void initScheduledTasks() { if (clientConfig.shouldFetchRegistry()) { // registry cache refresh timer// 註釋:間隔多久去拉取服務註冊信息,默認時間 30秒int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds(); int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();// 註釋:定時任務,每間隔 30秒 去拉取一次服務註冊信息scheduler.schedule( new TimedSupervisorTask( "cacheRefresh",scheduler,cacheRefreshExecutor,registryFetchIntervalSeconds,TimeUnit.SECONDS,expBackOffBound, new CacheRefreshThread()),registryFetchIntervalSeconds, TimeUnit.SECONDS);} if (clientConfig.shouldRegisterWithEureka()) {// 註釋:間隔多久發送一次心跳續約,默認間隔時間 30 秒int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs(); int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs); // Heartbeat timer// 註釋:定時任務,每間隔 30秒 去想 EurekaServer 發送一次心跳續約scheduler.schedule( new TimedSupervisorTask( "heartbeat",scheduler,heartbeatExecutor,renewalIntervalInSecs,TimeUnit.SECONDS,expBackOffBound, new HeartbeatThread()),renewalIntervalInSecs, TimeUnit.SECONDS); // InstanceInfo replicator// 註釋:實例信息複製器,定時刷新dataCenterInfo數據中心信息,默認30秒instanceInfoReplicator = new InstanceInfoReplicator( this,instanceInfo,clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2); // burstSize// 註釋:實例化狀態變化監聽器statusChangeListener = new ApplicationInfoManager.StatusChangeListener() { @Overridepublic String getId() { return "statusChangeListener";} @Overridepublic void notify(StatusChangeEvent statusChangeEvent) { if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) { // log at warn level if DOWN was involvedlogger.warn("Saw local status change event {}", statusChangeEvent);} else {logger.info("Saw local status change event {}", statusChangeEvent);}// 註釋:狀態有變化的話,會回調這個方法instanceInfoReplicator.onDemandUpdate();}};// 註釋:註冊狀態變化監聽器if (clientConfig.shouldOnDemandUpdateStatusChange()) {applicationInfoManager.registerStatusChangeListener(statusChangeListener);}instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());} else {logger.info("Not registering with Eureka server per configuration");}}【分析二】:在這個方法從上往下一路註釋分析下來,幹了EurekaClient我們最想知道的一些事情,定時任務獲取註冊信息,定時任務刷新緩存,定時任務心跳續約,定時任務同步數據中心數據,狀態變化監聽回調等。但是唯獨沒看到註冊,這是怎麼回事呢?【分析三】:我們忘記了一個重要的方法,instanceInfoReplicator.onDemandUpdate() 就是在狀態改變的時候,我們是如何處理的?由此,我們覺得這裡面肯定有貓膩,不然沒辦法註冊呀。public boolean onDemandUpdate() { if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {scheduler.submit(new Runnable() { @Overridepublic void run() {logger.debug("Executing on-demand update of local InstanceInfo");Future latestPeriodic = scheduledPeriodicRef.get(); if (latestPeriodic != null && !latestPeriodic.isDone()) {logger.debug("Canceling the latest scheduled update, it will be rescheduled at the end of on demand update");latestPeriodic.cancel(false);}// 註釋:這裡進行了實例信息刷新和註冊InstanceInfoReplicator.this.run();}}); return true;} else {logger.warn("Ignoring onDemand update due to rate limiter"); return false;}}【分析四】:onDemandUpdate 這個方法,看來看去,唯獨 InstanceInfoReplicator.this.run() 這個方法還有點用,而且還是 run 方法呢,感情 InstanceInfoReplicator 這個類還是實現了 Runnable 接口?經過查看這個類,還真是實現了 Runnable 接口。【分析五】:於是乎,我們有理由相信,這個方法應該我們要找的註冊所在的地方,翻開代碼看看究竟。public void run() { try {discoveryClient.refreshInstanceInfo();Long dirtyTimestamp = instanceInfo.isDirtyWithTime(); if (dirtyTimestamp != null) {discoveryClient.register();instanceInfo.unsetIsDirty(dirtyTimestamp);}} catch (Throwable t) {logger.warn("There was a problem with the instance info replicator", t);} finally {Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);scheduledPeriodicRef.set(next);}}【分析六】:映入眼簾的就是 discoveryClient.register() 這個刺眼的 register 方法,終於有點苗頭了,原來註冊方法找的這麼千辛萬苦。雖然找到了這裡,但是我還是想看看這個讓我們找的千辛萬苦的註冊方法到底是怎麼註冊的呢?boolean register() throws Throwable {logger.info(PREFIX + appPathIdentifier + ": registering service...");EurekaHttpResponse 
httpResponse; try {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;}【分析七】:原來調用了 EurekaHttpClient 封裝的客戶端請求對象來進行註冊的,再繼續深探 registrationClient.register 方法,於是我們來到了 AbstractJerseyEurekaHttpClient.register 方法。@Overridepublic EurekaHttpResponse register(InstanceInfo info) {String urlPath = "apps/" + info.getAppName();ClientResponse response = null; try {Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();addExtraHeaders(resourceBuilder);response = resourceBuilder.header("Accept-Encoding", "gzip").type(MediaType.APPLICATION_JSON_TYPE).accept(MediaType.APPLICATION_JSON)// 註釋:打包帶上當前應用的所有信息 info.post(ClientResponse.class, info); 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();}}}【分析八】:原來調用的是 Jersey RESTful 框架來進行請求的,然後在 EurekaServer 那邊就會在 ApplicationResource.addInstance 方法接收客戶端的註冊請求,因此我們的 EurekaClient 是如何註冊的就到此為止了。【分析九】:至於那些續約、心跳的流程分析和這個註冊的流程大體差不多,相信大家按照我剛剛這麼分析斷點下去,一定能分析的很到位的。


分享到:


相關文章: