使用头部进行条件操作

本节介绍 Spring Data REST 如何使用标准 HTTP 头部来增强性能、对操作进行条件化处理,并有助于构建更复杂的的客户端。

ETagIf-MatchIf-None-Match 头部

ETag 头部 提供了一种标记资源的方法。这可以防止客户端相互覆盖,同时还可以减少不必要的调用。

请考虑以下示例

示例 1. 带有版本号的 POJO
class Sample {

	@Version Long version; (1)

	Sample(Long version) {
		this.version = version;
	}
}
1 @Version 注解(如果您使用的是 Spring Data JPA,则为 JPA 注解,对于所有其他模块,则为 Spring Data 的 org.springframework.data.annotation.Version 注解)将此字段标记为版本标记。

在前面的示例中,当 Spring Data REST 将 POJO 作为 REST 资源提供服务时,它会带有一个 ETag 头部,其值为版本字段的值。

如果我们提供一个 If-Match 头部,例如以下内容,我们可以有条件地 PUTPATCHDELETE 该资源

curl -v -X PATCH -H 'If-Match: <value of previous ETag>' ...

只有当资源的当前 ETag 状态与 If-Match 头部匹配时,才会执行操作。此安全措施可防止客户端相互覆盖。两个不同的客户端可以获取资源并具有相同的 ETag。如果一个客户端更新资源,则它会在响应中获得一个新的 ETag。但第一个客户端仍然拥有旧的头部。如果该客户端尝试使用 If-Match 头部进行更新,则更新将失败,因为它们不再匹配。相反,该客户端会收到 HTTP 412 Precondition Failed 消息以发送回。然后,客户端可以根据需要进行后续处理。

术语“版本”可能在不同的数据存储甚至应用程序中具有不同的语义。Spring Data REST 有效地委托给数据存储的元模型来判断一个字段是否已版本化,如果是,则仅在 ETag 元素匹配时才允许列出的更新。

If-None-Match 头部 提供了一种替代方法。If-None-Match 不用于条件更新,而是用于条件查询。请考虑以下示例

curl -v -H 'If-None-Match: <value of previous etag>' ...

前面的命令(默认情况下)运行 GET。Spring Data REST 在执行 GET 时会检查 If-None-Match 头部。如果头部与 ETag 匹配,则它会得出结论,即没有任何更改,并且不会发送资源副本,而是发送回 HTTP 304 Not Modified 状态码。从语义上讲,它表示“如果提供的头部值与服务器端版本不匹配,则发送整个资源。否则,不发送任何内容。”

此 POJO 来自基于 ETag 的单元测试,因此它没有 @Entity(JPA)或 @Document(MongoDB)注解,正如应用程序代码中预期的那样。它仅关注带有 @Version 的字段如何导致 ETag 头部。

If-Modified-Since 头部

If-Modified-Since 头部 提供了一种方法来检查资源自上次请求以来是否已更新,这使应用程序能够避免重新发送相同的数据。请考虑以下示例

示例 2. 在域类型中捕获的上次修改日期
@Document
public class Receipt {

	public @Id String id;
	public @Version Long version;
	public @LastModifiedDate Date date;  (1)

	public String saleItem;
	public BigDecimal amount;

}
1 Spring Data Commons 的 @LastModifiedDate 注解允许以多种格式捕获此信息(JodaTime 的 DateTime、旧版 Java DateCalendar、JDK8 日期/时间类型以及 long/Long)。

使用前面的示例中的日期字段,Spring Data REST 会返回类似于以下内容的 Last-Modified 头部

Last-Modified: Wed, 24 Jun 2015 20:28:15 GMT

此值可以被捕获并用于后续查询,以避免在数据未更新时再次获取相同的数据,如下例所示

curl -H "If-Modified-Since: Wed, 24 Jun 2015 20:28:15 GMT" ...

使用前面的命令,您要求仅在资源自指定时间以来已更改时才获取资源。如果是,则您会获得一个修订后的 Last-Modified 头部,用于更新客户端。如果不是,您将收到 HTTP 304 Not Modified 状态码。

此头部格式完美,可用于未来的查询。

不要将头部值与不同的查询混合使用。结果可能是灾难性的。仅在您请求完全相同的 URI 和参数时使用头部值。

构建更高效的前端

ETag 元素与 If-MatchIf-None-Match 头部相结合,使您可以构建一个对用户数据计划和移动设备电池寿命更友好的前端。为此

  1. 确定需要锁定实体并添加版本属性。

    HTML5 很好地支持 data-* 属性,因此将版本存储在 DOM 中(例如 data-etag 属性)。

  2. 确定哪些条目将受益于跟踪最近的更新。在获取这些资源时,将 Last-Modified 值存储在 DOM 中(可能是 data-last-modified)。

  3. 在获取资源时,还在 DOM 节点中嵌入 self URI(可能是 data-uridata-self),以便轻松返回到资源。

  4. 调整PUT/PATCH/DELETE操作以使用If-Match,并处理HTTP 412 Precondition Failed状态码。

  5. 调整GET操作以使用If-None-MatchIf-Modified-Since,并处理HTTP 304 Not Modified状态码。

通过在您的DOM(或可能是原生移动应用中的其他位置)中嵌入ETag元素和Last-Modified值,您可以通过避免重复获取相同内容来减少数据消耗和电池电量。您还可以避免与其他客户端发生冲突,并且在需要协调差异时获得提醒。

通过这种方式,只需对前端进行少量调整和一些实体级编辑,后端就能提供时间敏感的详细信息,您可以在构建客户友好的客户端时利用这些信息。