【SpringCloud 技術專題】「Eureka 源碼分析」從源碼層面讓你認識 Eureka 工作流程和運作機制(下)

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"承接上文的對應的Eureka的上篇介紹,我們開始介紹,詳見 [【SpringCloud技術專題】「Eureka源碼分析」從源碼層面讓你認識Eureka工作流程和運作機制(上)]","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"原理回顧","attrs":{}}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"Eureka Server 提供服務註冊服務,各個節點啓動後,會在Eureka Server中進行註冊,這樣Eureka Server中的服務註冊表中將會存儲所有可用服務節點的信息,服務節點的信息可以在界面中直觀的看到。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"Eureka Client 是一個Java 客戶端,用於簡化與Eureka Server的交互,客戶端同時也具備一個內置的、使用輪詢負載算法的負載均衡器。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"在應用啓動後,將會向Eureka Server發送心跳(默認週期爲30秒),如果Eureka Server在多個心跳週期(默認3個心跳週期=90秒)沒有收到某個節點的心跳,Eureka Server將會從服務註冊表中把這個服務節點移除。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"高可用情況下的:Eureka Server之間將會通過複製的方式完成數據的同步;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"Eureka Client具有緩存的機制,即使所有的Eureka Server 都掛掉的話,客戶端依然可以利用緩存中的信息消費其它服務的API;","attrs":{}}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"EurekaServer 啓動流程分析","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"EurekaServer 處理服務註冊、集羣數據複製","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"EurekaClient 是如何註冊到 EurekaServer 的?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"剛纔在org.springframework.cloud.netflix.eureka.server.InstanceRegistry 的每個方法都打了一個斷點,而且現在EurekaServer已經處於Debug運行狀態,那麼我們就隨便找一個被 @EnableEurekaClient 的微服務啓動試試微服務來試試吧,直接Run。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當啓動後,就一定會調用註冊register方法,那麼就接着往下看,拭目以待;","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實例註冊方法機制","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"InstanceRegistry.register(final InstanceInfo info, final boolean isReplication) 方法進斷點了。\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"InstanceRegistry.register順着堆棧信息往上看,是 ApplicationResource.addInstance 方法被調用了,分析addInstance;","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"ApplicationResource 類","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主要是處理接收 Http 的服務請求。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@POST\[email protected]({\"application/json\", \"application/xml\"})\npublic Response addInstance(InstanceInfo info,\n @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {\n logger.debug(\"Registering instance {} (replication={})\", info.getId(), isReplication);\n // validate that the instanceinfo contains all the necessary required fields\n if (isBlank(info.getId())) {\n return Response.status(400).entity(\"Missing instanceId\").build();\n } else if (isBlank(info.getHostName())) {\n return Response.status(400).entity(\"Missing hostname\").build();\n } else if (isBlank(info.getAppName())) {\n return Response.status(400).entity(\"Missing appName\").build();\n } else if (!appName.equals(info.getAppName())) {\n return Response.status(400).entity(\"Mismatched appName, expecting \" + appName + \" but was \" + info.getAppName()).build();\n } else if (info.getDataCenterInfo() == null) {\n return Response.status(400).entity(\"Missing dataCenterInfo\").build();\n } else if (info.getDataCenterInfo().getName() == null) {\n return Response.status(400).entity(\"Missing dataCenterInfo Name\").build();\n }\n\n // handle cases where clients may be registering with bad DataCenterInfo with missing data\n DataCenterInfo dataCenterInfo = info.getDataCenterInfo();\n if (dataCenterInfo instanceof UniqueIdentifier) {\n String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId();\n if (isBlank(dataCenterInfoId)) {\n boolean experimental = \"true\".equalsIgnoreCase(serverConfig.getExperimental(\"registration.validation.dataCenterInfoId\"));\n if (experimental) {\n String entity = \"DataCenterInfo of type \" + dataCenterInfo.getClass() + \" must contain a valid id\";\n return Response.status(400).entity(entity).build();\n } else if (dataCenterInfo instanceof AmazonInfo) {\n AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo;\n String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId);\n if (effectiveId == null) {\n amazonInfo.getMetadata().put(AmazonInfo.MetaDataKey.instanceId.getName(), info.getId());\n }\n } else {\n logger.warn(\"Registering DataCenterInfo of type {} without an appropriate id\", dataCenterInfo.getClass());\n }\n }\n }\n registry.register(info, \"true\".equals(isReplication));\n return Response.status(204).build(); // 204 to be backwards compatible\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏的寫法貌似看起來和我們之前 Controller 的 RESTFUL 寫法有點不一樣,仔細一看,原來是Jersey RESTful 框架,是一個產品級的RESTful service 和 client 框架。與Struts類似,它同樣可以和hibernate,spring框架整合。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看到 registry.register(info, \"true\".equals(isReplication)); 註冊啊,原來EurekaClient客戶端啓動後會調用會通過Http(s)請求,直接調到ApplicationResource.addInstance 方法,只要是和註冊有關的,都會調用這個方法。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接着我們深入 registry.register(info, \"true\".equals(isReplication)) 查看;","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Override\npublic void register(final InstanceInfo info, final boolean isReplication) {\n handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);\n super.register(info, isReplication);\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication) 方法;","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"private void handleRegistration(InstanceInfo info, int leaseDuration,\n boolean isReplication) {\n log(\"register \" + info.getAppName() + \", vip \" + info.getVIPAddress()\n + \", leaseDuration \" + leaseDuration + \", isReplication \"\n + isReplication);\n publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration,\n isReplication));\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後通過 ApplicationContext 發佈了一個事件 EurekaInstanceRegisteredEvent 服務註冊事件,可以給 EurekaInstanceRegisteredEvent 添加監聽事件,那麼用戶就可以在此刻實現自己想要的一些業務邏輯。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後我們再來看看 super.register(info, isReplication) 方法,該方法是 InstanceRegistry 的父類 PeerAwareInstanceRegistryImpl 的方法。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"服務戶廁機制","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進入PeerAwareInstanceRegistryImpl 類的 register(final InstanceInfo info, final boolean isReplication) 方法;","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Override\npublic void register(final InstanceInfo info, final boolean isReplication) {\n // 註釋:續約時間,默認時間是常量值 90 秒\n int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;\n // 註釋:續約時間,當然也可以從配置文件中取出來,所以說續約時間值也是可以讓我們自己自定義配置的\n if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {\n leaseDuration = info.getLeaseInfo().getDurationInSecs();\n }\n // 註釋:將註冊方的信息寫入 EurekaServer 的註冊表,父類爲 AbstractInstanceRegistry\n super.register(info, leaseDuration, isReplication);\n // 註釋:EurekaServer 節點之間的數據同步,複製到其他Peer\n replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"進入super.register(info, leaseDuration, isReplication),如何寫入EurekaServer 的註冊表的,進入AbstractInstanceRegistry.register(InstanceInfo registrant, int leaseDuration, boolean isReplication) 方法。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {\n try {\n read.lock();\n // 註釋:registry 這個變量,就是我們所謂的註冊表,註冊表是保存在內存中的;\n Map> gMap = registry.get(registrant.getAppName());\n REGISTER.increment(isReplication);\n if (gMap == null) {\n final ConcurrentHashMap> gNewMap = new ConcurrentHashMap>();\n gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);\n if (gMap == null) {\n gMap = gNewMap;\n }\n }\n Lease existingLease = gMap.get(registrant.getId());\n // Retain the last dirty timestamp without overwriting it, if there is already a lease\n if (existingLease != null && (existingLease.getHolder() != null)) {\n Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();\n Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();\n logger.debug(\"Existing lease found (existing={}, provided={}\", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);\n if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {\n logger.warn(\"There is an existing lease and the existing lease's dirty timestamp {} is greater\" +\n \" than the one that is being registered {}\", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);\n logger.warn(\"Using the existing instanceInfo instead of the new instanceInfo as the registrant\");\n registrant = existingLease.getHolder();\n }\n } else {\n // The lease does not exist and hence it is a new registration\n synchronized (lock) {\n if (this.expectedNumberOfRenewsPerMin > 0) {\n // Since the client wants to cancel it, reduce the threshold\n // (1\n // for 30 seconds, 2 for a minute)\n this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;\n this.numberOfRenewsPerMinThreshold =\n (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());\n }\n }\n logger.debug(\"No previous lease information found; it is new registration\");\n }\n Lease lease = new Lease(registrant, leaseDuration);\n if (existingLease != null) {\n lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());\n }\n gMap.put(registrant.getId(), lease);\n synchronized (recentRegisteredQueue) {\n recentRegisteredQueue.add(new Pair(\n System.currentTimeMillis(),\n registrant.getAppName() + \"(\" + registrant.getId() + \")\"));\n }\n // This is where the initial state transfer of overridden status happens\n if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {\n logger.debug(\"Found overridden status {} for instance {}. Checking to see if needs to be add to the \"\n + \"overrides\", registrant.getOverriddenStatus(), registrant.getId());\n if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {\n logger.info(\"Not found overridden id {} and hence adding it\", registrant.getId());\n overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());\n }\n }\n InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());\n if (overriddenStatusFromMap != null) {\n logger.info(\"Storing overridden status {} from map\", overriddenStatusFromMap);\n registrant.setOverriddenStatus(overriddenStatusFromMap);\n }\n\n // Set the status based on the overridden status rules\n InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);\n registrant.setStatusWithoutDirty(overriddenInstanceStatus);\n\n // If the lease is registered with UP status, set lease service up timestamp\n if (InstanceStatus.UP.equals(registrant.getStatus())) {\n lease.serviceUp();\n }\n registrant.setActionType(ActionType.ADDED);\n recentlyChangedQueue.add(new RecentlyChangedItem(lease));\n registrant.setLastUpdatedTimestamp();\n invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());\n logger.info(\"Registered instance {}/{} with status {} (replication={})\",\n registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);\n } finally {\n read.unlock();\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"發現這個方法有點長,大致閱讀,主要更新了註冊表的時間之外,還更新了緩存等其它東西,大家有興趣的可以深究閱讀該方法;","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"集羣之間的複製","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication) 的這個方法。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"private void replicateToPeers(Action action, String appName, String id,\n InstanceInfo info /* optional */,\n InstanceStatus newStatus /* optional */, boolean isReplication) {\n Stopwatch tracer = action.getTimer().start();\n try {\n if (isReplication) {\n numberOfReplicationsLastMin.increment();\n }\n // If it is a replication already, do not replicate again as this will create a poison replication\n // 註釋:如果已經複製過,就不再複製 \n if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {\n return;\n }\n // 遍歷Eureka Server集羣中的所有節點,進行復制操作 \n for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {\n // If the url represents this host, do not replicate to yourself.\n if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {\n continue;\n }\n // 沒有複製過,遍歷Eureka Server集羣中的node節點,依次操作,包括取消、註冊、心跳、狀態更新等。\n replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);\n }\n } finally {\n tracer.stop();\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每當有註冊請求,首先更新 EurekaServer 的註冊表,然後再將信息同步到其它EurekaServer的節點上去;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們看看 node 節點是如何進行復制操作的,進入 replicateInstanceActionsToPeers 方法。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"private void replicateInstanceActionsToPeers(Action action, String appName,\n String id, InstanceInfo info, InstanceStatus newStatus,\n PeerEurekaNode node) {\n try {\n InstanceInfo infoFromRegistry = null;\n CurrentRequestVersion.set(Version.V2);\n switch (action) {\n case Cancel:\n node.cancel(appName, id);\n break;\n case Heartbeat:\n InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);\n infoFromRegistry = getInstanceByAppAndId(appName, id, false);\n node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);\n break;\n case Register:\n node.register(info);\n break;\n case StatusUpdate:\n infoFromRegistry = getInstanceByAppAndId(appName, id, false);\n node.statusUpdate(appName, id, newStatus, infoFromRegistry);\n break;\n case DeleteStatusOverride:\n infoFromRegistry = getInstanceByAppAndId(appName, id, false);\n node.deleteStatusOverride(appName, id, infoFromRegistry);\n break;\n }\n } catch (Throwable t) {\n logger.error(\"Cannot replicate information to {} for action {}\", node.getServiceUrl(), action.name(), t);\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"節點之間的複製狀態操作,都在這裏體現的淋漓盡致,那麼我們就拿 Register 類型 node.register(info) 來看,我們來看看 node 究竟是如何做到同步信息的,進入 node.register(info) 方法看看;","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"同級之間的複製機制","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PeerEurekaNode.register(final InstanceInfo info) 方法,一窺究竟如何同步數據。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"public void register(final InstanceInfo info) throws Exception {\n // 註釋:任務過期時間給任務分發器處理,默認時間偏移當前時間 30秒\n long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);\n batchingDispatcher.process(\n taskId(\"register\", info),\n new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {\n public EurekaHttpResponse execute() {\n return replicationClient.register(info);\n }\n },\n expiryTime\n );\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏涉及到了 Eureka 的任務批處理,通常情況下Peer之間的同步需要調用多次,如果EurekaServer一多的話,那麼將會有很多http請求,所以自然而然的孕育出了任務批處理,但是也在一定程度上導致了註冊和下線的一些延遲,突出優勢的同時也勢必會造成一些劣勢,但是這些延遲情況還是能符合常理在容忍範圍之內的。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 expiryTime 超時時間之內,批次處理要做的事情就是合併任務爲一個List,然後發送請求的時候,將這個批次List直接打包發送請求出去,這樣的話,在這個批次的List裏面,可能包含取消、註冊、心跳、狀態等一系列狀態的集合List。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們再接着看源碼,batchingDispatcher.process 這麼一調用,然後我們就直接看這個 TaskDispatchers.createBatchingTaskDispatcher 方法。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public static TaskDispatcher createBatchingTaskDispatcher(String id,\n int maxBufferSize,\n int workloadSize,\n int workerCount,\n long maxBatchingDelay,\n long congestionRetryDelayMs,\n long networkFailureRetryMs,\n TaskProcessor taskProcessor) {\n final AcceptorExecutor acceptorExecutor = new AcceptorExecutor<>(\n id, maxBufferSize, workloadSize, maxBatchingDelay, congestionRetryDelayMs, networkFailureRetryMs\n );\n final TaskExecutors taskExecutor = TaskExecutors.batchExecutors(id, workerCount, taskProcessor, acceptorExecutor);\n return new TaskDispatcher() {\n @Override\n public void process(ID id, T task, long expiryTime) {\n acceptorExecutor.process(id, task, expiryTime);\n }\n\n @Override\n public void shutdown() {\n acceptorExecutor.shutdown();\n taskExecutor.shutdown();\n }\n };\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏的 process 方法會將任務添加到隊列中,有入隊列自然有出隊列,具體怎麼取任務,我就不一一給大家講解了,我就講講最後是怎麼觸發任務的。進入 final TaskExecutors taskExecutor = TaskExecutors.batchExecutors(id, workerCount, taskProcessor, acceptorExecutor) 這句代碼的 TaskExecutors.batchExecutors 方法。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"static TaskExecutors batchExecutors(final String name,\n int workerCount,\n final TaskProcessor processor,\n final AcceptorExecutor acceptorExecutor) {\n final AtomicBoolean isShutdown = new AtomicBoolean();\n final TaskExecutorMetrics metrics = new TaskExecutorMetrics(name);\n return new TaskExecutors<>(new WorkerRunnableFactory() {\n @Override\n public WorkerRunnable create(int idx) {\n return new BatchWorkerRunnable<>(\"TaskBatchingWorker-\" +name + '-' + idx, isShutdown, metrics, processor, acceptorExecutor);\n }\n }, workerCount, isShutdown);\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們發現 TaskExecutors 類中的 batchExecutors 這個靜態方法,有個 BatchWorkerRunnable 返回的實現類,因此我們再次進入 BatchWorkerRunnable 類看看究竟,而且既然是 Runnable,那麼勢必會有 run 方法。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Override\npublic void run() {\n try {\n while (!isShutdown.get()) {\n // 註釋:獲取信號量釋放 batchWorkRequests.release(),返回任務集合列表\n List> holders = getWork();\n metrics.registerExpiryTimes(holders);\n\n List tasks = getTasksOf(holders);\n // 註釋:將批量任務打包請求Peer節點\n ProcessingResult result = processor.process(tasks);\n switch (result) {\n case Success:\n break;\n case Congestion:\n case TransientError:\n taskDispatcher.reprocess(holders, result);\n break;\n case PermanentError:\n logger.warn(\"Discarding {} tasks of {} due to permanent error\", holders.size(), workerName);\n }\n metrics.registerTaskResult(result, tasks.size());\n }\n } catch (InterruptedException e) {\n // Ignore\n } catch (Throwable e) {\n // Safe-guard, so we never exit this loop in an uncontrolled way.\n logger.warn(\"Discovery WorkerThread error\", e);\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這就是我們 BatchWorkerRunnable 類的 run 方法,這裏面首先要獲取信號量釋放,才能獲得任務集合,一旦獲取到了任務集合的話,那麼就直接調用 processor.process(tasks) 方法請求 Peer 節點同步數據,接下來我們看看 ReplicationTaskProcessor.process 方法;","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Override\npublic ProcessingResult process(List tasks) {\n ReplicationList list = createReplicationListOf(tasks);\n try {\n // 註釋:這裏通過 JerseyReplicationClient 客戶端對象直接發送list請求數據\n EurekaHttpResponse response = replicationClient.submitBatchUpdates(list);\n int statusCode = response.getStatusCode();\n if (!isSuccess(statusCode)) {\n if (statusCode == 503) {\n logger.warn(\"Server busy (503) HTTP status code received from the peer {}; rescheduling tasks after delay\", peerId);\n return ProcessingResult.Congestion;\n } else {\n // Unexpected error returned from the server. This should ideally never happen.\n logger.error(\"Batch update failure with HTTP status code {}; discarding {} replication tasks\", statusCode, tasks.size());\n return ProcessingResult.PermanentError;\n }\n } else {\n handleBatchResponse(tasks, response.getEntity().getResponseList());\n }\n } catch (Throwable e) {\n if (isNetworkConnectException(e)) {\n logNetworkErrorSample(null, e);\n return ProcessingResult.TransientError;\n } else {\n logger.error(\"Not re-trying this exception because it does not seem to be a network exception\", e);\n return ProcessingResult.PermanentError;\n }\n }\n return ProcessingResult.Success;\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"感覺快要見到真相了,所以我們迫不及待的進入 JerseyReplicationClient.submitBatchUpdates(ReplicationList replicationList) 方法一窺究竟。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Override\npublic EurekaHttpResponse submitBatchUpdates(ReplicationList replicationList) {\n ClientResponse response = null;\n try {\n response = jerseyApacheClient.resource(serviceUrl)\n // 註釋:這纔是重點,請求目的相對路徑,peerreplication/batch/\n .path(PeerEurekaNode.BATCH_URL_PATH)\n .accept(MediaType.APPLICATION_JSON_TYPE)\n .type(MediaType.APPLICATION_JSON_TYPE)\n .post(ClientResponse.class, replicationList);\n if (!isSuccess(response.getStatus())) {\n return anEurekaHttpResponse(response.getStatus(), ReplicationListResponse.class).build();\n }\n ReplicationListResponse batchResponse = response.getEntity(ReplicationListResponse.class);\n return anEurekaHttpResponse(response.getStatus(), batchResponse).type(MediaType.APPLICATION_JSON_TYPE).build();\n } finally {\n if (response != null) {\n response.close();\n }\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看到了相對路徑地址,我們搜索下\"batch\"這樣的字符串看看有沒有對應的接收方法或者被@Path註解進入的;在 eureka-core-1.4.12.jar 這個包下面,果然搜到到了 @Path(\"batch\") 這樣的字樣,直接進入,發現這是 PeerReplicationResource 類的方法 batchReplication,我們進入這方法看看。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Path(\"batch\")\[email protected]\npublic Response batchReplication(ReplicationList replicationList) {\n try {\n ReplicationListResponse batchResponse = new ReplicationListResponse();\n // 註釋:這裏將收到的任務列表,依次循環解析處理,主要核心方法在 dispatch 方法中。\n for (ReplicationInstance instanceInfo : replicationList.getReplicationList()) {\n try {\n batchResponse.addResponse(dispatch(instanceInfo));\n } catch (Exception e) {\n batchResponse.addResponse(new ReplicationInstanceResponse(Status.INTERNAL_SERVER_ERROR.getStatusCode(), null));\n logger.error(instanceInfo.getAction() + \" request processing failed for batch item \"\n + instanceInfo.getAppName() + '/' + instanceInfo.getId(), e);\n }\n }\n return Response.ok(batchResponse).build();\n } catch (Throwable e) {\n logger.error(\"Cannot execute batch Request\", e);\n return Response.status(Status.INTERNAL_SERVER_ERROR).build();\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看到了循環一次遍歷任務進行處理,不知不覺覺得心花怒放,勝利的重點馬上就要到來了,我們進入 PeerReplicationResource.dispatch 方法看看。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"private ReplicationInstanceResponse dispatch(ReplicationInstance instanceInfo) {\n ApplicationResource applicationResource = createApplicationResource(instanceInfo);\n InstanceResource resource = createInstanceResource(instanceInfo, applicationResource);\n\n String lastDirtyTimestamp = toString(instanceInfo.getLastDirtyTimestamp());\n String overriddenStatus = toString(instanceInfo.getOverriddenStatus());\n String instanceStatus = toString(instanceInfo.getStatus());\n\n Builder singleResponseBuilder = new Builder();\n switch (instanceInfo.getAction()) {\n case Register:\n singleResponseBuilder = handleRegister(instanceInfo, applicationResource);\n break;\n case Heartbeat:\n singleResponseBuilder = handleHeartbeat(resource, lastDirtyTimestamp, overriddenStatus, instanceStatus);\n break;\n case Cancel:\n singleResponseBuilder = handleCancel(resource);\n break;\n case StatusUpdate:\n singleResponseBuilder = handleStatusUpdate(instanceInfo, resource);\n break;\n case DeleteStatusOverride:\n singleResponseBuilder = handleDeleteStatusOverride(instanceInfo, resource);\n break;\n }\n return singleResponseBuilder.build();\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隨便抓一個類型,那我們也拿 Register 類型來看,進入 PeerReplicationResource.handleRegister 看看。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"private static Builder handleRegister(ReplicationInstance instanceInfo, ApplicationResource applicationResource) {\n // 註釋:private static final String REPLICATION = \"true\"; 定義的一個常量值,而且還是回調 ApplicationResource.addInstance 方法\n applicationResource.addInstance(instanceInfo.getInstanceInfo(), REPLICATION);\n return new Builder().setStatusCode(Status.OK.getStatusCode());\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Peer節點的同步旅程終於結束了,最終又回調到了 ApplicationResource.addInstance 這個方法,這個方法在最終是EurekaClient啓動後註冊調用的方法,然而Peer節點的信息同步也調用了這個方法,僅僅只是通過一個變量 isReplication 爲true還是false來判斷是否是節點複製。剩下的ApplicationResource.addInstance流程前面已經提到過了,相信大家已經明白了註冊的流程是如何扭轉的,包括批量任務是如何處理EurekaServer節點之間的信息同步的了。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"EurekaClient 啓動流程分析","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"調換運行模式","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Run運行discovery-eureka服務,Debug 運行 provider-user 服務,先觀察日誌先;","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"2017-10-23 19:43:07.688 INFO 1488 --- [ main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0\n2017-10-23 19:43:07.694 INFO 1488 --- [ main] o.s.c.n.eureka.InstanceInfoFactory : Setting initial instance status as: STARTING\n2017-10-23 19:43:07.874 INFO 1488 --- [ main] c.n.d.provider.DiscoveryJerseyProvider : Using JSON encoding codec LegacyJacksonJson\n2017-10-23 19:43:07.874 INFO 1488 --- [ main] c.n.d.provider.DiscoveryJerseyProvider : Using JSON decoding codec LegacyJacksonJson\n2017-10-23 19:43:07.971 INFO 1488 --- [ main] c.n.d.provider.DiscoveryJerseyProvider : Using XML encoding codec XStreamXml\n2017-10-23 19:43:07.971 INFO 1488 --- [ main] c.n.d.provider.DiscoveryJerseyProvider : Using XML decoding codec XStreamXml\n2017-10-23 19:43:08.134 INFO 1488 --- [ main] c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration\n2017-10-23 19:43:08.344 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Disable delta property : false\n2017-10-23 19:43:08.344 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Single vip registry refresh property : null\n2017-10-23 19:43:08.344 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Force full registry fetch : false\n2017-10-23 19:43:08.344 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Application is null : false\n2017-10-23 19:43:08.344 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Registered Applications size is zero : true\n2017-10-23 19:43:08.344 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Application version is -1: true\n2017-10-23 19:43:08.345 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Getting all instance registry info from the eureka server\n2017-10-23 19:43:08.630 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : The response status is 200\n2017-10-23 19:43:08.631 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Starting heartbeat executor: renew interval is: 30\n2017-10-23 19:43:08.634 INFO 1488 --- [ main] c.n.discovery.InstanceInfoReplicator : InstanceInfoReplicator onDemand update allowed rate per min is 4\n2017-10-23 19:43:08.637 INFO 1488 --- [ main] com.netflix.discovery.DiscoveryClient : Discovery Client initialized at timestamp 1508758988637 with initial instances count: 0\n2017-10-23 19:43:08.657 INFO 1488 --- [ main] c.n.e.EurekaDiscoveryClientConfiguration : Registering application springms-provider-user with eureka with status UP\n2017-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]\n2017-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...\n2017-10-23 19:43:08.768 INFO 1488 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 7900 (http)\n2017-10-23 19:43:08.768 INFO 1488 --- [ main] c.n.e.EurekaDiscoveryClientConfiguration : Updating port to 7900\n2017-10-23 19:43:08.773 INFO 1488 --- [ main] c.s.cloud.MsProviderUserApplication : Started ProviderApplication in 882.1 seconds (JVM running for 10.398)\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"服務提供方主體加載流程","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"【1】:仔細查看下日誌,先是 DefaultLifecycleProcessor 類處理了一些 bean,然後接下來肯定會調用一些實現 SmartLifecycle 類的start 方法;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"【2】: 接着初始化設置了EurekaClient的狀態爲 STARTING,初始化編碼使用的格式,哪些用JSON,哪些用XML;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"【3】: 緊接着打印了強制獲取註冊信息狀態爲false,已註冊的應用大小爲0,客戶端發送心跳續約,心跳續約間隔爲30秒,最後打印Client初始化完成;","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"EnableEurekaClient 組件。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Target(ElementType.TYPE)\[email protected](RetentionPolicy.RUNTIME)\[email protected]\[email protected]\[email protected]\npublic @interface EnableEurekaClient {}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"@EnableEurekaClient","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"這個註解類竟然也使用了註解 @EnableDiscoveryClient,那麼我們有必要去這個註解類看看。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Target(ElementType.TYPE)\[email protected](RetentionPolicy.RUNTIME)\[email protected]\[email protected]\[email protected](EnableDiscoveryClientImportSelector.class)\npublic @interface EnableDiscoveryClient {}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"@EnableDiscoveryClient","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個註解類有個比較特殊的註解 ","attrs":{}},{"type":"link","attrs":{"href":"https://my.oschina.net/u/3201731","title":"","type":null},"content":[{"type":"text","text":"@Import","attrs":{}}]},{"type":"text","text":",由此我們猜想,這裏的大多數邏輯是不是都寫在這個 ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"EnableDiscoveryClientImportSelector","attrs":{}},{"type":"text","text":" 類呢?","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"EnableDiscoveryClientImportSelector","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Order(Ordered.LOWEST_PRECEDENCE - 100)\npublic class EnableDiscoveryClientImportSelector\n extends SpringFactoryImportSelector {\n @Override\n protected boolean isEnabled() {\n return new RelaxedPropertyResolver(getEnvironment()).getProperty(\n \"spring.cloud.discovery.enabled\", Boolean.class, Boolean.TRUE);\n }\n @Override\n protected boolean hasDefaultFactory() {\n return true;\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"EnableDiscoveryClientImportSelector 類繼承了 SpringFactoryImportSelector 類,但是重寫了一個 isEnabled() 方法,默認值返回 true,爲什麼會返回true","attrs":{}},{"type":"text","text":"。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"/**\n * Select and return the names of which class(es) should be imported based on\n * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.\n */\[email protected]\npublic String[] selectImports(AnnotationMetadata metadata) {\n if (!isEnabled()) { \n return new String[0];\n }\n AnnotationAttributes attributes = AnnotationAttributes.fromMap(\n metadata.getAnnotationAttributes(this.annotationClass.getName(), true));\n Assert.notNull(attributes, \"No \" + getSimpleName() + \" attributes found. Is \"\n + metadata.getClassName() + \" annotated with @\" + getSimpleName() + \"?\");\n // Find all possible auto configuration classes, filtering duplicates\n List factories = new ArrayList<>(new LinkedHashSet<>(SpringFactoriesLoader\n .loadFactoryNames(this.annotationClass, this.beanClassLoader)));\n if (factories.isEmpty() && !hasDefaultFactory()) {\n throw new IllegalStateException(\"Annotation @\" + getSimpleName()\n + \" found, but there are no implementations. Did you forget to include a starter?\");\n }\n if (factories.size() > 1) {\n // there should only ever be one DiscoveryClient, but there might be more than\n // one factory\n log.warn(\"More than one implementation \" + \"of @\" + getSimpleName()\n + \" (now relying on @Conditionals to pick one): \" + factories);\n }\n return factories.toArray(new String[factories.size()]);\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"EnableDiscoveryClientImportSelector.selectImports","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"首先通過註解獲取了一些屬性,然後加載了一些類名稱,我們進入loadFactoryNames 方法看看。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public static List loadFactoryNames(Class> factoryClass, ClassLoader classLoader) {\n String factoryClassName = factoryClass.getName();\n try {\n // 註釋:public static final String FACTORIES_RESOURCE_LOCATION = \"META-INF/spring.factories\";\n // 註釋:這個 jar 包下的一個配置文件\n Enumeration urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :\n ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));\n List result = new ArrayList();\n while (urls.hasMoreElements()) {\n URL url = urls.nextElement();\n Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));\n String factoryClassNames = properties.getProperty(factoryClassName);\n result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));\n }\n return result;\n }\n catch (IOException ex) {\n throw new IllegalArgumentException(\"Unable to load [\" + factoryClass.getName() +\n \"] factories from location [\" + FACTORIES_RESOURCE_LOCATION + \"]\", ex);\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"加載了一個配置文件,配置文件裏面寫了啥呢?打開SpringFactoryImportSelector該文件所在的jar包的spring.factories文件一看。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"# AutoConfiguration\norg.springframework.boot.autoconfigure.EnableAutoConfiguration=\\\norg.springframework.cloud.client.CommonsClientAutoConfiguration,\\\norg.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration,\\\norg.springframework.cloud.client.hypermedia.CloudHypermediaAutoConfiguration,\\\norg.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration,\\\norg.springframework.cloud.commons.util.UtilAutoConfiguration\n# Environment Post Processors\norg.springframework.boot.env.EnvironmentPostProcessor=\\\norg.springframework.cloud.client.HostInfoEnvironmentPostProcessor\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"都是一些 Configuration 後綴的類名,所以這些都是加載的一堆堆的配置文件類。factories對象裏面只有一個類名路徑爲 org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration 。","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"EurekaDiscoveryClientConfiguration","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Configuration\[email protected]\[email protected](EurekaClientConfig.class)\[email protected](value = \"eureka.client.enabled\", matchIfMissing = true)\[email protected]\npublic class EurekaDiscoveryClientConfiguration implements SmartLifecycle, Ordered {\n @Override\n public void start() {\n // only set the port if the nonSecurePort is 0 and this.port != 0\n if (this.port.get() != 0 && this.instanceConfig.getNonSecurePort() == 0) {\n this.instanceConfig.setNonSecurePort(this.port.get());\n }\n // only initialize if nonSecurePort is greater than 0 and it isn't already running\n // because of containerPortInitializer below\n if (!this.running.get() && this.instanceConfig.getNonSecurePort() > 0) {\n maybeInitializeClient();\n if (log.isInfoEnabled()) {\n log.info(\"Registering application \" + this.instanceConfig.getAppname()\n + \" with eureka with status \"\n + this.instanceConfig.getInitialStatus());\n }\n this.applicationInfoManager\n .setInstanceStatus(this.instanceConfig.getInitialStatus());\n if (this.healthCheckHandler != null) {\n this.eurekaClient.registerHealthCheck(this.healthCheckHandler);\n }\n this.context.publishEvent(\n new InstanceRegisteredEvent<>(this, this.instanceConfig));\n this.running.set(true);\n }\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先看到該類實現了SmartLifecycle 接口,那麼就肯定會實現 start 方法,而且這個 start 方法感覺應在會被加載執行的。this.applicationInfoManager.setInstanceStatus(this.instanceConfig.getInitialStatus()) 這段代碼有一個觀察者模式的回調存在。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"// ApplicationInfoManager.setInstanceStatus 的方法\npublic synchronized void setInstanceStatus(InstanceStatus status) {// 打上斷點\n InstanceStatus prev = instanceInfo.setStatus(status);\n if (prev != null) {\n for (StatusChangeListener listener : listeners.values()) {\n try {\n listener.notify(new StatusChangeEvent(prev, status));\n } catch (Exception e) {\n logger.warn(\"failed to notify listener: {}\", listener.getId(), e);\n }\n }\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個方法會因爲狀態的改變而回調所有實現 StatusChangeListener 這個類的地方,前提得先註冊到 listeners 中去纔行。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"於是乎,我們斷定,若想要回調,那麼就必須有地方先註冊這個事件,而且這個註冊還必須提前執行在 start 方法前執行,於是我們得先在ApplicationInfoManager 這個類中找到註冊到 listeners 的這個方法。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public void registerStatusChangeListener(StatusChangeListener listener) {// 打上斷點\n listeners.put(listener.getId(), listener);\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"於是我們逆向找下 registerStatusChangeListener 被調用的地方。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很不巧的是,盡然只有1個地方被調用,這個地方就是 DiscoveryClient.initScheduledTasks 方法,而且 initScheduledTasks方法又是在 DiscoveryClient 的構造函數裏面調用的,同時我們也對 initScheduledTasks 以及 initScheduledTasks 被調用的構造方法地方打上斷點。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"果不其然,EurekaDiscoveryClientConfiguration.start 方法被調用了,緊接着 this.applicationInfoManager.setInstanceStatus(this.instanceConfig.getInitialStatus()) 也進入斷點,然後在往下走,又進入的DiscoveryClient.initScheduledTasks 方法中的 notify 回調處。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看着斷點依次經過我們上述分析的地方,然後也符合日誌打印的順序,所以我們現在應該是有必要好好看看 DiscoveryClient.initScheduledTasks 這個方法究竟幹了什麼偉大的事情。然而又想了想,還不如看看 initScheduledTasks 被調用的構造方法。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"DiscoveryClient 經過 @Inject 註解過的構造方法。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Inject\nDiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, DiscoveryClientOptionalArgs args, Provider backupRegistryProvider) {\n if (args != null) {\n this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;\n this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;\n this.eventListeners.addAll(args.getEventListeners());\n } else {\n this.healthCheckCallbackProvider = null;\n this.healthCheckHandlerProvider = null;\n }\n \n this.applicationInfoManager = applicationInfoManager;\n InstanceInfo myInfo = applicationInfoManager.getInfo();\n\n clientConfig = config;\n staticClientConfig = clientConfig;\n transportConfig = config.getTransportConfig();\n instanceInfo = myInfo;\n if (myInfo != null) {\n appPathIdentifier = instanceInfo.getAppName() + \"/\" + instanceInfo.getId();\n } else {\n logger.warn(\"Setting instanceInfo to a passed in null value\");\n }\n\n this.backupRegistryProvider = backupRegistryProvider;\n\n this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);\n localRegionApps.set(new Applications());\n\n fetchRegistryGeneration = new AtomicLong(0);\n\n remoteRegionsToFetch = new AtomicReference(clientConfig.fetchRegistryForRemoteRegions());\n remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(\",\"));\n\n if (config.shouldFetchRegistry()) {\n this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + \"lastUpdateSec_\", new long[]{15L, 30L, 60L, 120L, 240L, 480L});\n } else {\n this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;\n }\n\n if (config.shouldRegisterWithEureka()) {\n this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + \"lastHeartbeatSec_\", new long[]{15L, 30L, 60L, 120L, 240L, 480L});\n } else {\n this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;\n }\n\n if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {\n logger.info(\"Client configured to neither register nor query for data.\");\n scheduler = null;\n heartbeatExecutor = null;\n cacheRefreshExecutor = null;\n eurekaTransport = null;\n instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());\n\n // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()\n // to work with DI'd DiscoveryClient\n DiscoveryManager.getInstance().setDiscoveryClient(this);\n DiscoveryManager.getInstance().setEurekaClientConfig(config);\n\n initTimestampMs = System.currentTimeMillis();\n\n logger.info(\"Discovery Client initialized at timestamp {} with initial instances count: {}\",\n initTimestampMs, this.getApplications().size());\n return; // no need to setup up an network tasks and we are done\n }\n\n try {\n // 註釋:定時任務調度準備\n scheduler = Executors.newScheduledThreadPool(3,\n new ThreadFactoryBuilder()\n .setNameFormat(\"DiscoveryClient-%d\")\n .setDaemon(true)\n .build());\n\n // 註釋:實例化心跳定時任務線程池\n heartbeatExecutor = new ThreadPoolExecutor(\n 1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,\n new SynchronousQueue(),\n new ThreadFactoryBuilder()\n .setNameFormat(\"DiscoveryClient-HeartbeatExecutor-%d\")\n .setDaemon(true)\n .build()\n ); // use direct handoff\n\n // 註釋:實例化緩存刷新定時任務線程池\n cacheRefreshExecutor = new ThreadPoolExecutor(\n 1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,\n new SynchronousQueue(),\n new ThreadFactoryBuilder()\n .setNameFormat(\"DiscoveryClient-CacheRefreshExecutor-%d\")\n .setDaemon(true)\n .build()\n ); // use direct handoff\n\n eurekaTransport = new EurekaTransport();\n scheduleServerEndpointTask(eurekaTransport, args);\n\n AzToRegionMapper azToRegionMapper;\n if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {\n azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);\n } else {\n azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);\n }\n if (null != remoteRegionsToFetch.get()) {\n azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(\",\"));\n }\n instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion());\n } catch (Throwable e) {\n throw new RuntimeException(\"Failed to initialize DiscoveryClient!\", e);\n }\n\n if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {\n fetchRegistryFromBackup();\n }\n\n // 註釋:初始化調度任務\n initScheduledTasks();\n try {\n Monitors.registerObject(this);\n } catch (Throwable e) {\n logger.warn(\"Cannot register timers\", e);\n }\n\n // This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()\n // to work with DI'd DiscoveryClient\n DiscoveryManager.getInstance().setDiscoveryClient(this);\n DiscoveryManager.getInstance().setEurekaClientConfig(config);\n\n initTimestampMs = System.currentTimeMillis();\n logger.info(\"Discovery Client initialized at timestamp {} with initial instances count: {}\",\n initTimestampMs, this.getApplications().size());\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從往下看,initScheduledTasks 這個方法顧名思義就是初始化調度任務,所以這裏面的內容應該就是重頭戲,進入看看。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"private void initScheduledTasks() {\n if (clientConfig.shouldFetchRegistry()) {\n // registry cache refresh timer\n // 註釋:間隔多久去拉取服務註冊信息,默認時間 30秒\n int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();\n int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();\n // 註釋:定時任務,每間隔 30秒 去拉取一次服務註冊信息\n scheduler.schedule(\n new TimedSupervisorTask(\n \"cacheRefresh\",\n scheduler,\n cacheRefreshExecutor,\n registryFetchIntervalSeconds,\n TimeUnit.SECONDS,\n expBackOffBound,\n new CacheRefreshThread()\n ),\n registryFetchIntervalSeconds, TimeUnit.SECONDS);\n }\n\n if (clientConfig.shouldRegisterWithEureka()) {\n // 註釋:間隔多久發送一次心跳續約,默認間隔時間 30 秒\n int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();\n int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();\n logger.info(\"Starting heartbeat executor: \" + \"renew interval is: \" + renewalIntervalInSecs);\n\n // Heartbeat timer\n // 註釋:定時任務,每間隔 30秒 去想 EurekaServer 發送一次心跳續約\n scheduler.schedule(\n new TimedSupervisorTask(\n \"heartbeat\",\n scheduler,\n heartbeatExecutor,\n renewalIntervalInSecs,\n TimeUnit.SECONDS,\n expBackOffBound,\n new HeartbeatThread()\n ),\n renewalIntervalInSecs, TimeUnit.SECONDS);\n\n // InstanceInfo replicator\n // 註釋:實例信息複製器,定時刷新dataCenterInfo數據中心信息,默認30秒\n instanceInfoReplicator = new InstanceInfoReplicator(\n this,\n instanceInfo,\n clientConfig.getInstanceInfoReplicationIntervalSeconds(),\n 2); // burstSize\n\n // 註釋:實例化狀態變化監聽器\n statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {\n @Override\n public String getId() {\n return \"statusChangeListener\";\n }\n\n @Override\n public void notify(StatusChangeEvent statusChangeEvent) {\n if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||\n InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {\n // log at warn level if DOWN was involved\n logger.warn(\"Saw local status change event {}\", statusChangeEvent);\n } else {\n logger.info(\"Saw local status change event {}\", statusChangeEvent);\n }\n\n // 註釋:狀態有變化的話,會回調這個方法\n instanceInfoReplicator.onDemandUpdate();\n }\n };\n\n // 註釋:註冊狀態變化監聽器\n if (clientConfig.shouldOnDemandUpdateStatusChange()) {\n applicationInfoManager.registerStatusChangeListener(statusChangeListener);\n }\n\n instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());\n } else {\n logger.info(\"Not registering with Eureka server per configuration\");\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個方法從上往下一路註釋分析下來,幹了EurekaClient我們最想知道的一些事情,定時任務獲取註冊信息,定時任務刷新緩存,定時任務心跳續約,定時任務同步數據中心數據,狀態變化監聽回調等。但是唯獨沒看到註冊,這是怎麼回事呢?","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"instanceInfoReplicator.onDemandUpdate() 就是在狀態改變的時候。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public boolean onDemandUpdate() {\n if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {\n scheduler.submit(new Runnable() {\n @Override\n public void run() {\n logger.debug(\"Executing on-demand update of local InstanceInfo\");\n\n Future latestPeriodic = scheduledPeriodicRef.get();\n if (latestPeriodic != null && !latestPeriodic.isDone()) {\n logger.debug(\"Canceling the latest scheduled update, it will be rescheduled at the end of on demand update\");\n latestPeriodic.cancel(false);\n }\n\n // 註釋:這裏進行了實例信息刷新和註冊\n InstanceInfoReplicator.this.run();\n }\n });\n return true;\n } else {\n logger.warn(\"Ignoring onDemand update due to rate limiter\");\n return false;\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"onDemandUpdate 這個方法,唯獨 InstanceInfoReplicator.this.run() 這個方法還有點用,而且還是 run 方法呢,感情 InstanceInfoReplicator 這個類還是實現了 Runnable 接口?經過查看這個類,還真是實現了 Runnable 接口。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個方法應該我們要找的註冊所在的地方。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public void run() {\n try {\n discoveryClient.refreshInstanceInfo();\n Long dirtyTimestamp = instanceInfo.isDirtyWithTime();\n if (dirtyTimestamp != null) {\n discoveryClient.register();\n instanceInfo.unsetIsDirty(dirtyTimestamp);\n }\n } catch (Throwable t) {\n logger.warn(\"There was a problem with the instance info replicator\", t);\n } finally {\n Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);\n scheduledPeriodicRef.set(next);\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"discoveryClient.register() 這個 register 方法,原來註冊方法就是這個。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"boolean register() throws Throwable {\n logger.info(PREFIX + appPathIdentifier + \": registering service...\");\n EurekaHttpResponse httpResponse;\n try {\n httpResponse = eurekaTransport.registrationClient.register(instanceInfo);\n } catch (Exception e) {\n logger.warn(\"{} - registration failed {}\", PREFIX + appPathIdentifier, e.getMessage(), e);\n throw e;\n }\n if (logger.isInfoEnabled()) {\n logger.info(\"{} - registration status: {}\", PREFIX + appPathIdentifier, httpResponse.getStatusCode());\n }\n return httpResponse.getStatusCode() == 204;\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原來調用了 EurekaHttpClient 封裝的客戶端請求對象來進行註冊的,再繼續深探 registrationClient.register 方法,於是我們來到了 AbstractJerseyEurekaHttpClient.register 方法。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Override\npublic EurekaHttpResponse register(InstanceInfo info) {\n String urlPath = \"apps/\" + info.getAppName();\n ClientResponse response = null;\n try {\n Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();\n addExtraHeaders(resourceBuilder);\n response = resourceBuilder\n .header(\"Accept-Encoding\", \"gzip\")\n .type(MediaType.APPLICATION_JSON_TYPE)\n .accept(MediaType.APPLICATION_JSON)\n // 註釋:打包帶上當前應用的所有信息 info\n .post(ClientResponse.class, info);\n return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();\n } finally {\n if (logger.isDebugEnabled()) {\n logger.debug(\"Jersey HTTP POST {}/{} with instance {}; statusCode={}\", serviceUrl, urlPath, info.getId(),\n response == null ? \"N/A\" : response.getStatus());\n }\n if (response != null) {\n response.close();\n }\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"調用的是 Jersey RESTful 框架來進行請求的,然後在 EurekaServer 那邊就會在 ApplicationResource.addInstance 方法接收客戶端的註冊請求,因此我們的 EurekaClient 是如何註冊的就到此爲止了。","attrs":{}}]}]}],"attrs":{}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章