查看原文
其他

【SFA官方译】:基于Spring实现REST的ETag功能

The following article is from 生活点亮技术 Author JackieTang


原文链接: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头,以便进一步使用:

  1. curl -H "Accept: application/json" -i http://localhost:8080/rest-sec/api/resources/1

  1. HTTP/1.1 200 OK

  2. ETag: "f88dd058fe004909615a64f01be66a7"

  3. Content-Type: application/json;charset=UTF-8

  4. Content-Length: 52

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

  1. curl -H "Accept: application/json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"'

  2. -i http://localhost:8080/rest-sec/api/resources/1

  1. HTTP/1.1 304 Not Modified

  2. ETag: "f88dd058fe004909615a64f01be66a7"

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

  1. curl --user admin@fake.com:adminpass -H "Content-Type: application/json" -i

  2.  -X PUT --data '{ "id":1, "name":"newRoleName2", "description":"theNewDescription" }'

  3.    http://localhost:8080/rest-sec/api/resources/1

  1. HTTP/1.1 200 OK

  2. ETag: "d41d8cd98f00b204e9800998ecf8427e"

  3. Content-Length: 0

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

  1. curl -H "Accept: application/json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"' -i

  2.  http://localhost:8080/rest-sec/api/resources/1

  1. HTTP/1.1 200 OK

  2. ETag: "03cb37ca667706c68c0aad4cb04c3a211"

  3. Content-Type: application/json;charset=UTF-8

  4. Content-Length: 56

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

4.Spring对ETag的支持

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

  1. <filter>

  2.   <filter-name>etagFilter</filter-name>

  3.   <filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>

  4. </filter>

  5. <filter-mapping>

  6.   <filter-name>etagFilter</filter-name>

  7.   <url-pattern>/api/*</url-pattern>

  8. </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”头。

  1. @Test

  2. public void givenResourceExists_whenRetrievingResource_thenEtagIsAlsoReturned() {

  3.    // Given

  4.    String uriOfResource = createAsUri();

  5.    // When

  6.    Response findOneResponse = RestAssured.given().header("Accept", "application/json").get(uriOfResource);

  7.    // Then

  8.    assertNotNull(findOneResponse.getHeader("ETag"));

  9. }

接下来我们将验证正常使用ETag的效果——如果检索资源的请求使用了正确的ETag值,那么服务器将不再返回资源。

  1. @Test

  2. public void givenResourceWasRetrieved_whenRetrievingAgainWithEtag_thenNotModifiedReturned() {

  3.    // Given

  4.    String uriOfResource = createAsUri();

  5.    Response findOneResponse = RestAssured.given().

  6.      header("Accept", "application/json").get(uriOfResource);

  7.    String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG);

  8.    // When

  9.    Response secondFindOneResponse= RestAssured.given().

  10.      header("Accept", "application/json").headers("If-None-Match", etagValue)

  11.      .get(uriOfResource);

  12.    // Then

  13.    assertTrue(secondFindOneResponse.getStatusCode() == 304);

  14. }

操作步骤

  • 首先创建并检索资源,然后存储ETAG值以供进一步使用。

  • 发送一个新的检索请求,这次使用 “If-None-Match” 头携带上一次请求得到的ETag值。

  • 在第二个请求中,服务器仅仅返回一个*304 Not Modified*,这是因为资源本身在两次检索操作之间确实没有变化

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

  1. @Test

  2. public void

  3.  givenResourceWasRetrievedThenModified_whenRetrievingAgainWithEtag_thenResourceIsReturned() {

  4.    // Given

  5.    String uriOfResource = createAsUri();

  6.    Response findOneResponse = RestAssured.given().

  7.      header("Accept", "application/json").get(uriOfResource);

  8.    String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG);

  9.    existingResource.setName(randomAlphabetic(6));

  10.    update(existingResource);

  11.    // When

  12.    Response secondFindOneResponse= RestAssured.given().

  13.      header("Accept", "application/json").headers("If-None-Match", etagValue)

  14.      .get(uriOfResource);

  15.    // Then

  16.    assertTrue(secondFindOneResponse.getStatusCode() == 200);

  17. }

操作步骤

  • 首先创建并检索资源,然后存储ETAG值以供下一次请求使用。

  • 更新上一步返回的资源

  • 发送一个新的检索请求,这次使用 “If-None-Match”头携带上一步返回的ETag值

  • 在这第二次请求中,服务器将返回一个200 OK和完整的资源,这是因为此时资源已经更新但请求携带的ETag值却是旧的。

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

  1. @Test

  2. public void givenResourceExists_whenRetrievedWithIfMatchIncorrectEtag_then412IsReceived() {

  3.    // Given

  4.    T existingResource = getApi().create(createNewEntity());

  5.    // When

  6.    String uriOfResource = baseUri + "/" + existingResource.getId();

  7.    Response findOneResponse = RestAssured.given().header("Accept", "application/json").

  8.      headers("If-Match", randomAlphabetic(8)).get(uriOfResource);

  9.    // Then

  10.    assertTrue(findOneResponse.getStatusCode() == 412);

  11. }

操作步骤

  • 首先创建资源

  • 然后使用指定了错误ETag值的“If-Match”头检索资源——这是一个有条件的GET请求

  • 服务器将返回一个412 未满足先决条件

6.ETag的其它用武之地

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

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

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

7.总结

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



推荐: 【SFA官方翻译】Spring Boot中配置Tomcat连接池

上一篇: 【SFA官方译】:Spring REST API实体和DTO之间的转换

关注公众号


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存