弱网客户端的福音:节省带宽的利器【ETag】

2023-03-07 16:30:34 浏览数 (1)

原文链接:https://www.baeldung.com/etags-for-rest-with-spring

作者:Eugen Paraschiv

译者:helloworldtang

目录

  • 1.概览
  • 2.REST和ETag
  • 3.使用 curl来验证ETag功能
  • 4.Spring对ETag的支持
  • 5.测试ETag
  • 6.ETag的其它用武之地
  • 7. 总结

1.概览

本文将重点介绍如何在Spring中添加ETag功能、如何使用 curl来验证添加了ETag功能的REST API以及对这些REST API进行集成测试。

2.REST和 ETag

来自Spring官方文档中对ETag特性的描述: ETag(实体标签)是由符合HTTP/1.1的Web服务器返回的HTTP响应头,用于检查给定URL的返回值是否发生变化。

ETag常用于这两个场景——缓存和条件请求。ETag的值可以是根据响应体计算出来的hash值。因为可能使用Hash函数,所以即使响应体出现很小的改动也会极大地改变输出,也就是ETag值会发生变化。这只适用于比较严格的ETag——协议也提供了一个简单的ETag。

使用If-*头将一个标准的GET请求转换为条件GET。与ETag一起使用的两个If-*头是 “If-None-Match”和“If-Match”——每一个HTTP头都有它自己的语义,正如本文后面所讨论的。

3.使用 curl来验证ETag功能

一个通过客户端和服务器通信来简单地测试ETag特性的操作可以分解为以下步骤:

首先,客户端发起一个对REST API的调用——响应包括了需要存储的ETag头,以便进一步使用:

代码语言:javascript复制
curl -H "Accept: application/json" -i http://localhost:8080/rest-sec/api/resources/1
代码语言:javascript复制
HTTP/1.1 200 OK
ETag: "f88dd058fe004909615a64f01be66a7"
Content-Type: application/json;charset=UTF-8
Content-Length: 52

– 客户端在下一步发起REST API请求时,会使用If-None-Match头携带上一步保存的ETag值;如果服务器上的资源没有发生变化,那么响应将不会包含任何响应体,并且返回的HTTP状态码将会是304——Not Modified

代码语言:javascript复制
curl -H "Accept: application/json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"'
 -i http://localhost:8080/rest-sec/api/resources/1
代码语言:javascript复制
HTTP/1.1 304 Not Modified
ETag: "f88dd058fe004909615a64f01be66a7"

现在,在检索资源之前,我们将通过执行更新操作来改变检索时返回的响应体:

代码语言:javascript复制
curl --user admin@fake.com:adminpass -H "Content-Type: application/json" -i 
  -X PUT --data '{ "id":1, "name":"newRoleName2", "description":"theNewDescription" }'
    http://localhost:8080/rest-sec/api/resources/1
代码语言:javascript复制
HTTP/1.1 200 OK
ETag: "d41d8cd98f00b204e9800998ecf8427e"
Content-Length: 0

– 我们发起最后一个请求来再次检索资源;请记住,自从上次检索以来,资源已经被更新了,所以前面存储的ETag值已经不能代表现在的资源了——响应将包含新的数据和一个新的ETag,这个新的ETag可以被存储起来以供后续使用:

代码语言:javascript复制
curl -H "Accept: application/json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"' -i 
  http://localhost:8080/rest-sec/api/resources/1
代码语言:javascript复制
HTTP/1.1 200 OK
ETag: "03cb37ca667706c68c0aad4cb04c3a211"
Content-Type: application/json;charset=UTF-8
Content-Length: 56

这就是ETag的作用了,你可以在更多场合使用,并且可以节省带宽。

4.Spring对ETag的支持

在Spring下启用ETag功能非常容易,并且对于应用程序来说也是完全透明的。通过在web.xml中简单地添加一个过滤器就可以启动这个功能:

代码语言:javascript复制
<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>/api/*</url-pattern>
</filter-mapping>

上面配置的过滤器与RESTful API映射在相同的URI规则。自Spring 3.0以来,这个 org.springframework.web.filter.ShallowEtagHeaderFilter过滤器本身就是ETag功能的标准实现了。

这是一个很浅的实现——ETag值是基于响应来计算的,这将节省带宽,而不是服务器性能。因此,一个从ETag中获益的请求仍然会被作为一个标准请求处理,消耗正常消耗的任何资源(数据库连接等),并且只有在将它的响应返回给客户端之前,ETag支持才会启动。

在这一点上,ETag值将根据响应体计算出来并和响应一起返回给客户端;另外,如果请求携带了If-None-Match头,那也将会被处理。

ETag机制的更深层实现可能提供更大的好处——比如服务缓存中的一些请求,完全不必执行计算——但是实现肯定不像浅层方法那么简单,也不像这里描述的浅层方法那样可插拔。

5.测试ETag

那就开始吧——在检索一个资源时,我们需要验证返回的响应体将包含一个“ETag”头。

代码语言:javascript复制
@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的效果——如果检索资源的请求使用了正确的ETag值,那么服务器将不再返回资源。

代码语言:javascript复制
@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,这是因为资源本身在两次检索操作之间确实没有变化

最后,我们来验证在第一个和第二个检索请求之间更改资源的情况:

代码语言:javascript复制
@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);
}

操作步骤

  • 首先创建并检索资源,然后存储ETag值以供下一次请求使用。
  • 更新上一步返回的资源
  • 发送一个新的检索请求,这次使用 “If-None-Match”头携带上一步返回的ETag值
  • 在这第二次请求中,服务器将返回一个200 OK和完整的资源,这是因为此时资源已经更新但请求携带的ETag值却是旧的。

最后一个测试用例——因为Spring尚未支持If-Match HTTP头,所以这个测试用例在运行时会失败

代码语言:javascript复制
@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 未满足先决条件

6.ETag的其它用武之地

我们只是使用ETag来进行读操作——这里有一个已经提交的RFC试图澄清应该如何处理写操作的ETag——这不是标准的,但是是一个有趣的思路。

当然,ETag机制还有其他用途,譬如基于Spring 3.1的乐观锁定机制 ,以及处理相关的“丢失更新问题”。

如果要使用ETag,需要了解下这些前人踩过的坑:潜在缺陷和注意事项。

7.总结

这篇文章只是简单地讲了使用Spring和ETag可以做些什么。 如果需要一个实现了ETag功能的RESTful服务以及配套的集成测试,请查看GitHub项目——这是一个基于maven的项目,因此应该很容易导入和运行。

0 人点赞