# 13.3.2 消费服务

在消费者的代码中，硬编码任何服务实例的 URL 都将是错误的。这不仅将消费者与服务的特定实例相结合，而且如果服务的主机或端口要更改，还会导致消费服务中断。

另一方面，当涉及在 Eureka 中查找服务时，消费应用程序还有一些工作，因为 Eureka 可能回复提供相同服务的多个实例。如果消费者查询 `ingredient-service` 服务，并收到六个服务实例，那该如何选择正确的服务实例呢？

好消息是，消费应用程序不需要做出选择，甚至不需要自己明确地查找服务。Spring Cloud 的 Eureka 客户端，以及 Ribbon 负载均衡器，使查找、选择和使用一个服务实例变得很简单。使用从 Eureka 查找到的服务有两种方法，包括：

* 一个负载均衡的 RestTemplate
* Feign 接口生成的客户端

您可以根据个人喜好来选择。下面将介绍这两种方式，先从负载均衡 RestTemplate开始。然后您自己决定喜欢哪一个。

## 使用 RestTemplate 消费服务

您在第 7 章中，首次看到了 Spring 的 RestTemplate。重温一下它是如何工作的。一旦创建或注入了 RestTemplate，您就可以发送 HTTP 调用，并将响应绑定到实体类上。例如，要执行 HTTP GET 请求，按 ID 检索 Ingredient，您可以使用以下 RestTemplate 代码：

```java
public Ingredient getIngredientById(String ingredientId) {
  return rest.getForObject("http://localhost:8080/ingredients/{id}",
                            Ingredient.class, ingredientId);
}
```

这段代码的唯一问题是，传递到 `getForObject()` 的 URL 是硬编码到特定的主机和端口的。虽然您可以将 URL 提取到一个配置属性中，但如果请求是对 Ingredient 服务的多个实例之一，您配置的任何 URL 都将只针对特定实例，而不能在多个实例中进行负载均衡。

但是，一旦将应用程序设为 Eureka 客户端，您就可以声明这是一个带有负载均衡的 RestTemplate。您所需要做的，就是为方法添加 `@Bean` 和 `@LoadBalanced` 注解：

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

`@LoadBalanced` 注解有两个目的：首先也是最重要的，它告诉 Spring Cloud，这个 RestTemplate 应该能够通过 Ribbon 查找服务。另外，它可以作为一个注入限定符。因为，如果您有两个或两个以上的 RestTemplate，这指定了要使用带有负载均衡的 RestTemplate。

例如，您希望使用负载均衡的 RestTemplate，像以前的代码那样查找 Ingredient。首先，将负载均衡 RestTemplate 注入到实体：

```java
@Component
public class IngredientServiceClient {

  private RestTemplate rest;

  public IngredientServiceClient(@LoadBalanced RestTemplate rest) {
    this.rest = rest;
  }

  ...

}
```

然后稍微改写一下 `getIngredientById()` 方法来获取 Ingredient，以便它使用服务的注册名称，而不是显式的指定主机和端口：

```java
public Ingredient getIngredientById(String ingredientId) {
  return rest.getForObject(
              "http://ingredient-service/ingredients/{id}",
              Ingredient.class, ingredientId);
}
```

你注意到两者的区别了吗？为 `getForObject()` 指定的 URL，没有使用任何特定的主机名或端口。使用服务名称 `ingredient-service` 来代替主机名和端口。内部 RestTemplate 要求 Ribbon 查找服务并选择一个实例。Ribbon 很高兴提供帮助，重写 URL 以包含所选服务实例的主机和端口，然后让 RestTemplate 照常进行后续工作。

正如您所看到的，使用负载均衡的 RestTemplate 与使用标准的 RestTemplate 并没有什么不同。关键不同在于，客户端代码只需要使用服务名称，而不是显式的主机名和端口。但是，如果您使用的是 WebClient 而不是 RestTemplate 呢？WebClient 也可以与 Ribbon 一起按名称来使用服务吗？

## 使用 WebClient 消费服务

在第 11 章，您看到了 WebClient 如何提供类似于 RestTemplate 的 HTTP 客户端。但它处理的是响应式类型，如 Flux 和 Mono。如果您已经被响应式编程错误所困扰，那么您可能更喜欢使用 WebClient 而不是RestTemplate。好消息是，您可以像使用 RestTemplate 那样使用 WebClient。首先要做的是声明一个 WebClient，并添加 `@LoadBalanced` 注解到 `WebClient.Builder` 方法上：

```java
@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
  return WebClient.builder();
}
```

有了 `WebClient.Builder` bean，现在就可以注入到任何需要使用它的地方了。例如，您可以将其注入到 IngredientServiceClient 的构造函数中：

```java
@Component
public class IngredientServiceClient {

  private WebClient.Builder wcBuilder;

  public IngredientServiceClient(
         @LoadBalanced WebClient.Builder wcBuilder) {
    this.wcBuilder = wcBuilder;
  }

 ...

}
```

最后，当您准备好使用它时，您可以使用 `WebClient.Builder` 以构建一个 WebClient，然后使用各服务在Eureka 中注册的服务名称发出实际请求：

```java
public Mono<Ingredient> getIngredientById(String ingredientId) {
  return wcBuilder.build()
    .get()
      .uri("http://ingredient-service/ingredients/{id}", ingredientId)
    .retrieve().bodyToMono(Ingredient.class);
}
```

与负载均衡的 RestTemplate 一样，在发出请求时无需明确指定主机或端口。服务名称将从给定的 URL 中提取，并用来从 Eureka 查找服务。然后，Ribbon 将选择一个服务实例，并且在发出请求之前，使用所选实例的主机和端口重写 URL。

这个编程模型很容易掌握，特别是如果你已经熟悉 RestTemplate 或 WebClient。Spring Cloud 还有另一个技巧，接下来让我们来看看，如何使用 Feign 来创建基于接口的客户端。

## 定义 Feign 接口客户端

Feign 是一个 REST 客户端库，它用一种独特的、接口驱动的方式来定义 REST 客户端。简而言之，如果你喜欢 Spring Data 自动实现 Repository 接口的方式，那您肯定会喜欢 Feign。

Feign 最初是 Netflix 公司的一个项目，但后来作为了一个独立开源项目，名为 OpenFeign (<https://github.com/OpenFeign>)。`Feign` 这个词的意思是“假装”，您很快就会看到，使用这个词，对于假装的 REST 的客户端确实是非常合适。

使用 Feign 的第一步是将依赖项添加到项目中。在 pom.xml 中，以下 `<dependency>` 做到了这一点：

```markup
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
```

使用 Spring Initializr 时，可以通过选中 `Feign` 复选框，自动添加相同的启动依赖项。不幸的是，基于这个依赖的自动配置无法自动启用 Feign。因此，需要将 `@EnableFeignClient` 注解添加到其中一个配置类：

```java
@Configuration
@EnableFeignClients
public RestClientConfiguration {
}
```

现在，有趣的部分来了。假设你想写一个客户端，从 Eureka 注册表中获取 `ingredient-service` 服务，并进而获取 Ingredient。您应该像下面这样使用：

```java
package tacos.ingredientclient.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import tacos.ingredientclient.Ingredient;

@FeignClient("ingredient-service")
public interface IngredientClient {

   @GetMapping("/ingredients/{id}")
   Ingredient getIngredient(@PathVariable("id") String id);

}
```

这是一个简单的接口，没有实现。但在运行时，当 Feign 接管以后，这些都不重要。Feign 会自动创建一个实现，并将其公开为 Spring 应用程序上下文的一个 bean。

仔细观察，你会看到有一些新注解。接口级的 `@FeignClient` 注解，指明了在此接口中声明的任何方法，将对名称为 `ingredient-service` 的服务发出请求。在内部，将通过 Ribbon 查找，就像使用 RestTemplate 时那样。

然后是 `getIngredient()` 方法，您一定认出了来自于 Spring MVC 的 `@GetMapping` 注解。事实上，确实是同样的注解！只是这次是用在客户端上，而不是在 Controller 上。也就是说，任何对 `getIngredient()` 的调用，都将导致 Ribbon 选择相应主机和端口，并把 GET 请求转到 `/ingredients/{id}` 上。`@PathVariable` 注解同样来自 Spring MVC，会将参数映射到给定路径中的占位符中。

剩下的就是在需要时注入 Feign 实现的接口，并开始使用它。例如，要在 Controller 中使用它，您可以执行这样的操作：

```java
@Controller
@RequestMapping("/ingredients")
public class IngredientController {

  private IngredientClient client;

  @Autowired
  public IngredientController(IngredientClient client) {
    this.client = client;
  }

  @GetMapping("/{id}")
  public String ingredientDetailPage(@PathVariable("id") String id,
                                     Model model) {
    model.addAttribute("ingredient", client.getIngredient(id));
    return "ingredientDetail";
 }

}
```

我不知道您怎么想，但我觉这真是太棒了！很难确定我最喜欢哪一个：负载均衡的 RestTemplate、WebClient，或者这个神奇的 Feign 接口。无论您选择哪一个，您都可以放心，您的 REST 客户端将能够使用在 Eureka 注册的服务，而无需对任何特定的主机名或端口进行硬编码。

另外，Feign 有自己的一组注解。`@RequestLine` 和 `@Param` 大致类似于 Spring MVC 的 `@RequestMapping` 和 `@PathVariable`，只是使用方式有点差异。不过，能够在客户端上使用已经熟悉的 Spring MVC 注解，这一点是相当好的。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://potoyang.gitbook.io/spring-in-action-v5/di-13-zhang-fu-wu-fa-xian/13.3-zhu-ce-bing-fa-xian-fu-wu/13.3.2-xiao-fei-fu-wu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
