vlambda博客
学习文章列表

读书笔记《building-a-restful-web-service-with-spring》构建REST客户端

Chapter 9. Building a REST Client

到目前为止,我们已经专注于 RESTful Web 服务的服务器端,让我们扭转局面,看看我们如何使用 Spring 为我们的示例财产管理系统构建客户端。在本章中,我们将讨论以下主题:

  • 使用 Spring 构建 RESTful 服务客户端的基本设置

  • 调用服务端点

  • 远程与本地客户端

  • 处理安全

  • 异常处理

正如我们将在本章中看到的那样,Spring 提供了有用的工具来快速有效地构建客户端。

The basic setup


客户端库应该是自包含和可移植的,以便 RESTful Web 服务的消费者可以使用它们。在服务器端和客户端 端之间共享代码会很诱人。但是,这样做会使客户端和服务器代码紧密耦合并阻碍客户端库的可移植性。

在我们的示例属性管理服务的上下文中,我们将客户端库构建为一个新的 Maven 模块。该模块将需要以下依赖项:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
  <version>4.1.6.RELEASE</version>
</dependency>
<dependency>
  <groupId>commons-logging</groupId>
  <artifactId>commons-logging</artifactId>
  <version>1.1.3</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.3.2</version>
</dependency>

要使用 Spring 构建 RESTful 客户端,我们至少需要对 Spring 的 web 模块的依赖。此外,Spring 在运行时需要 Apache commons-logging 库。最后,我们将利用 Jackson 来执行我们的 Java 类与其 JSON 表示之间的数据绑定。

Declaring a client

有了这些 依赖项,让我们直接开始为我们的示例物业管理系统构建一个客户端。我们将从 Inventory 组件开始,使用以下(简化的)客户端界面:

public interface InventoryServiceClient {
  public Room getRoom(long roomId);
}

该接口允许通过其标识符检索房间。

Note

下一节将讨论为什么将我们的客户端声明为接口是一个好主意。

我们现在可以开始实现我们的客户端:

public class RemoteInventoryServiceClient implements InventoryServiceClient {

  private final String serviceUrl;
  private final RestTemplate template;

  public RemoteInventoryServiceClient(String serviceUrl) {
    this.serviceUrl = serviceUrl;
    template = new RestTemplate();
  }

  @Override
  public Room getRoom(long roomId) {
    ParameterizedTypeReference<ApiResponse<Room>> typeReference = new ParameterizedTypeReference<ApiResponse<Room>>() {};
    return (Room) ResponseHandler.handle(
      () -> template.exchange(serviceUrl + "/rooms/" + roomId, HttpMethod.GET, null, typeReference).getBody());
  }
}

Spring 为我们提供的主要类是 org.springframework.web.client.RestTemplate。这个类允许我们使用 RestTemplate.exchange() 对后端服务器进行 HTTP 调用。

正如第4章中所讨论的,数据表示,我们的服务响应以通用信封格式包装,其中包含有关请求状态的信息。由于所有响应都将具有相同的格式,我们可以创建一个实用方法来以一致的方式处理它们。这就是 ResponseHandler.handle() 的目的。

由于我们使用 通用类型来指定要包含的有效负载响应的类型,因此我们需要将该信息传递给 Spring,以便它可以正确提取数据。这是通过声明 ParameterizedTypeReference 来实现的。

Note

请注意,JSON 封送处理不需要特殊设置。由于杰克逊在类路径中,它会自动发生。

Remote versus local clients


正如我们之前在本章中看到的,我们的客户端是使用接口定义的。这种增加抽象级别的主要动机是我们可以用远程实现 代替本地实现。假设组件 A 依赖于组件 B。如果这些组件部署在不同的服务器上,我们将希望使用 B 的客户端实现远程调用组件。但是,如果两个组件共存于同一个 JVM 中,则使用远程客户端会导致不必要的网络延迟。将客户端替换为直接调用组件 B 的 Java 实现的客户端可确保不会发生网络连接,从而减少延迟。

Note

这种模式通常用于微服务架构(https://en.wikipedia.org/wiki/Microservices),它们变得非常流行。这种架构风格提倡将复杂的应用程序分解成小的独立组件。

Availability and booking services

让我们看一下示例物业管理系统的可用性和预订组件。可用性服务取决于 预订服务。为了解决这个问题,我们为预订服务定义了一个简单的客户端接口,如下所示:

public interface BookingServiceClient {

    /**
     * Looks up the booking with the given identifier.
     *
     * @param bookingId the booking identifier to look up
     *
     * @return the booking with the given ID
     */
    public Booking getBooking(long bookingId);
}

这个客户端接口定义了一个单一的方法来通过它的标识符来查找一个预订。

我们的第一个部署方法是让两个组件在不同的 JVM 中运行。因此,我们实现了可用性服务可以利用的远程客户端:

public class RemoteBookingServiceClient implements BookingServiceClient {

  private final String serviceUrl;
  private final RestTemplate template;

  public RemoteBookingServiceClient(String serviceUrl) {
    if (serviceUrl == null) {
      throw new IllegalArgumentException("serviceUrl cannot be null");
    }
    this.serviceUrl = serviceUrl;
    template = new RestTemplate();
  }

  @Override
  public Booking getBooking(long bookingId) {
    //omitted to clarity
    return (Booking) ResponseHandler.handle(
      () -> template.exchange(serviceUrl + "/bookings/" + bookingId, HttpMethod.GET, null, typeReference).getBody());
  }
}

在本章的前面部分,我们已经介绍了如何使用 org.springframework.web.client.RestTemplate 来远程调用服务。

现在,为了减少 延迟,我们决定在同一个JVM 中运行两个 组件.使用此客户端实现将抵消在同一进程中托管两种服务的任何好处。因此,我们需要一个新的实现:

public class LocalBookingServiceClient implements BookingServiceClient {

  @Autowired
  private BookingService bookingService;

  @Override
  public Booking getBooking(long bookingId) {
    com.packtpub.springrest.model.Booking booking = bookingService.getBooking(bookingId);
    Booking clientBooking = new Booking();
    clientBooking.setId(booking.getId());
    // omitted setting other fields for clarity
    return clientBooking;
  }
}

通过这个新实现,我们只需将预订检索委托给实际服务,绕过任何网络。然后,我们需要将数据转换为客户端调用者所期望的。

Handling security


第7章中,处理安全,我们 学会了将安全性应用于 RESTful 端点。例如,我们讨论了如何为预订服务设置 HTTP Basic 身份验证。我们可以扩展上一节的示例并添加安全处理。接下来的两节说明如何处理 BasicDigest 身份验证。

The Basic authentication

此身份验证方案要求 Authorization 标头包含以 Base64 编码的用户名/密码 对。这很容易通过如下修改客户端来实现:

public RemoteBookingServiceClient(String serviceUrl, String username, String password) {

  template = new RestTemplate();
  String credentials = Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
  template.getInterceptors().add((request, body, execution) -> {
    request.getHeaders().add("Authorization", "Basic " + credentials);
    return execution.execute(request, body);
  });
}

这个新的构造函数使用用户名和密码来验证客户端。然后它生成 Base64 编码的凭据,并为了将 Authorization 令牌添加到每个请求,定义一个新的 org.springframework.http。 client.ClientHttpRequestInterceptor。拦截器以适当的格式添加标头并允许执行请求。

The Digest authentication

对于这个认证方案,我们来看看开发者如何选择Apache的HttpClient (https://hc.apache.org ) 作为底层 HTTP 客户端框架。为此,我们需要在我们的项目中添加以下依赖项:

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpclient</artifactId>
  <version>4.3.4</version>
</dependency>

而且,我们改变了创建 RestTemplate 实例的方式:

CredentialsProvider provider = new BasicCredentialsProvider();
CloseableHttpClient client = HttpClientBuilder.create().setDefaultCredentialsProvider(provider).build();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password);
provider.setCredentials(AuthScope.ANY, credentials);
RestTemplate template = new RestTemplate(new DigestAuthHttpRequestFactory(host, client));

通过将请求工厂传递给我们的模板,我们能够利用 HttpClient 来管理 Digest 身份验证。我们需要创建一个HttpComponentsClientHttpRequestFactory的扩展,如下:

public class DigestAuthHttpRequestFactory extends HttpComponentsClientHttpRequestFactory {

  @Override
  protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
        AuthCache authCache = new BasicAuthCache();
        authCache.put(host, new DigestScheme());
        BasicHttpContext localcontext = new BasicHttpContext();
        localcontext.setAttribute(AUTH_CACHE, authCache);
        return localcontext;
    }
}

此类创建一个 HTTP 上下文,该上下文设置为存储 Digest 凭据。我们通过创建 org 的新实例来指示 HttpClient 使用 Digest 方案处理身份验证。 apache.http.impl.auth.DigestScheme。通过这种设置,我们的客户端类透明地处理身份验证,其方式与上一节中处理 Basic 身份验证方案的方式相同。

HTTP public key pinning

HTTP public key pinning (HPKP) 是一个 Trust on First Use 防止假冒的安全技术 的欺诈性 SSL 证书。来自支持 HPKP 的 RESTful Web 服务的响应将包括标头 Public-Key-Pins,其中包含 哈希SSL 证书的主题公钥信息 (SPKI)。客户端应用程序应在第一次收到此值时缓存该值,并使用先前缓存的标头值验证任何后续响应。如果值不匹配,客户端应用程序可以阻止与欺诈服务器的进一步通信。

虽然 Spring 框架不提供开箱即用的 HPKP 支持,但通过使用请求过滤器来实现此安全功能非常简单。

Note

您可以在 HTTP 公钥签名的更多信息" target="_blank">https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning。

Exception handling


第 3 章第一个端点,触及定义一致的响应格式,包括错误代码和错误描述。客户端的推论是定义一个异常, 封装有关特定调用失败原因的信息。因此,我们可以定义以下异常:

public class ClientException extends RuntimeException {

  private final int errorCode;
  private final String errorDescription;

  public ClientException(ApiError error) {
    super(error.getErrorCode() + ": " + error.getDescription());
    this.errorCode = error.getErrorCode();
    this.errorDescription = error.getDescription();
  }

  public int getErrorCode() {
    return errorCode;
  }

  public String getErrorDescription() {
    return errorDescription;
  }
}

使用具有此类异常处理的 RESTful Web 服务的客户端将具有一致的方式来管理和报告错误。但是,在某些情况下,通用异常处理需要在客户端进行更多工作。例如,当服务器无法处理当前负载并要求客户端在延迟几秒钟后重新发出请求时,它可能会生成特定错误。

在这种情况下,如果服务消费者能够捕获特定异常,而不是必须自省一般异常,那么他们的工作就会变得更轻松。客户端实现可以声明一个ClientException的扩展,例如,ServerOverloadedClientException,并在相关的地方抛出此异常而不是通用异常。

细心的读者会注意到我们将 ClientException 声明为未经检查的异常。这使我们能够管理我们的异常处理而不暴露它。

Note

使用已检查或未检查异常的决定通常更多是个人偏好,而不是出于技术动机的选择。需要在客户端库的每个方法上捕获异常可能会相当麻烦。同样,不必捕获异常可能会导致消费者无法处理他们本来可以恢复的错误情况。

Summary


在倒数第二章中,您学会了构建 RESTful Web 服务客户端、使用身份验证以及处理异常处理。这很好地完成了使用 Spring Framework 构建 RESTful Web 服务的故事。

在本书的最后一章中,我们将研究如何部署和扩展此类服务以处理大量请求。