作者: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的项目,因此应该很容易导入和运行。