基于头部的条件操作

本节将展示 Spring Data REST 如何使用标准的 HTTP 头部来提升性能、执行条件操作,并有助于构建更精巧的前端。

ETagIf-MatchIf-None-Match 头部

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

请看以下示例

示例 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 注解)将此字段标记为版本标识。

上述示例中的 POJO,当 Spring Data REST 将其作为 REST 资源提供时,会有一个 ETag 头部,其值为版本字段的值。

如果我们提供一个如下所示的 If-Match 头部,我们可以有条件地对该资源执行 PUTPATCHDELETE 操作

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

只有当资源的当前 ETag 状态与 If-Match header 匹配时,操作才会被执行。这种保障措施可以防止客户端相互覆盖。两个不同的客户端可以获取资源并拥有相同的 ETag。如果一个客户端更新了资源,它会在响应中获得一个新的 ETag。但第一个客户端仍然持有旧的 header。如果该客户端尝试使用 If-Match header 进行更新,更新会失败,因为它们不再匹配。相反,该客户端会收到一个 HTTP 412 Precondition Failed 消息。客户端随后可以根据需要进行追赶。

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

The If-None-Match header 提供了一种替代方案。与条件更新不同,If-None-Match 允许进行条件查询。请考虑以下示例

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

前面的命令(默认情况下)运行一个 GET。Spring Data REST 在执行 GET 时会检查 If-None-Match header。如果 header 与 ETag 匹配,它会断定资源没有改变,并且不发送资源的副本,而是返回一个 HTTP 304 Not Modified 状态码。从语义上讲,它的意思是“如果提供的 header 值与服务器端的版本不匹配,则发送整个资源。否则,不发送任何东西。”

这个 POJO 来自一个基于 ETag 的单元测试,因此它不包含应用代码中通常会有的 @Entity (JPA) 或 @Document (MongoDB) 注解。它仅关注带有 @Version 的字段如何生成 ETag 头部。

If-Modified-Since 头部

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

示例 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 header,用以更新客户端。如果没有变化,您会收到一个 HTTP 304 Not Modified 状态码。

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

请勿在不同的查询中混用头部值。结果可能会是灾难性的。仅在请求完全相同的 URI 和参数时使用这些头部值。

构建更高效的前端架构

ETag 元素与 If-MatchIf-None-Match header 结合使用,可以让您构建一个对用户的流量套餐和手机电池寿命更友好的前端。要实现这一点

  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 状态码。

通过将 ETag 元素和 Last-Modified 值嵌入到您的 DOM 中(或者对于原生移动应用,可能嵌入到其他地方),您可以通过不反复检索相同的内容来减少数据和电池电量的消耗。您还可以避免与其他客户端冲突,并在需要协调差异时收到警报。

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