Spring Cloud - 使用Feign进行同步通信
介绍
在分布式环境中,服务需要相互通信。通信可以同步进行,也可以异步进行。在本节中,我们将了解服务如何通过同步 API 调用进行通信。
虽然这听起来很简单,但作为进行 API 调用的部分,我们需要注意以下几点 -
查找被调用方的地址 - 调用方服务需要知道它想要调用的服务的地址。
负载均衡 - 调用方服务可以进行一些智能负载均衡,以将负载分散到被调用方服务中。
区域感知 - 调用方服务最好调用同一区域中的服务以获得快速响应。
Netflix Feign 和 Spring 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://127.0.0.1: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://127.0.0.1:8900/eureka
现在让我们编译我们的项目并使用以下命令执行它 -
java -Dapp_port=8083 -jar .\target\spring-cloud-feign-client-1.0.jar
总共有以下项目正在运行 -
独立 Eureka 服务器
客户服务
餐厅服务
我们可以通过 https://127.0.0.1:8900/ 上的仪表板确认上述项目正在运行。
现在,让我们尝试查找所有可以为位于 DC 的 Jane 提供服务的餐厅。
为此,首先让我们访问客户服务以获取相同的信息:https://127.0.0.1:8080/customer/1
{ "id": 1, "name": "Jane", "city": "DC" }
然后,调用餐厅服务:https://127.0.0.1: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://127.0.0.1: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://127.0.0.1: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
现在,我们已在区域感知模式下设置了两个餐厅和客户服务实例。
现在,让我们通过访问 https://127.0.0.1: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
而欧盟区域的客户服务则不会处理任何请求。