知識庫 / Spring RSS 訂閱

Spring 中 RESTful 服務的 ETag 支持

REST,Spring
HongKong
5
04:12 AM · Dec 06 ,2025

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/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>
   <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,則可以使用 AbstractAnnotationConfigDispatcherServletInitializergetServletFilters 方法來設置過濾器。

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 能夠實現的全部可能性進行了初步探討。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.