Spring Cloud 简明教程
Spring Cloud - Synchronous Communication with Feign
Introduction
在分布式环境中,服务需要彼此通信。通信可以同步或异步发生。在本节中,我们将了解服务如何通过同步 API 调用进行通信。
虽然这听起来很简单,但作为 API 调用的一部分,我们需要处理以下问题 −
-
Finding address of the callee − 调用者服务需要知道要调用的服务地址。
-
Load balancing − 调用者服务可以执行一些智能负载均衡,以将负载分配到被调用服务上。
-
Zone awareness − 调用者服务最好调用位于同一区域中的服务,以便快速响应。
Netflix Feign 和 Spring RestTemplate (连同 Ribbon )是用于进行同步 API 调用的两个众所周知的 HTTP 客户端。在本教程中,我们将使用 Feign Client 。
Feign – Dependency Setting
让我们使用我们在前面章节中使用的 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);
}
}
Points to note in the above code −
-
@ EnableDiscoveryClient − 这是我们用于从 Eureka 服务器读/写的相同注释。
-
@EnableFeignCLient − 此注释扫描我们的程序包以查找启用的 feign 客户端,并相应地初始化它。
完成后,现在让我们简要了解一下我们定义 feign 客户端所需的 Feign 接口。
Using Feign Interfaces for API calls
只需在接口中定义 API 调用,Feign 就可以轻松设置 feign 客户端,以便用于构造调用 API 所需的样板代码。例如,考虑我们有两个服务 −
-
Service A − 使用 Feign 客户端的调用者服务。
-
Service B - 被上述 Feign 客户端调用的调用者服务
调用者服务,即本例中的服务 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);
}
Points to note -
-
@FeignClient 注释将由 Spring Feign 初始化的接口,并且可以被其余代码使用。
-
请注意,FeignClient 注释需要包含服务名称,这用于发现服务地址,即从 Eureka 或其他发现平台发现服务 B 的地址。
-
然后,我们可以定义我们计划从服务 A 调用的所有 API 函数名称。这可以是一般的 HTTP 调用,其中包含 GET、POST、PUT 等动词。
完成后,服务 A 可以简单地使用以下代码来调用服务 B 的 API -
@Autowired
ServiceBInterface serviceB
.
.
.
ObjectOfServiceB object = serviceB. getObjectById(5);
我们来看一个示例,以了解实际操作。
Example – Feign Client with Eureka
假设我们要查找与客户所在城市相同的城市的餐厅。我们将使用以下服务 -
-
Customer Service - 拥有所有客户信息。我们之前在 Eureka 客户端部分中定义了此信息。
-
Eureka Discovery Server - 拥有上述服务的相关信息。我们之前在 Eureka 服务器部分中定义了此信息。
-
Restaurant Service - 我们将定义的新服务,其中包含所有餐厅信息。
我们首先向我们的客户服务添加一个基本控制器 -
@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
Note - 一旦启动 Eureka 服务器和此服务,我们应该能够看到在 Eureka 中注册的此服务的一个实例。
若要查看我们的 API 是否正常工作,让我们点击 [role="bare"] [role="bare"]http://localhost:8081/customer/1
我们将获得以下输出 -
{
"id": 1,
"name": "Jane",
"city": "DC"
}
这证明我们的服务运行良好。
现在,让我们开始定义 Restaurant 服务将用来获取客户所在城市的 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 客户端包含服务名称和我们计划在 Restaurant 服务中使用的 API 调用。
最后,让我们在 Restaurant 服务中定义一个使用上述接口的控制器。
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: http://localhost:8900/eureka
现在,让我们编译我们的项目,并使用以下命令执行该项目:
java -Dapp_port=8083 -jar .\target\spring-cloud-feign-client-1.0.jar
总而言之,我们有以下各项运行:
-
Standalone Eureka server
-
Customer service
-
Restaurant service
我们可以从 [role="bare"] [role="bare"]http://localhost:8900/ 上的仪表板上确认上述各项是否正常工作。
现在,让我们尝试找到能够为 Jane 服务的所有餐厅,Jane 居住在华盛顿特区。
为此,首先让我们访问对应的客户服务: [role="bare"] [role="bare"]http://localhost:8080/customer/1
{
"id": 1,
"name": "Jane",
"city": "DC"
}
然后,对 Restaurant 服务进行一次调用: [role="bare"] [role="bare"]http://localhost:8082/restaurant/customer/1
[
{
"id": 1,
"name": "Pandas",
"city": "DC"
},
{
"id": 3,
"name": "Little Italy",
"city": "DC"
}
]
正如我们所见,Jane 可以由华盛顿特区地区的两家餐厅提供服务。
此外,我们可以看到客户服务的日志中:
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 – Zone Awareness
Feign 客户端还支持区域感知。假设我们收到一个针对服务的传入请求,我们需要选择应该为该请求服务的服务器。与其在位于远处的服务器上发送和处理该请求,不如选择同一区域中的服务器会更有成效。
现在,让我们尝试设置一个区域感知的 Feign 客户端。为此,我们将使用上一个示例中的案例。我们将遵循以下步骤:
-
A standalone Eureka server
-
两个区域感知的客户服务实例(代码与上述代码保持一致,我们只使用“Eureka 区域感知”中提到的属性文件)
-
两个分区感知餐厅服务的实例。
现在,让我们首先启动分区感知的客户服务。重新回顾一下,以下为 application property 文件。
spring:
application:
name: customer-service
server:
port: ${app_port}
eureka:
instance:
metadataMap:
zone: ${zoneName}
client:
serviceURL:
defaultZone: http://localhost: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: http://localhost: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
现在,我们已经以分区感知模式设置两个餐厅和客户服务的实例。
现在,让我们访问 [role="bare"] [role="bare"]http://localhost: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
而欧盟分区中的客户服务不会提供任何服务。