- 内容提要
- 译者序
- 前言
- 第 1 章 欢迎迈入云世界,Spring
- 第 2 章 使用 Spring Boot 构建微服务
- 第 3 章 使用 Spring Cloud 配置服务器控制配置
- 第 4 章 服务发现
- 第 5 章 使用 Spring Cloud 和 Netflix Hystrix 的客户端弹性模式
- 第 6 章 使用 Spring Cloud 和 Zuul 进行服务路由
- 第 7 章 保护微服务
- 第 8 章 使用 Spring Cloud Stream 的事件驱动架构
- 第 9 章 使用 Spring Cloud Sleuth 和 Zipkin 进行分布式跟踪
- 第 10 章 部署微服务
- 附录 A 在桌面运行云服务
- 附录 B OAuth2 授权类型
4.5 使用服务发现来查找服务
现在已经有了通过 Eureka 注册的组织服务。我们还可以让许可证服务调用该组织服务,而不必直接知晓任何组织服务的位置。许可证服务将通过 Eureka 来查找组织服务的实际位置。
为了达成我们的目的,我们将研究 3 个不同的 Spring/Netflix 客户端库,服务消费者可以使用它们来和 Ribbon 进行交互。从最低级别到最高级别,这些库包含了不同的与 Ribbon 进行交互的抽象层次。这里将要探讨的库包括:
- Spring DiscoveryClient;
- 启用了 RestTemplate 的 Spring DiscoveryClient;
- Netflix Feign 客户端。
本章将介绍这些客户端,并在许可证服务的上下文中介绍它们的用法。在开始详细介绍客户端的细节之前,我在代码中编写了一些便利的类和方法,以便读者可以使用相同的服务端点来处理不同的客户端类型。
首先,我修改了 src/main/java/com/thoughtmechanix/licenses/controllers/LicenseServiceController. java 以包含许可证服务的新路由。这个新路由允许指定要用于调用服务的客户端的类型。这是一个辅助路由,因此,当我们探索通过 Ribbon 调用组织服务的各种不同方法时,可以通过单个路由来尝试每种机制。 LicenseServiceController
类中新路由的代码如代码清单 4-5 所示。
代码清单 4-5 使用不同的 REST 客户端调用许可证服务
@RequestMapping(value="/{licenseId}/{clientType}", method = RequestMethod.GET)
public License getLicensesWithClient( ⇽--- clientType 确定 Spring REST 要使用的客户端的类型
→ @PathVariable("organizationId") String organizationId,
→ @PathVariable("licenseId") String licenseId,
→ @PathVariable("clientType") String clientType) {
return licenseService.getLicense(organizationId, licenseId, clientType);
}
在上述代码中,该路由上传递的 clientType
参数决定了我们将在代码示例中使用的客户端类型。可以在此路由上传递的具体类型包括:
- Discovery——使用 DiscoveryClient 和标准的 Spring
RestTemplate
类来调用组织服务; - Rest——使用增强的 Spring
RestTemplate
来调用基于 Ribbon 的服务; - Feign——使用 Netflix 的 Feign 客户端库来通过 Ribbon 调用服务。
注意
因为我对这 3 种类型的客户端使用同一份代码,所以读者可能会看到代码中出现某些客户端的注解,即使在某些情况下并不需要它们。例如,读者可以在代码中同时看到
@EnableDiscoveryClient
和@EnableFeignClients
注解,即使运行的代码只解释了其中一种客户端类型。通过这种方式,我就可以为我的示例共用一份代码。我会在遇到它们的时候指出这些冗余和代码。
src/main/java/com/thoughtmechanix/licenses/services/LicenseService.java 中的 LicenseService
类添加了一个名为 retrieveOrgInfo()
的简单方法,该方法将根据传递到路由的 clientType
类型进行解析,以用于查找组织服务实例。 LicenseService
类上的 getLicense()
方法将使用 retrieveOrgInfo()
方法从 Postgres 数据库中检索组织数据。代码清单 4-6 展示了 getLicense()
方法。
代码清单 4-6 getLicense()
方法将使用多个方法来执行 REST 调用
public License getLicense(String organizationId, String licenseId, String
→ clientType) {
License license = licenseRepository.findByOrganizationIdAndLicenseId(
→ organizationId, licenseId);
Organization org = retrieveOrgInfo(organizationId, clientType);
return license
.withOrganizationName( org.getName())
.withContactName( org.getContactName())
.withContactEmail( org.getContactEmail() )
.withContactPhone( org.getContactPhone() )
.withComment(config.getExampleProperty());
}
读者可以在 licensing-service 源代码的 src/main/java/com/thoughtmechanix/licenses/clients 包中找到使用 Spring DiscoveryClient、Spring RestTemplate 或 Feign 库构建的客户端。
4.5.1 使用 Spring DiscoveryClient 查找服务实例
Spring DiscoveryClient 提供了对 Ribbon 和 Ribbon 中缓存的注册服务的最低层次访问。使用 DiscoveryClient,可以查询通过 Ribbon 注册的所有服务以及这些服务对应的 URL。
接下来,我们将创建一个简单的示例,使用 DiscoveryClient 从 Ribbon 中检索组织服务 URL,然后使用标准的 RestTemplate
类调用该服务。要开始使用 DiscoveryClient,需要先使用 @EnableDiscoveryClient
注解来标注 src/main/java/com/thoughtmechanix/ licenses/Application. java 中的 Application
类,如代码清单 4-7 所示。
代码清单 4-7 创建引导类以使用 Spring Discovery Client
@SpringBootApplication
@EnableDiscoveryClient ⇽--- 激活 Spring DiscoveryClient
@EnableFeignClients ⇽--- 现在忽略这个注解,本章稍后将进行介绍
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@EnableDiscoveryClient
注解是 Spring Cloud 的触发器,其作用是使应用程序能够使用 DiscoveryClient 和 Ribbon 库。现在可以忽略 @EnableFeignClients
注解,因为本章稍后就会介绍它。
如代码清单 4-8 所示,我们现在来看看如何通过 Spring DiscoveryClient 调用组织服务。读者可以在 src/main/java/com/thoughtmechanix/licenses/OrganizationDiscovery Client.java 中找到这段代码。
代码清单 4-8 使用 DiscoveryClient 查找信息
/*为了简洁,省略了 package 和 import 部分*/
@Component
public class OrganizationDiscoveryClient {
@Autowired
private DiscoveryClient discoveryClient; ⇽--- DiscoveryClient 被自动注入这个类
public Organization getOrganization(String organizationId) {
RestTemplate restTemplate = new RestTemplate();
List<ServiceInstance> instances =
→ discoveryClient.getInstances("organizationservice"); ⇽--- 获取组织服务的所有实例的列表
if (instances.size()==0) return null;
String serviceUri = String.format("%s/v1/organizations/%s",
→ instances.get(0).getUri().toString(),
→ organizationId); ⇽--- 检索要调用的服务端点
ResponseEntity<Organization> restExchange = ⇽--- 使用标准的 Spring REST 模板类去调用服务
→ restTemplate.exchange(
→ serviceUri,
→ HttpMethod.GET,
→ null, Organization.class, organizationId);
return restExchange.getBody();
}
}
在这段代码中,我们首先感兴趣的是 DiscoveryClient
。这是用于与 Ribbon 交互的类。要检索通过 Eureka 注册的所有组织服务实例,可以使用 getInstances()
方法传入要查找的服务的关键字,以检索 ServiceInstance
对象的列表。
ServiceInstance
类用于保存关于服务的特定实例(包括它的主机名、端口和 URI)的信息。
在代码清单 4-8 中,我们使用列表中的第一个 ServiceInstance
去构建目标 URL,此 URL 可用于调用服务。一旦获得目标 URL,就可以使用标准的 Spring RestTemplate 来调用组织服务并检索数据。
DiscoveryClient 与实际运用
通过介绍 DiscoveryClient,我完成了使用 Ribbon 来构建服务消费者的过程。然而,在实际运用中,只有在服务需要查询 Ribbon 以了解哪些服务和服务实例已经通过它注册时,才应该直接使用 DiscoveryClient。上述代码存在以下几个问题。
- 没有利用 Ribbon 的客户端负载均衡 ——尽管通过直接调用 DiscoveryClient 可以获得服务列表,但是要调用哪些返回的服务实例就成为了开发人员的责任。
- 开发人员做了太多的工作 ——现在,开发人员必须构建一个用来调用服务的 URL。尽管这是一件小事,但是编写的代码越少意味着需要调试的代码就越少。
善于观察的 Spring 开发人员可能已经注意到,上述代码中直接实例化了
RestTemplate
类。这与正常的 Spring REST 调用相反,通常情况下,开发人员会利用 Spring 框架,通过@Autowired
注解将RestTemplate
注入使用RestTemplate
的类中。代码清单 4-8 实例化了
RestTemplate
类,这是因为一旦在应用程序类中通过@EnableDiscoveryClient
注解启用了 Spring DiscoveryClient,由 Spring 框架管理的所有RestTemplate
都将注入一个启用了 Ribbon 的拦截器,这个拦截器将改变使用 RestTemplate 类创建 URL 的行为。直接实例化 RestTemplate 类可以避免这种行为。总而言之,有更好的机制来调用支持 Ribbon 的服务。
4.5.2 使用带有 Ribbon 功能的 Spring RestTemplate 调用服务
接下来,我们将看到如何使用带有 Ribbon 功能的 RestTemplate
的示例。这是通过 Spring 与 Ribbon 进行交互的更为常见的机制之一。要使用带有 Ribbon 功能的 RestTemplate
类,需要使用 Spring Cloud 注解 @LoadBalanced
来定义 RestTemplate
bean 的构造方法。对于许可证服务,可以在 src/main/java/com/thoughtmechanix/licenses/Application.java 中找到用于创建 RestTemplate bean 的方法。
代码清单 4-9 展示了使用 getRestTemplate()
方法来创建支持 Ribbon 的 Spring RestTemplate
bean。
代码清单 4-9 标注和定义 RestTemplate
构造方法
package com.thoughtmechanix.licenses;
// 为了简洁,省略了大部分 import 语句
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication ⇽--- 因为我们在示例中使用了多种客户端类型,因此在代码中包含了这些注解。但是,在使用支持 Ribbon 的 RestTemplate 时,并不需要用到 @EnableDiscoveryClient 和 @EnableFeignClients,因此可以将它们移除
@EnableDiscoveryClient
@EnableFeignClients
public class Application {
@LoadBalanced ⇽--- @LoadBalanced 注解告诉 Spring Cloud 创建一个支持 Ribbon 的 RestTemplate 类
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
注意
在 Spring Cloud 的早期版本中,
RestTemplate
类默认自动支持 Ribbon。但是,自从 Spring Cloud 发布 Angel 版本之后,Spring Cloud 中的 RestTemplate 就不再支持 Ribbon。如果要将 Ribbon 和 RestTemplate 一起使用,则必须使用@LoadBalanced
注解进行显式标注。
既然已经定义了支持 Ribbon 的 RestTemplate
类,任何时候想要使用 RestTemplate
bean 来调用服务,就只需要将它自动装配到使用它的类中。
除了在定义目标服务的 URL 上有一点小小的差异,使用支持 Ribbon 的 RestTemplate
类几乎和使用标准的 RestTemplate
类一样。我们将使用要调用的服务的 Eureka 服务 ID 来构建目标 URL,而不是在 RestTemplate
调用中使用服务的物理位置。
让我们通过查看代码清单 4-10 来了解这一差异。代码清单 4-10 中的代码可以在 src/main/ java/com/thoughtmechanix/licenses/-clients/OrganizationRestTemplate. java 中找到。
代码清单 4-10 使用支持 Ribbon 的 RestTemplate
来调用服务
/*为了简洁,省略了 Package 和 impoot 部分*/
@Component
public class OrganizationRestTemplateClient {
@Autowired
RestTemplate restTemplate;
public Organization getOrganization(String organizationId){
ResponseEntity<Organization> restExchange = restTemplate.exchange(
→ "http://organizationservice/v1/organizations/{organizationId}", ⇽--- 在使用支持 Ribbon 的 Rest Template 时,使用 Eureka 服务 ID 来构建目标 URL
→ HttpMethod.GET,
→ null, Organization.class, organizationId);
return restExchange.getBody();
}
}
这段代码看起来和前面的例子有些类似,但是它们有两个关键的区别。首先,Spring(Cloud)DiscoveryClient 不见了;其次,读者可能会对 restTemplate.exchange()
调用中使用的 URL 感到奇怪:
restTemplate.exchange(
→ "http://organizationservice/v1/organizations/{organizationId}",
→ HttpMethod.GET,
→ null, Organization.class, organizationId);
URL 中的服务器名称与通过 Eureka 注册的组织服务的应用程序 ID——organizationervice 相匹配:
http:// {applicationid}
/v1/organizations/{organizationId}
启用 Ribbon 的 RestTemplate
将解析传递给它的 URL,并使用传递的内容作为服务器名称,该服务器名称作为从 Ribbon 查询服务实例的键。实际的服务位置和端口与开发人员完全抽象隔离。
此外,通过使用 RestTemplate
类,Ribbon 将在所有服务实例之间轮询负载均衡所有请求。
4.5.3 使用 Netflix Feign 客户端调用服务
Netflix 的 Feign 客户端库是 Spring 启用 Ribbon 的 RestTemplate
类的替代方案。Feign 库采用不同的方法来调用 REST 服务,方法是让开发人员首先定义一个 Java 接口,然后使用 Spring Cloud 注解来标注接口,以映射 Ribbon 将要调用的基于 Eureka 的服务。Spring Cloud 框架将动态生成一个代理类,用于调用目标 REST 服务。除了编写接口定义,开发人员不需要编写其他调用服务的代码。
要在许可证服务中允许使用 Feign 客户端,需要向许可证服务的 src/main/java/com/ thoughtmechanix/licenses/Application.java 添加一个新注解 @EnableFeignClients
。代码清单 4-11 展示了这段代码。
代码清单 4-11 在许可证服务中启用 Spring Cloud/Netflix Feign 客户端
@SpringBootApplication ⇽--- 因为现在只使用 Feign 客户端,读者可以在代码中移除 @EnableDiscoveryClient 注解
@EnableDiscoveryClient
@EnableFeignClients ⇽--- 需要使用 @EnableFeign Clients 以在代码中启用 Feign 客户端
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
既然已经在许可证服务中启用了 Feign 客户端,那么我们就来看一个 Feign 客户端接口定义,它可以用来调用组织服务上的端点。代码清单 4-12 展示了一个接口定义示例,这段代码可以在 src/main/java/com/thoughtmechanix/licenses/clients/OrganizationFeignClient.java 中找到。
代码清单 4-12 定义用于调用组织服务的 Feign 接口
/*为了简洁,省略了 package 和 import 部分*/
@FeignClient("organizationservice") ⇽--- 使用 @FeignClient 注解标识服务
public interface OrganizationFeignClient {
@RequestMapping(
→ method= RequestMethod.GET,
→ value="/v1/organizations/{organizationId}",
→ consumes="application/json") ⇽--- 使用 @RequestMapping 注解来定义端点的路径和动作
Organization getOrganization(
→ @PathVariable("organizationId") String organizationId); ⇽--- 使用 @PathVariable 来定义传入端点的参数
}
我们通过使用 @FeignClient
注解来开始这个 Feign 示例,并将这个接口代表的服务的应用程序 ID 传递给它。接下来,在这个接口中定义一个 getOrganization()
方法,该方法可以由客户端调用以触发组织服务。
定义 getOrganization()
方法的方式看起来就像在 Spring 控制器类中公开一个端点一样。首先,为 getOrganization()
方法定义一个 @RequestMapping
注解,该注解映射 HTTP 动词以及将在组织服务中公开的端点。其次,使用 @PathVariable
注解将 URL 上传递的组织 ID 映射到调用的方法的 organizationId
参数。调用组织服务的返回值将被自动映射到 Organization
类,这个类被定义为 getOrganization()
方法的返回值类型。
要使用 OrganizationFeignClient
类,开发人员需要做的只是自动装配并使用它。Feign 客户端代码将为开发人员承担所有的编码工作。
错误处理
在使用标准的 Spring
RestTemplate
类时,所有服务调用的 HTTP 状态码都将通过ResponseEntity
类的getStatusCode()
方法返回。通过 Feign 客户端,任何被调用的服务返回的 HTTP 状态码 4xx ~ 5xx 都将映射为FeignException
。FeignException
包含可以被解析为特定错误消息的 JSON 体。Feign 为开发人员提供了编写错误解码器类的功能,该类可以将错误映射回自定义的异常类。有关编写错误解码器的内容超出了本书的范围,读者可以在 Feign GitHub 存储库中找到与此相关的示例。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论