vlambda博客
学习文章列表

读书笔记《building-a-restful-web-service-with-spring》数据表示

Chapter 4. Data Representation

我们在上一章中了解了如何构建 RESTful 端点。我们还简要讨论了数据如何在 REST 响应中表示。在本章中,我们将扩展这些讨论并涵盖以下主题:

  • 数据传输对象设计模式

  • 控制 JSON 中的响应格式

  • 格式化回复的提示

  • API 演进

在深入研究如何控制 JSON 响应的细节之前,让我们先看一下 DTO 设计模式。

The Data-Transfer-Object design pattern


数据传输对象是一个简单的属性包装器,它在应用程序的层之间传递。这种模式在数据如何在内部存储和管理以及如何表示之间提供了一个很好的抽象级别。

此类对象通常不定义业务逻辑,而只是履行数据容器的角色。例如,在示例物业管理 Web 服务的上下文中,我们为 Rooms 声明了一个 DTO 类。以下代码片段说明了这个 DTO 类:

public class RoomDTO implements Serializable {

    private static final long serialVersionUID = 2682046985632747474L;

    private long id;
    private String name;
    private long roomCategoryId;
    private String description;

    public RoomDTO(Room room) {
        this.id = room.getId();
        this.name = room.getName();
        this.roomCategoryId = room.getRoomCategory().getId();
        this.description = room.getDescription();
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public long getRoomCategoryId() {
        return roomCategoryId;
    }

    public String getDescription() {
        return description;
    }
}

在此示例中,我们定义了一个房间的属性,我们希望这些属性可用于我们服务的数据层(在本例中,是我们 API 的 JSON 响应)。我们的数据层对象作为构造函数参数传递,以便可以轻松初始化DTO 对象。

DTO 对象不需要实现 java.io.Serializable。但是,这样做会很有用,以便在不同的 JVM 中运行应用程序的不同层。

Note

您可以在 " target="_blank">http://en.wikipedia.org/wiki/Data_transfer_object

读者可能也知道这种 模式是Value-Object 设计模式。早期的 Java EE 文献错误地使用该术语来描述数据传输对象的概念。从语义上讲,VO 模式与 DTO 设计模式有很大不同。 DTO 对象是模型对象的定制版本,VO 对象表示固定的数据集,类似于 Java enum。它们通常由它们的值来标识并且是不可变的。

Tip

当使用 Java Persistence API 时(就像我们在使用 Hibernate 的示例 Web 服务中所做的那样),DTO 变得非常有用。在一个类中混合持久性和表示问题 非常棘手,并且经常导致冲突。

The API response format


由 API 设计人员决定哪种格式最适合他们的用例。话虽如此,采用通用响应信封格式是一种很好的做法。通过这种方法,RESTful Web 服务提供了一个 统一接口,使客户端开发人员能够以一致的方式处理响应,而不管调用的操作如何。

下一节提供了一个示例信封格式。

The envelope format

与任何操作相关的第一条信息 是它是否成功。我们可以用状态封装这些信息。其次,大多数请求都会返回数据。因此,我们信封中的字段可以提供对响应负载的通用访问。以下格式将构成 API 返回的任何响应的基础:

{
  "status": "OK",
  "data": {…}
}

使用这种响应格式,我们确保客户端开发人员能够以一致的方式检查请求是否成功并访问有效负载。

Error management

知道请求失败很重要,但从表面上看,并不是很有用。理想情况下,我们应该提供一些关于请求失败原因的详细信息。例如,请求可能具有无效的参数值,或者请求者可能无权执行操作。

错误处理是设计健壮且记录良好的 API 的一个重要方面。由于大多数 RESTful Web 服务都是通过 HTTP/HTTPS 访问的,因此我们可以利用 HTTP 状态代码对错误进行分类并提供有关请求错误的线索。

Note

例如,如果不允许操作,服务器应返回 403 HTTP 错误代码,如果操作不被允许,则应返回 400 HTTP 错误代码请求参数无效。

除了 HTTP 响应代码之外,API 开发人员可能希望提供自己的错误代码来响应,从而为服务消费者提供有关操作失败原因的更多线索。为了方便这一点,我们可以修改我们建议的信封格式以包含错误属性:

{
  "status": "ERROR",
  "data": null,
  "error": {
    "errorCode": 999,
    "description": "Email address is invalid"
  }
}

在此示例中,操作无法成功完成,因为传递了无效的电子邮件地址。我们的响应包含一个错误属性,指定了特定于应用程序的错误代码和描述。

Pagination support

为了使返回资源列表的操作具有可扩展性,它们必须提供某种形式的分页。例如,在我们的 示例 Web 服务中,我们提供了一个端点,列出了给定类别中的所有房间,如 第 3 章第一个端点。因此,我们可以扩展我们建议的信封格式以在这种情况下包含分页信息。让我们考虑以下 JSON 响应:

{
    "status": "OK",
    "data": [...],
    "error": null,
    "pageNumber": 1,
    "nextPage": "http://localhost:8080/rooms?categoryId=1&page=2",
    "total": 13
}

除了标准属性之外,我们还包括返回的页码以及可用资源的总数,以及对下一页资源的引用。

Customizing JSON responses

为了遵守 准则或要求,API 设计人员可能希望控制 JSON 响应的格式。正如第3章中提到的,第一个端点 , Spring Web 利用 Jackson 来执行 JSON 序列化。因此,要自定义我们的 JSON 格式,我们必须配置 Jackson 处理器。 Spring Web 提供了基于 XML 或基于 Java 的方法来处理配置。下面,我们将看看基于 Java 的配置。

假设我们需要使用带下划线的小写属性名称,而不是驼峰式。为了减少响应的大小,我们还被要求不要包含具有 null 值的属性。

默认情况下,响应格式如下:

{
    "status": "OK",
    "data": {
    "id": 1,
    "name": "Room 1",
    "roomCategoryId": 1,
    "description": "Nice, spacious double bed room with usual amenities"
    },
    "error": null
}

如果我们创建一个 WebMvcConfigurerAdapter 扩展,那么我们就可以指导 Jackson 如何格式化 JSON 消息:

@Configuration
@EnableWebMvc
public class JsonConfiguration extends WebMvcConfigurerAdapter {

  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(new MappingJackson2HttpMessageConverter(new Jackson2ObjectMapperBuilder()
    .propertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES)
    .serializationInclusion(Include.NON_NULL)
    .build()));
  }
}

当应用程序启动时,Spring 会发现这个类(感谢 @Configuration 注解),并添加我们的转换器 MappingJackson2HttpMessageConverter .我们将此转换器配置为使用下划线将属性名称格式化为小写,并告诉 Jackson 忽略 null 属性。生成的 JSON 格式如下:

{
  "status": "OK",
  "data": {
    "id": 1,
    "name": "Room 1",
    
"room_category_id": 1,
    "description": "Nice, spacious double bed room with usual amenities"
  }
}

我们可以看到 现在error属性已经消失了,房间类别ID属性名称在我们被要求使用的格式。

Note

org.springframework.http.converter.json.Jackson2ObjectMapperBuilder 提供了许多 更多选项来控制 JSON 格式。 Javadoc 位于 http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.html

本节为读者提供了控制响应格式的基本工具。这些响应格式应被视为服务 API 的一部分,因此应谨慎管理对现有响应的修改。下一节将讨论如何使用版本控制来管理这些问题。

API evolutions


系统随着时间的推移而发展,并且它们所公开的功能随着这些发展而变化。 API,是 它以库或 RESTful API 的形式,必须适应这些变化,同时保持某种形式的向后兼容性。

因此,在设计 API 时考虑演变是一个好主意。下一节将介绍如何 HATEOAS (超媒体作为应用程序状态的引擎)可以用来管理演进。

HATEOAS

这个 REST 原则为服务消费者提供了一种自我发现的方法。让我们考虑以下对 RESTful 端点的响应:

{
  "id": 1,
  "category": "http://myservice.com/categories/33",
...
}

此响应包括 到相关资源的超媒体链接,因此消费者不需要事先知道从何处获取资源。

使用这种方法,服务设计人员可以通过在发生变化时引入新的超媒体链接来管理演进,并在不需要修改服务使用者的情况下淘汰旧链接。它是一种强大的机制,可以帮助有效地管理服务演进。

Versioning strategies

虽然纯粹主义者会不同意在 RESTful Web 服务中使用版本控制,但在现实世界中,版本控制为 管理 API 的主要演变。由于资源处于 REST 的中心,版本控制的自然位置是在 URI 中。

URI versioning

让我们考虑 URI /rooms/{roomId} 之后的 ,它提供了对房间资源的访问,如所述在 Data-Transfer-Object 设计模式部分。我们想添加一个新字段,其中包含与房间图片有关的 URL (pictureUrl)。这样的更改是前向兼容的,并且不需要对客户端进行特殊处理即可继续使用该服务。

现在,我们意识到拥有不止一张图片可以提高用户参与度。我们可以简单地添加一个新字段以及现有的图片 URL。但是,将图片 URL 字段替换为 URL 列表 (pictureUrls) 更有意义。此更改不 向后兼容。因此,我们需要创建一个新版本的服务,以确保现有客户能够继续工作。为此,我们将版本包含在资源 URI 中。

通过这种方法,/rooms/{roomId} 变为 /v{versionNumber}/rooms/{roomId}。例如,在我们的第一个 API 中 ID #1 的房间的 URI 是 /v1.0/rooms/1,它将包含 pictureUrl 字段。我们新版本中的房间将可以通过 /v1.1/rooms/1 访问并返回图片 URL 列表。

Tip

基于 URI 的版本的一个好的做法是将非版本 URI 别名为最新的 API 版本。所以,如果我们的 API 的最新版本是 2.0,/rooms/1 应该是 /v2.0/rooms/1< /代码>。

Representation versioning

在前面的示例中,我们生成了 Rooms 的不同表示。因此,我们可以考虑使用 MIME 类型版本控制来管理版本控制。以下代码片段说明了我们如何使用 Spring Web 实现这一点:

@RequestMapping(value = "/{roomId}", method = RequestMethod.GET)
public RoomDTO getRoom(@PathVariable("roomId") long id) {
  Room room = inventoryService.getRoom(id);
  return new RoomDTO(room);
}

@RequestMapping(value = "/{roomId}", method = RequestMethod.GET, consumes = "application/json;version=2")
public RoomDTOv2 getRoomV2(@PathVariable("roomId") long id) {
  Room room = inventoryService.getRoom(id);
  return new RoomDTOv2(room);
}

使用此设置,当请求的 Content-Type 标头设置为 application/json;version=2。否则,getRoom() 将处理房间访问请求。

Other approaches

除了前面描述的两种方法外,还可以考虑使用其他解决方案。例如,专用标头(例如 X-API-Version)可用于指定哪个 API 版本 应该被使用。以下代码将处理获取我们 API 版本 3 的房间的请求:

@RequestMapping(value = "/{roomId}", method = RequestMethod.GET, headers = {"X-API-Version=3"})
public RoomDTOv3 getRoomV3(@PathVariable("roomId") long id) {
  Room room = inventoryService.getRoom(id);
  return new RoomDTOv3(room);
}

在这个例子中,我们指示 Spring 将请求映射到包含标头 X-API-Version/rooms/{roomId} ,此方法的值为 3

无论选择采用哪种技术,API 设计人员都应牢记以下原则:

  • 只暴露需要的东西;保持向后兼容性是一项艰巨的工作。

  • 支持前向兼容的更改而不是破坏性的更改。强制客户端升级到新的 API 版本并不总是可行的。

  • 从一开始,设计 API 就支持进化。根据选择的方法,将此类支持反向移植到 API 中可能并不简单。

Summary


在本章的过程中,我们发现了 Data-Transfer-Object 设计模式。我们研究了设计响应的良好做法,以及如何控制其格式。最后,我们讨论了版本控制以及如何在实践中使用它来管理 API 演进。

现在是继续实施我们的示例物业管理系统 Web 服务的时候了。在下一章中,我们将深入研究如何使用 Spring Web 在 RESTful Web 服务中处理 CRUD 操作。