ETags 与 Spring 集成
1. 概述
本文将重点介绍在 Spring中使用 ETags、REST API 的集成测试以及使用curl的消费场景。
2. REST 和 ETag
来自有关 ETag 支持的官方 Spring 文档:
ETag (实体标签)是由符合 HTTP/1.1 的 Web 服务器返回的HTTP 响应标头,用于确定给定 URL 的内容更改。
我们可以将 ETag 用于两件事——缓存和条件请求。ETag 值可以被认为是从响应体的字节中计算出来的哈希值。因为服务可能使用加密散列函数,所以即使是对主体的最小修改也会极大地改变输出,从而改变 ETag 的值。这仅适用于强 ETag——该协议也提供了弱 Etag 。
**使用 If-* 标头将标准 GET 请求转换为条件 GET。**与 ETag 一起使用的两个 If-* 标头是“ If-None-Match ”和“ If-Match ”——每个都有自己的语义,如本文后面所述。
3. 使用curl的客户端-服务器通信
我们可以将涉及 ETag 的简单客户端-服务器通信分解为以下步骤:
首先,客户端进行 REST API 调用——响应包括将存储以供进一步使用的** ETag 标头:**
curl -H "Accept: application/json" -i http://localhost:8080/spring-boot-rest/foos/1
HTTP/1.1 200 OK
ETag: "f88dd058fe004909615a64f01be66a7"
Content-Type: application/json;charset=UTF-8
Content-Length: 52
**对于下一个请求,客户端将包含If-None-Match请求标头和上一步中的 ETag 值。**如果服务器上的资源未更改,则响应将不包含正文和状态代码304 – Not Modified:
curl -H "Accept: application/json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"'
-i http://localhost:8080/spring-boot-rest/foos/1
HTTP/1.1 304 Not Modified
ETag: "f88dd058fe004909615a64f01be66a7"
现在,在再次检索资源之前,让我们通过执行更新来更改它:
curl -H "Content-Type: application/json" -i
-X PUT --data '{ "id":1, "name":"Transformers2"}'
http://localhost:8080/spring-boot-rest/foos/1
HTTP/1.1 200 OK
ETag: "d41d8cd98f00b204e9800998ecf8427e"
Content-Length: 0
最后,我们发出最后一个请求以再次检索 Foo。请记住,自上次请求以来我们已经对其进行了更新,因此之前的 ETag 值应该不再起作用。响应将包含新数据和新的 ETag,可以再次存储以供进一步使用:
curl -H "Accept: application/json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"' -i
http://localhost:8080/spring-boot-rest/foos/1
HTTP/1.1 200 OK
ETag: "03cb37ca667706c68c0aad4cb04c3a211"
Content-Type: application/json;charset=UTF-8
Content-Length: 56
有了它 - ETags 在野外并节省带宽。
4. Spring 中的 ETag 支持
关于 Spring 支持:在 Spring 中使用 ETag 非常容易设置并且对应用程序完全透明。我们可以通过在web.xml中添加一个简单的过滤器来启用支持:
<filter>
<filter-name>etagFilter</filter-name>
<filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>etagFilter</filter-name>
<url-pattern>/foos/*</url-pattern>
</filter-mapping>
我们将过滤器映射到与 RESTful API 本身相同的 URI 模式上。过滤器本身是自 Spring 3.0 以来 ETag 功能的标准实现。
实现是浅层的——应用程序根据响应计算 ETag,这将节省带宽但不会节省服务器性能。
因此,受益于 ETag 支持的请求仍将作为标准请求处理,消耗它通常会消耗的任何资源(数据库连接等),并且只有在将其响应返回给客户端之前,ETag 支持才会启动在。
此时,ETag 将从响应主体中计算出来并设置在资源本身上;此外,如果在请求上设置了If-None-Match标头,它也会被处理。
ETag 机制的更深层次的实现可能会提供更大的好处——例如从缓存中处理一些请求并且根本不必执行计算——但实现肯定不会像浅层方法那样简单,也不像可插入的方法描述here。
4.1.基于 Java 的配置
让我们通过在 Spring 上下文中声明一个ShallowEtagHeaderFilter bean来看看基于 Java 的配置是什么样子的:
@Bean
public ShallowEtagHeaderFilter shallowEtagHeaderFilter() {
return new ShallowEtagHeaderFilter();
}
请记住,如果我们需要提供进一步的过滤器配置,我们可以改为声明一个FilterRegistrationBean实例:
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean
= new FilterRegistrationBean<>( new ShallowEtagHeaderFilter());
filterRegistrationBean.addUrlPatterns("/foos/*");
filterRegistrationBean.setName("etagFilter");
return filterRegistrationBean;
}
最后,如果我们不使用 Spring Boot,我们可以使用AbstractAnnotationConfigDispatcherServletInitializer的getServletFilters方法设置过滤器。
4.2. 使用 ResponseEntity 的*eTag()*方法
这个方法是在 Spring 框架 4.1 中引入的,我们可以用它来控制单个端点检索到的 ETag 值。
例如,假设我们使用版本化实体作为 Optimist Locking 机制 来访问我们的数据库信息。
我们可以使用版本本身作为 ETag 来指示实体是否已被修改:
@GetMapping(value = "/{id}/custom-etag")
public ResponseEntity<Foo>
findByIdWithCustomEtag(@PathVariable("id") final Long id) {
// ...Foo foo = ...
return ResponseEntity.ok()
.eTag(Long.toString(foo.getVersion()))
.body(foo);
}
如果请求的条件标头与缓存数据匹配,服务将检索相应的304-Not Modified状态。
5. 测试 ETag
让我们从简单的开始——我们需要验证检索单个资源的简单请求的响应是否实际上会返回“ ETag ”标头:
@Test
public void givenResourceExists_whenRetrievingResource_thenEtagIsAlsoReturned() {
// Given
String uriOfResource = createAsUri();
// When
Response findOneResponse = RestAssured.given().
header("Accept", "application/json").get(uriOfResource);
// Then
assertNotNull(findOneResponse.getHeader("ETag"));
}
**接下来,我们验证 ETag 行为的路径。**如果从服务器检索Resource的请求使用正确的ETag值,则服务器不会检索资源:
@Test
public void givenResourceWasRetrieved_whenRetrievingAgainWithEtag_thenNotModifiedReturned() {
// Given
String uriOfResource = createAsUri();
Response findOneResponse = RestAssured.given().
header("Accept", "application/json").get(uriOfResource);
String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG);
// When
Response secondFindOneResponse= RestAssured.given().
header("Accept", "application/json").headers("If-None-Match", etagValue)
.get(uriOfResource);
// Then
assertTrue(secondFindOneResponse.getStatusCode() == 304);
}
一步步:
- 我们创建和检索资源 ,存储ETag值
- 发送一个新的检索请求,这次使用“ If-None-Match ”标头指定先前存储的ETag值
- 在第二个请求中,服务器仅返回304 Not Modified,因为资源本身在两次检索操作之间确实没有被修改
最后,我们验证在第一次和第二次检索请求之间更改资源的情况:
@Test
public void
givenResourceWasRetrievedThenModified_whenRetrievingAgainWithEtag_thenResourceIsReturned() {
// Given
String uriOfResource = createAsUri();
Response findOneResponse = RestAssured.given().
header("Accept", "application/json").get(uriOfResource);
String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG);
existingResource.setName(randomAlphabetic(6));
update(existingResource);
// When
Response secondFindOneResponse= RestAssured.given().
header("Accept", "application/json").headers("If-None-Match", etagValue)
.get(uriOfResource);
// Then
assertTrue(secondFindOneResponse.getStatusCode() == 200);
}
一步步:
- 我们首先创建和检索Resource——并存储ETag值以供进一步使用
- 然后我们更新相同的Resource
- 发送一个新的 GET 请求,这次使用“ If-None-Match ”标头指定我们之前存储的ETag
- 在第二个请求中,服务器将返回200 OK以及完整的资源,因为ETag值不再正确,因为我们同时更新了资源
最后,最后一个测试——由于该功能尚未在 Spring 中实现而无法 工作——是对If-Match HTTP 标头的支持:
@Test
public void givenResourceExists_whenRetrievedWithIfMatchIncorrectEtag_then412IsReceived() {
// Given
T existingResource = getApi().create(createNewEntity());
// When
String uriOfResource = baseUri + "/" + existingResource.getId();
Response findOneResponse = RestAssured.given().header("Accept", "application/json").
headers("If-Match", randomAlphabetic(8)).get(uriOfResource);
// Then
assertTrue(findOneResponse.getStatusCode() == 412);
}
一步步:
- 我们创建一个资源
- 然后使用指定错误ETag值的“ If-Match ”标头检索它——这是一个有条件的 GET 请求
- 服务器应该返回412 Precondition Failed
6. ETag 很大
**我们只将 ETag 用于读取操作。**存在一个RFC ,试图阐明实现应该如何处理写操作中的 ETags——这不是标准的,但读起来很有趣。
当然,ETag 机制还有其他可能的用途,例如用于乐观锁定机制以及处理相关的“丢失更新问题” 。
在使用 ETag 时,还有几个已知的潜在陷阱和注意事项 需要注意。