1. 概述
本文將重點介紹在 Spring 中使用 ETags 的方法,REST API 的集成測試以及使用 curl 的消費場景。
2. REST 和 ETag
根據官方 Spring 文檔關於 ETag 支持的説明:
ETag (實體標籤) 是一種 HTTP 響應頭,由 HTTP/1.1 兼容的 Web 服務器返回,用於確定給定 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/1HTTP/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/1HTTP/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/1HTTP/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/1HTTP/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>
<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 機制可能能夠提供更大的益處——例如,從緩存中服務某些請求,並且根本無需進行計算——但這無疑不會像此處描述的淺層方法那樣簡單或可插拔。
4.1. 基於Java的配置
讓我們看看基於Java的配置將是什麼樣子,通過在我們的Spring上下文中聲明一個ShallowEtagHeaderFilter bean 來實現:
@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 Framework 4.1 中引入,我們可以使用它來控制單個端點檢索的 ETag 值。
例如,假設我們使用版本化實體作為樂觀鎖機制來訪問數據庫信息。
我們可以使用版本本身作為 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 行為的正常流程。如果從服務器獲取 資源的請求使用正確的 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 響應,以及完整的 Resource,因為 ETag 值不再正確,因為我們已經更新了 Resource
最後一次測試 – 由於該功能尚未在 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);
}逐步操作:
- 我們創建資源
- 然後使用“If-Match”標頭指定錯誤的 ETag 值 – 這是一個條件 GET 請求
- 服務器應返回 412 Precondition Failed
6. ETags 體積較大
我們僅使用 ETags 進行讀取操作。 一份 RFC 試圖明確説明在寫入操作中,實現如何處理 ETags – 這不是標準做法,但值得一讀。
當然,ETag 機制還有其他可能的用途,例如作為樂觀鎖機制,以及處理 相關的“丟失更新問題”。
同時,在使用 ETags 時,還需要注意一些已知的 潛在的陷阱和注意事項。
7. 結論
本文只對 Spring 和 ETags 能夠實現的全部可能性進行了初步探討。