Spring cloud服務發現過程

基本使用示例

基礎結構

  • 服務註冊中心 :Eureka提供的服務端,提供服務註冊與發現的功能。
  • 服務提供者:提供服務的應用,可以是Spring boot應用,也可以是其他技術平臺且遵循 Eureka 通信機制的應用。它將自己提供的服務註冊到 Eureka 以供其他應用發現。
  • 服務消費者: 消費者應用從服務註冊中心獲取服務列表,從而使消費者可以知道去何處調用其所需要的服務。

很多時候,客戶端既是服務提供者也是服務消費者。

下面我們創建一個基本的Spring cloud模式的微服務工程,用來說明服務的註冊和發現以及微服務的調用流程以及方式。

  • eureka-server 註冊中心
  • scm-message 消息推送服務
  • scm-form 申請單服務

具體的搭建以及代碼實現過程如下:

創建一個maven工程,引入Springboot以及Springcloud的依賴,定義好統一的版本號。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  	<modelVersion>4.0.0</modelVersion>
  	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.4.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
  
	<groupId>com.starnetuc</groupId>
    <artifactId>scm</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>
    
  	<properties>
		<java.version>1.8</java.version>
		<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
	</properties>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
	<modules>
		<module>eureka-server</module>
		<module>scm-form</module>
		<module>scm-message</module>
		<module>scm-message-api</module>
	</modules>
</project>

eureka-server

pom依賴

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.starnetuc</groupId>
    <artifactId>scm</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <artifactId>eureka-server</artifactId>
  
  	<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
	</dependencies>
</project>

配置文件

server.port=8761
#不需要將自己註冊到自己的註冊中心(集羣的時候需要true)
eureka.client.registerWithEureka=false
#不拉取註冊服務信息
eureka.client.fetchRegistry=false

啓動文件

@SpringBootApplication
@EnableEurekaServer //啓動eureka server的註解
public class EurekaServerApplication {
	public static void main(String[] args) {
		SpringApplication.run(EurekaServerApplication.class, args);
	}	
}

scm-message

pom 文件

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.starnetuc</groupId>
    <artifactId>scm</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <artifactId>scm-message</artifactId>
  
    
  	<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
</project>

配置文件

server.port=8004
#服務名稱  
spring.application.name=eureka-client-message
eureka.client.service-url.defaultZone=http://127.0.0.1:8761/eureka/

啓動文件

@SpringBootApplication
@EnableDiscoveryClient  //開啓eureka client的註解
public class ScmMessageApplication {
	
	public static void main(String[] args) {
		SpringApplication.run(ScmMessageApplication.class, args);
	}
	
}

接口文件

@RestController
public class ScmMessageController {

	Logger logger = LoggerFactory.getLogger(ScmMessageController.class);
	
	@GetMapping("/message/send/{content}")
	public String getForm(@PathVariable("content") String content) {
		logger.debug("send message, content: {}", content);
		return "send message complete, " + content;
	}
}

scm-form

pom文件

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.starnetuc</groupId>
    <artifactId>scm</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <artifactId>scm-form</artifactId>
  
  	<dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
</project>

配置文件

server.port=8003
#服務名稱  
spring.application.name=eureka-client-form
eureka.client.service-url.defaultZone=http://127.0.0.1:8761/eureka/

啓動文件

@SpringBootApplication
@EnableDiscoveryClient
public class ScmFormApplication {

	public static void main(String[] args) {
		SpringApplication.run(ScmFormApplication.class, args);
	}
}

接口文件

@RestController
public class ScmFormController {


	@Bean
	@LoadBalanced
	RestTemplate restTemplate() {
		return new RestTemplate();
	}
	

	@Autowired
	RestTemplate restTemplate;
	
	@GetMapping("/message/send/{content}")
	public String sendMsg(@PathVariable("content") String content) {
		return restTemplate.getForEntity("http://eureka-client-message/message/send/{content}", String.class, content).getBody();
		
	}

}

測試訪問結果

訪問 http://127.0.0.1:8003/message/send/hello%20world

結果send message complete, hello world

服務的註冊與發現

"message""eurekaServer""form"messageeurekaServerformmessage服務啓動獲取註冊中心的服務列表返回註冊中心的服務列表提交註冊信息每隔30秒發送一個保活報文,進行服務續約每隔30秒請求獲取服務列表,獲取其變化的數據返回服務列表的變化數據form服務啓動服務調用message服務停止發送註銷請求檢查服務列表,剔除失效服務"message""eurekaServer""form"messageeurekaServerform

服務註冊:

“服務提供者”在啓動的時候會通過發送REST請求的方式將自己註冊到Eureka Server上,同時帶上自身服務的一些元數據信息

eureka server接收到這個REST請求之後,會將註冊上來的服務維護在自己的服務集合當中。

POST /eureka/apps/EUREKA-CLIENT-MESSAGE HTTP/1.1
Accept-Encoding: gzip
Content-Type: application/json
Accept: application/json
DiscoveryIdentity-Name: DefaultClient
DiscoveryIdentity-Version: 1.4
DiscoveryIdentity-Id: 192.168.0.112
Transfer-Encoding: chunked
Host: 127.0.0.1:8761
Connection: Keep-Alive
User-Agent: Java-EurekaClient/v1.9.8
{
	"instance": {
		"instanceId": "192.168.0.112:eureka-client-message:8004",
		"hostName": "192.168.0.112",
		"app": "EUREKA-CLIENT-MESSAGE",
		"ipAddr": "192.168.0.112",
		"status": "UP",
		"overriddenStatus": "UNKNOWN",
		"port": {
			"$": 8004,
			"@enabled": "true"
		},
		"securePort": {
			"$": 443,
			"@enabled": "false"
		},
		"countryId": 1,
		"dataCenterInfo": {
			"@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
			"name": "MyOwn"
		},
		"leaseInfo": {
			"renewalIntervalInSecs": 30,
			"durationInSecs": 90,
			"registrationTimestamp": 0,
			"lastRenewalTimestamp": 0,
			"evictionTimestamp": 0,
			"serviceUpTimestamp": 0
		},
		"metadata": {
			"management.port": "8004"
		},
		"homePageUrl": "http://192.168.0.112:8004/",
		"statusPageUrl": "http://192.168.0.112:8004/actuator/info",
		"healthCheckUrl": "http://192.168.0.112:8004/actuator/health",
		"vipAddress": "eureka-client-message",
		"secureVipAddress": "eureka-client-message",
		"isCoordinatingDiscoveryServer": "false",
		"lastUpdatedTimestamp": "1566229507268",
		"lastDirtyTimestamp": "1566229508949"
	}
}

服務同步:

服務提供者發送註冊請求到其中一個註冊中心時,會將請求轉發給集羣中相連的其他註冊中心,從而實現註冊中心之間的服務同步。

服務續約:

註冊完服務之後,服務提供者會維護一個心跳用來持續告訴Eureka server服務的狀態。防止Eureka server的“剔除任務”將該服務實例從服務列表中排除出去。

eureka.instance.lease-renewal-interval-in-seconds=30 #服務續約任務的調用間隔時間
eureka.instance.lease-expiration-duration-in-seconds=90 #服務的失效時間
PUT /eureka/apps/EUREKA-CLIENT-MESSAGE/192.168.0.112:eureka-client-message:8004?status=UP&lastDirtyTimestamp=1566229508949 HTTP/1.1
DiscoveryIdentity-Name: DefaultClient
DiscoveryIdentity-Version: 1.4
DiscoveryIdentity-Id: 192.168.0.112
Accept-Encoding: gzip
Content-Length: 0
Host: 127.0.0.1:8761
Connection: Keep-Alive
User-Agent: Java-EurekaClient/v1.9.8

lastDirtyTimestamp:服務最後更新的時間。如果eureka server判斷自己維護的實例已經過期,那麼會返回404要求client重新註冊

服務獲取:

當啓動服務消費者的時候,會發送一個Rest請求給服務註冊中心,來獲取上面註冊的服務清單。爲了性能考慮Eureka server會維護一份只讀的服務清單返回給客戶端,同時該緩存定期刷新。

eureka.client.fetch-registry=false #開啓從註冊中心獲取服務列表
eureka.client.registry-fetch-interval-seconds=30 #刷新服務列表的間隔時間

先啓動message,後啓動form。form得到的服務列表內容

GET /eureka/apps/ HTTP/1.1
Accept: application/json
DiscoveryIdentity-Name: DefaultClient
DiscoveryIdentity-Version: 1.4
DiscoveryIdentity-Id: 192.168.0.112
Accept-Encoding: gzip
Host: 127.0.0.1:8761
Connection: Keep-Alive
User-Agent: Java-EurekaClient/v1.9.8

HTTP/1.1 200 
Content-Encoding: gzip
Content-Type: application/json
Content-Length: 543
Date: Mon, 19 Aug 2019 15:56:28 GMT

{
	"applications": {
		"versions__delta": "1",
		"apps__hashcode": "UP_1_",
		"application": [{
			"name": "EUREKA-CLIENT-MESSAGE",
			"instance": [{
				"instanceId": "192.168.0.112:eureka-client-message:8004",
				"hostName": "192.168.0.112",
				"app": "EUREKA-CLIENT-MESSAGE",
				"ipAddr": "192.168.0.112",
				"status": "UP",
				"overriddenStatus": "UNKNOWN",
				"port": {
					"$": 8004,
					"@enabled": "true"
				},
				"securePort": {
					"$": 443,
					"@enabled": "false"
				},
				"countryId": 1,
				"dataCenterInfo": {
					"@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
					"name": "MyOwn"
				},
				"leaseInfo": {
					"renewalIntervalInSecs": 30,
					"durationInSecs": 90,
					"registrationTimestamp": 1566229509143,
					"lastRenewalTimestamp": 1566230229121,
					"evictionTimestamp": 0,
					"serviceUpTimestamp": 1566229509144
				},
				"metadata": {
					"management.port": "8004"
				},
				"homePageUrl": "http://192.168.0.112:8004/",
				"statusPageUrl": "http://192.168.0.112:8004/actuator/info",
				"healthCheckUrl": "http://192.168.0.112:8004/actuator/health",
				"vipAddress": "eureka-client-message",
				"secureVipAddress": "eureka-client-message",
				"isCoordinatingDiscoveryServer": "false",
				"lastUpdatedTimestamp": "1566229509144",
				"lastDirtyTimestamp": "1566229508949",
				"actionType": "ADDED"
			}]
		}]
	}
}

增量獲取,先啓動message,後啓動form。message得到的增量內容

GET /eureka/apps/delta HTTP/1.1
Accept: application/json
DiscoveryIdentity-Name: DefaultClient
DiscoveryIdentity-Version: 1.4
DiscoveryIdentity-Id: 192.168.0.112
Accept-Encoding: gzip
Host: 127.0.0.1:8761
Connection: Keep-Alive
User-Agent: Java-EurekaClient/v1.9.8

HTTP/1.1 200 
Content-Encoding: gzip
Content-Type: application/json
Content-Length: 532
Date: Mon, 19 Aug 2019 15:56:39 GMT

{
	"applications": {
		"versions__delta": "5",
		"apps__hashcode": "UP_2_",
		"application": [{
			"name": "EUREKA-CLIENT-FORM",
			"instance": [{
				"instanceId": "192.168.0.112:eureka-client-form:8003",
				"hostName": "192.168.0.112",
				"app": "EUREKA-CLIENT-FORM",
				"ipAddr": "192.168.0.112",
				"status": "UP",
				"overriddenStatus": "UNKNOWN",
				"port": {
					"$": 8003,
					"@enabled": "true"
				},
				"securePort": {
					"$": 443,
					"@enabled": "false"
				},
				"countryId": 1,
				"dataCenterInfo": {
					"@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
					"name": "MyOwn"
				},
				"leaseInfo": {
					"renewalIntervalInSecs": 30,
					"durationInSecs": 90,
					"registrationTimestamp": 1566230188521,
					"lastRenewalTimestamp": 1566230188521,
					"evictionTimestamp": 0,
					"serviceUpTimestamp": 1566230188521
				},
				"metadata": {
					"management.port": "8003"
				},
				"homePageUrl": "http://192.168.0.112:8003/",
				"statusPageUrl": "http://192.168.0.112:8003/actuator/info",
				"healthCheckUrl": "http://192.168.0.112:8003/actuator/health",
				"vipAddress": "eureka-client-form",
				"secureVipAddress": "eureka-client-form",
				"isCoordinatingDiscoveryServer": "false",
				"lastUpdatedTimestamp": "1566230188521",
				"lastDirtyTimestamp": "1566230188428",
				"actionType": "ADDED"
			}]
		}]
	}
}

服務調用:

消費者在獲取服務清單後,通過服務名可以獲得具體提供的服務的實例名和該實例的元數據信息。因爲有了這些服務實例的詳細信息,所以客戶端可以根據自己的需要決定具體調用哪個實例。

這裏會涉及到客戶端對於調用的服務提供者的選擇和負載均衡。

服務下線:

服務實例進行正常的關閉操作時,會觸發一個服務下線的REST請求給Eureka server,告訴服務註冊中心該服務下線了。服務端在接受到請求之後,將該服務狀態置爲下線(DOWN),並把下線事件傳播出去。

DELETE /eureka/apps/EUREKA-CLIENT-FORM/192.168.0.112:eureka-client-form:8003 HTTP/1.1
DiscoveryIdentity-Name: DefaultClient
DiscoveryIdentity-Version: 1.4
DiscoveryIdentity-Id: 192.168.0.112
Accept-Encoding: gzip
Host: 127.0.0.1:8761
Connection: Keep-Alive
User-Agent: Java-EurekaClient/v1.9.8

HTTP/1.1 204 
Content-Type: application/json
Date: Tue, 20 Aug 2019 14:45:43 GMT

服務註冊中心

失效剔除

有些時候,服務實例並不一定會正常下線,由於一些異常的原因,服務無法正常發出下線請求。Eureka server爲了將這些服務剔除,會啓動一個定時任務,會在默認的間隔時間裏把續約超時的服務剔除出去。

自我保護

EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

Eureka server 在運行期間,會統計心跳失敗率率在15分鐘以內是否高於85%,如果低於這種情況,Eureka Server會將這些實例註冊信息保護起來,讓這些實例不會過期,儘可能保護這些註冊信息。

eureka.server.enable-self-preservation=false #關閉自我保護機制

服務調用的基本原理

前文提到每個服務會從註冊中心同步一份服務列表。有了這份服務列表服務調用的時候可以從列表中通過負載均衡策略獲取到合適的服務提供者,然後從服務信息中得到正確的URI地址,進行訪問。

Ribbon是Netflix發佈的負載均衡器,它有助於控制HTTP和TCP的客戶端的行爲。爲Ribbon配置服務提供者地址後,Ribbon就可基於某種負載均衡算法,自動地幫助服務消費者去請求。

Eureka和Ribbon會觸發自動配置,將服務列表的維護交給Eureka的服務治理機制來維護,Ribbon要查詢服務列表就可以從Eureka維護的服務列表中獲取。

Ribbon怎麼做到負載均衡

@RestController

public class ScmFormController {
	
	@Bean
	@LoadBalanced
	RestTemplate restTemplate() {
		return new RestTemplate();
	}
	
	@Autowired
	RestTemplate restTemplate;
		
	@GetMapping("/message/send/{content}")
	public String sendMsg(@PathVariable("content") String content) {
		return restTemplate.getForEntity("http://eureka-client-message/message/send/{content}", String.class, content).getBody();
	}
}

再來看看上文提供的服務調用代碼

RestTemplate 是 Spring 提供的對 http 請求封裝的工具類。提供一些供http訪問的API如下

  • getForEntity(…)
  • postForEntity(…)
  • put(…)
  • delete(…)

可以看出 RestTemplate 構造時添加了一個註解 @LoadBalanced。

這裏就引入了 Ribbon 對於服務調用時的負載均衡。也就是上述代碼進行服務調用時需要先通過 Ribbon 進行服務的選擇和URL地址的構造 才能夠確定服務提供方的地址。然後通過 RestTemplate 才能夠發起http的請求。

大致的一個流程如下:

  1. 使用 @LoadBalanced 註解的 RestTemplate 會被 LoadBalancerClient 自動裝配。

  2. LoadBalancerAutoConfiguration 關於負載均衡的自動配置,在這裏的自動配置

    創建了一個 LoadBalancerInterceptor 的Bean,用於實現對客戶端發起請求時進行攔截,以實現客戶端的負載均衡。

    創建了一個 RestTemplateCustomizer 的Bean,用於給 RestTemplate 增加 LoadBalancerInterceptor 攔截器。

    維護了一個被 @LoadBalanced 註解修飾的 RestTemplate 對象列表,並在這裏進行初始化。通過調用 RestTemplateCustomizer 的實例來給客戶端負載均衡的 RestTemplate 增加LoadBalancerInterceptor 攔截器。

  3. LoadBalancerInterceptor 是一個攔截器,當 restTamplate 對象向外發起 HTTP 請求時,會被 LoadBalancerInterceptor 的 intercept 函數所攔截。攔截函數InterceptingClientHttpRequest

    		@Override
    		public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
    			if (this.iterator.hasNext()) {
    				ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
    				return nextInterceptor.intercept(request, body, this);
    			}
    			else {
    				HttpMethod method = request.getMethod();
    				Assert.state(method != null, "No standard HTTP method");
    				ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
    				request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
    				if (body.length > 0) {
    					if (delegate instanceof StreamingHttpOutputMessage) {
    						StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate;
    						streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream));
    					}
    					else {
    						StreamUtils.copy(body, delegate.getBody());
    					}
    				}
    				return delegate.execute();
    			}
    		}
    	}
    
	@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
			final ClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI();
		String serviceName = originalUri.getHost();
		Assert.state(serviceName != null,
				"Request URI does not contain a valid hostname: " + originalUri);
		return this.loadBalancer.execute(serviceName,
				this.requestFactory.createRequest(request, body, execution));
	}

​ 此處可見loadBalancer.execute() 可以根據服務名來選擇實例併發起實際的請求。

  1. 而 Ribbon 提供了 LoadBalancerClient 的具體實現。org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient

    在 Ribbon 的具體實現當中,Ribbon 使用了 ILoadBalancer 接口進行服務的選擇。Ribbon對這個接口有多個實現,如 BaseLoadBalancer, DynamicServerListLoadBalancer 和 ZoneAwareLoadBalancer等等。這些具體實現都實現了不同的負載均衡策略。通過這些負載均衡的實現可以選擇到合適的服務。

    而在 RibbonClientConfiguration配置類中,可知默認採用了 ZoneAwareLoadBalancer 來實現負載均衡器。

    public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
    	throws IOException {
    	ILoadBalancer loadBalancer = getLoadBalancer(serviceId);  //獲取負載均衡實現類
    	Server server = getServer(loadBalancer, hint); //通過負載均衡得到合適的server
    	if (server == null) {
    		throw new IllegalStateException("No instances available for " + serviceId);
    	}
    	RibbonServer ribbonServer = new RibbonServer(serviceId, server,
    				isSecure(server, serviceId),
    				serverIntrospector(serviceId).getMetadata(server));
    	return execute(serviceId, ribbonServer, request);
    }
    
    @Override
    public <T> T execute(String serviceId, ServiceInstance serviceInstance,
    		LoadBalancerRequest<T> request) throws IOException {
    	Server server = null;
    	if (serviceInstance instanceof RibbonServer) {
    		server = ((RibbonServer) serviceInstance).getServer();
    	}
    	if (server == null) {
    		throw new IllegalStateException("No instances available for " + serviceId);
    	}
    	RibbonLoadBalancerContext context = this.clientFactory
    			.getLoadBalancerContext(serviceId);
    	RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);
    	try {
    		T returnVal = request.apply(serviceInstance); //執行request
    		statsRecorder.recordStats(returnVal);
    		return returnVal;
    	}
    	// catch IOException and rethrow so RestTemplate behaves correctly
    	catch (IOException ex) {
    		statsRecorder.recordStats(ex);
    		throw ex;
    	}
    	catch (Exception ex) {
    		statsRecorder.recordStats(ex);
    		ReflectionUtils.rethrowRuntimeException(ex);
    	}
    	return null;
    }
    

    最後通過 request.apply(serviceInstance);執行請求

  2. request還會被包裝一次

    HttpRequest serviceRequest = new ServiceRequestWrapper(request,
    								instance, AsyncLoadBalancerInterceptor.this.loadBalancer);
    return execution.executeAsync(serviceRequest, body);
    

    ServiceRequestWrapper 重寫了 getUrl方法

    @Override
    public URI getURI() {
    	URI uri = this.loadBalancer.reconstructURI(this.instance, getRequest().getURI());
    	return uri;
    }
    

    這裏看到通過 loadBalancer構造URI。這裏的loadBalancer就是RibbonLoadBalancerCLient。

    通過ribbon的負載均衡,最後得到一個合適的訪問的URI。

Feign的作用

Feign的使用

@FeignClient(value="eureka-client-message")
public interface ScmMessageFeignClient {

	@GetMapping("/message/send/{content}")
	public String sendMessage(@PathVariable("content") String content);
	
}

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients

public class ScmFormApplication {
	
	
	public static void main(String[] args) {
		SpringApplication.run(ScmFormApplication.class, args);
	}
	
}

@RestController

public class ScmFormController {
	@Autowired
	ScmMessageFeignClient scmMessageFeignClient;
	
	
	@GetMapping("/form/message/send/{content}")
	public String sendMsg(@PathVariable("content") String content) {
		return scmMessageFeignClient.sendMessage("hello");
	}	
}

Feign 聲明式、模板化的HTTP客戶端, Feign可以幫助我們更快捷、優雅地調用HTTP API。

Spring cloud當中Feign集成了 Ribbon以及Hystrix等。使得Feign具有負載均衡以及融斷的功能。

Feign使用的一個基本原理:

  1. 啓動類中添加@EnableFeignClient,它的作用是自動掃描註冊標記爲@FeignClient的用戶定義接口,動態創建代理類並注入到IOC容器當中。
  2. ReflectiveFeign內部使用了jdk的動態代理爲目標接口生成了一個動態代理類,這裏會生成一個InvocationHandler(jdk動態代理原理)統一的方法攔截器,同時爲接口的每個方法生成一個SynchronousMethodHandler攔截器,並解析方法上的 元數據,生成一個http請求模板。
  3. 發送http請求的時候會通過 ribbon 使用負載均衡策略發現服務,得到最終的請求URI。

總結一下,Spring cloud的服務調用

Eureka負責維護服務列表,以供服務請求時候查找服務,選擇服務。

Feign實現了聲明式的,模板化的http客戶端構造。

Feign的http客戶端發出請求後會通過Ribbon進行負載均衡,根據一定的策略得到合適的請求URI。

最終使用http客戶端的API 執行URI的請求訪問得到結果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章