Spring Cloud - 使用Feign進行同步通訊



介紹

在分散式環境中,服務需要相互通訊。通訊可以同步進行,也可以非同步進行。在本節中,我們將瞭解服務如何透過同步 API 呼叫進行通訊。

雖然這聽起來很簡單,但作為進行 API 呼叫的部分,我們需要注意以下幾點 -

  • 查詢被呼叫方的地址 - 呼叫方服務需要知道它想要呼叫的服務的地址。

  • 負載均衡 - 呼叫方服務可以進行一些智慧負載均衡,以將負載分散到被呼叫方服務中。

  • 區域感知 - 呼叫方服務最好呼叫同一區域中的服務以獲得快速響應。

Netflix FeignSpring RestTemplate(以及 Ribbon)是用於進行同步 API 呼叫的兩個眾所周知的 HTTP 客戶端。在本教程中,我們將使用 Feign Client

Feign – 依賴設定

讓我們使用我們在前面章節中一直在使用的 Restaurant 案例。讓我們開發一個 Restaurant 服務,其中包含有關餐廳的所有資訊。

首先,讓我們使用以下依賴項更新服務的 pom.xml -

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

然後,使用正確的註解,即 @EnableDiscoveryClient 和 @EnableFeignCLient,對我們的 Spring 應用程式類進行註解。

package com.tutorialspoint;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class RestaurantService{
   public static void main(String[] args) {
      SpringApplication.run(RestaurantService.class, args);
   }
}

上面程式碼中的注意事項 -

  • @ EnableDiscoveryClient - 這與我們用於讀取/寫入 Eureka 伺服器的註解相同。

  • @EnableFeignCLient - 此註解掃描我們的包以查詢程式碼中啟用的 feign 客戶端並相應地初始化它。

完成後,現在讓我們簡要了解一下我們需要定義 Feign 客戶端的 Feign 介面。

使用 Feign 介面進行 API 呼叫

Feign 客戶端可以透過在介面中定義 API 呼叫來簡單地設定,該介面可用於在 Feign 中構建呼叫 API 所需的樣板程式碼。例如,假設我們有兩個服務 -

  • 服務 A - 使用 Feign Client 的呼叫方服務。

  • 服務 B - 上述 Feign 客戶端將呼叫的被呼叫方服務的 API。

呼叫方服務,在本例中為服務 A,需要為其打算呼叫的 API 建立一個介面,即服務 B。

package com.tutorialspoint;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient(name = "service-B")
public interface ServiceBInterface {
   @RequestMapping("/objects/{id}", method=GET)
   public ObjectOfServiceB getObjectById(@PathVariable("id") Long id);
   @RequestMapping("/objects/", method=POST)
   public void postInfo(ObjectOfServiceB b);
   @RequestMapping("/objects/{id}", method=PUT)
   public void postInfo((@PathVariable("id") Long id, ObjectOfBServiceB b);
}

注意事項 -

  • @FeignClient 註解介面,這些介面將由 Spring Feign 初始化,並可供程式碼的其他部分使用。

  • 請注意,FeignClient 註解需要包含服務的名稱,這用於從 Eureka 或其他發現平臺發現服務 B 的服務地址。

  • 然後,我們可以定義我們計劃從服務 A 呼叫的所有 API 函式名稱。這可以是使用 GET、POST、PUT 等動詞的一般 HTTP 呼叫。

完成後,服務 A 可以簡單地使用以下程式碼來呼叫服務 B 的 API -

@Autowired
ServiceBInterface serviceB
.
.
.
ObjectOfServiceB object = serviceB. getObjectById(5);

讓我們看一個例子,看看它是如何工作的。

示例 – 帶有 Eureka 的 Feign Client

假設我們想查詢與客戶所在城市相同的餐廳。我們將使用以下服務 -

  • 客戶服務 - 包含所有客戶資訊。我們之前在 Eureka Client 部分中定義了它。

  • Eureka 發現伺服器 - 包含上述服務的資訊。我們之前在 Eureka Server 部分中定義了它。

  • 餐廳服務 - 我們將定義的新服務,其中包含所有餐廳資訊。

讓我們首先向我們的客戶服務新增一個基本控制器 -

@RestController
class RestaurantCustomerInstancesController {
   static HashMap<Long, Customer> mockCustomerData = new HashMap();
   static{
      mockCustomerData.put(1L, new Customer(1, "Jane", "DC"));
      mockCustomerData.put(2L, new Customer(2, "John", "SFO"));
      mockCustomerData.put(3L, new Customer(3, "Kate", "NY"));
   }
   @RequestMapping("/customer/{id}")
   public Customer getCustomerInfo(@PathVariable("id") Long id) {
      return mockCustomerData.get(id);
   }
}

我們還將為上述控制器定義一個 Customer.java POJO

package com.tutorialspoint;
public class Customer {
   private long id;
   private String name;
   private String city;
   public Customer() {}
   public Customer(long id, String name, String city) {
      super();
      this.id = id;
      this.name = name;
      this.city = city;
   }
   public long getId() {
      return id;
   }
   public void setId(long id) {
      this.id = id;
   }
   public String getName() {
      return name;
   }
   public void setName(String name) {
      this.name = name;
   }
   public String getCity() {
      return city;
   }
   public void setCity(String city) {
      this.city = city;
   }
}

因此,一旦添加了它,讓我們重新編譯我們的專案並執行以下查詢以啟動 -

java -Dapp_port=8081 -jar .\target\spring-cloud-eureka-client-1.0.jar

注意 - 一旦 Eureka 伺服器和此服務啟動,我們應該能夠在 Eureka 中看到此服務的例項已註冊。

要檢視我們的 API 是否有效,讓我們訪問 https://:8081/customer/1

我們將獲得以下輸出 -

{
   "id": 1,
   "name": "Jane",
   "city": "DC"
}

這證明我們的服務執行良好。

現在讓我們繼續定義餐廳服務將用來獲取客戶城市的 Feign 客戶端。

package com.tutorialspoint;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient(name = "customer-service")
public interface CustomerService {
   @RequestMapping("/customer/{id}")
   public Customer getCustomerById(@PathVariable("id") Long id);
}

Feign 客戶端包含服務名稱和我們計劃在餐廳服務中使用的 API 呼叫。

最後,讓我們在餐廳服務中定義一個控制器,它將使用上述介面。

package com.tutorialspoint;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class RestaurantController {
   @Autowired
   CustomerService customerService;
   static HashMap<Long, Restaurant> mockRestaurantData = new HashMap();
   static{
      mockRestaurantData.put(1L, new Restaurant(1, "Pandas", "DC"));
      mockRestaurantData.put(2L, new Restaurant(2, "Indies", "SFO"));
      mockRestaurantData.put(3L, new Restaurant(3, "Little Italy", "DC"));
}
   @RequestMapping("/restaurant/customer/{id}")
   public List<Restaurant> getRestaurantForCustomer(@PathVariable("id") Long
id) {
      String customerCity = customerService.getCustomerById(id).getCity();
      return mockRestaurantData.entrySet().stream().filter(
entry -> entry.getValue().getCity().equals(customerCity))
.map(entry -> entry.getValue())
.collect(Collectors.toList());
   }
}

這裡最重要的行是以下內容 -

customerService.getCustomerById(id)

這就是我們之前定義的 Feign 客戶端進行 API 呼叫的魔力所在。

讓我們也定義 Restaurant POJO -

package com.tutorialspoint;
public class Restaurant {
   private long id;
   private String name;
   private String city;
   public Restaurant(long id, String name, String city) {
      super();
      this.id = id;
      this.name = name;
      this.city = city;
   }
   public long getId() {
      return id;
   }
   public void setId(long id) {
      this.id = id;
   }
   public String getName() {
      return name;
   }
   public void setName(String name) {
      this.name = name;
   }
   public String getCity() {
      return city;
   }
   public void setCity(String city) {
      this.city = city;
   }
}

一旦定義了它,讓我們使用以下 application.properties 檔案建立一個簡單的 JAR 檔案 -

spring:
   application:
      name: restaurant-service
server:
   port: ${app_port}
eureka:
   client:
      serviceURL:
         defaultZone: https://:8900/eureka

現在讓我們編譯我們的專案並使用以下命令執行它 -

java -Dapp_port=8083 -jar .\target\spring-cloud-feign-client-1.0.jar

總共有以下專案正在執行 -

  • 獨立 Eureka 伺服器

  • 客戶服務

  • 餐廳服務

我們可以透過 https://:8900/ 上的儀表板確認上述專案正在執行。

Feign Client with Eureka

現在,讓我們嘗試查詢所有可以為位於 DC 的 Jane 提供服務的餐廳。

為此,首先讓我們訪問客戶服務以獲取相同的資訊:https://:8080/customer/1

{
   "id": 1,
   "name": "Jane",
   "city": "DC"
}

然後,呼叫餐廳服務:https://:8082/restaurant/customer/1

[
   {
      "id": 1,
      "name": "Pandas",
      "city": "DC"
   },
   {
      "id": 3,
      "name": "Little Italy",
      "city": "DC"
   }
]

正如我們所看到的,Jane 可以由位於 DC 區域的 2 家餐廳提供服務。

此外,從客戶服務的日誌中,我們可以看到 -

2021-03-11 11:52:45.745 INFO 7644 --- [nio-8080-exec-1]
o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
Querying customer for id with: 1

總之,正如我們所看到的,無需編寫任何樣板程式碼,甚至無需指定服務的地址,我們就可以向服務發出 HTTP 呼叫。

Feign Client – 區域感知

Feign 客戶端也支援區域感知。假設我們收到對服務的傳入請求,我們需要選擇應該處理該請求的伺服器。與其將該請求傳送到並處理到距離較遠的伺服器上,不如選擇同一區域中的伺服器更有意義。

現在讓我們嘗試設定一個區域感知的 Feign 客戶端。為此,我們將使用與上一個示例相同的案例。我們將擁有以下內容 -

  • 一個獨立的 Eureka 伺服器

  • 兩個區域感知的客戶服務例項(程式碼與上面相同,我們只需使用“Eureka 區域感知”中提到的屬性檔案即可)

  • 兩個區域感知的餐廳服務例項。

現在,讓我們首先啟動區域感知的客戶服務。概括一下,這是 application property 檔案。

spring:
   application:
      name: customer-service
server:
   port: ${app_port}
eureka:
   instance:
      metadataMap:
         zone: ${zoneName}
   client:
      serviceURL:
         defaultZone: https://:8900/eureka

為了執行,我們將有兩個服務例項執行。為此,讓我們開啟兩個 shell,然後在一個 shell 上執行以下命令 -

java -Dapp_port=8080 -Dzone_name=USA -jar .\target\spring-cloud-eureka-client-
1.0.jar --spring.config.location=classpath:application-za.yml

並在另一個 shell 上執行以下命令 -

java -Dapp_port=8081 -Dzone_name=EU -jar .\target\spring-cloud-eureka-client-
1.0.jar --spring.config.location=classpath:application-za.yml

現在讓我們建立區域感知的餐廳服務。為此,我們將使用以下 application-za.yml

spring:
   application:
      name: restaurant-service
server:
   port: ${app_port}
eureka:
   instance:
      metadataMap:
         zone: ${zoneName}
client:
   serviceURL:
      defaultZone: https://:8900/eureka

為了執行,我們將有兩個服務例項執行。為此,讓我們開啟兩個 shell,然後在一個 shell 上執行以下命令

java -Dapp_port=8082 -Dzone_name=USA -jar .\target\spring-cloud-feign-client-
1.0.jar --spring.config.location=classpath:application-za.yml

並在另一個 shell 上執行以下命令 -

java -Dapp_port=8083 -Dzone_name=EU -jar .\target\spring-cloud-feign-client-
1.0.jar --spring.config.location=classpath:application-za.yml

現在,我們已在區域感知模式下設定了兩個餐廳和客戶服務例項。

Zone Aware Mode

現在,讓我們透過訪問 https://:8082/restaurant/customer/1 進行測試,在這裡我們訪問的是美國區域。

[
   {
      "id": 1,
      "name": "Pandas",
      "city": "DC"
   },
   {
      "id": 3,
      "name": "Little Italy",
      "city": "DC"
   }
]

但這裡需要注意的更重要的一點是,請求是由位於美國區域的客戶服務處理的,而不是位於歐盟區域的服務。例如,如果我們訪問相同的 API 5 次,我們將看到位於美國區域的客戶服務將在日誌語句中顯示以下內容 -

2021-03-11 12:25:19.036 INFO 6500 --- [trap-executor-0]
c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via
configuration
Got request for customer with id: 1
Got request for customer with id: 1
Got request for customer with id: 1
Got request for customer with id: 1
Got request for customer with id: 1

而歐盟區域的客戶服務則不會處理任何請求。

廣告

© . All rights reserved.