Spring 中 RESTful API 的 ETag 支持

REST,Spring
Remote
1
08:56 PM · Dec 01 ,2025

1. 概述

本文將重點介紹在 Spring 中使用 ETags 的方法, REST API 的集成測試以及與 curl 的消費場景。

2. REST 和 ETags

從官方 Spring 文檔關於 ETag 支持的説明:

一個 ETag (實體標籤) 是 HTTP/1.1 兼容 Web 服務器返回的 HTTP 響應頭,用於確定給定 URL 的內容是否發生變化。

我們可以使用 ETags 用於兩個目的 – 緩存和條件請求。 ETag 值可以被認為是對響應體字節計算出的 哈希值。 由於服務很可能使用加密哈希函數,即使響應體發生最小修改,輸出和因此 ETag 的值也會發生顯著變化。 僅對強 ETag 成立 – 協議還提供了一個 弱 ETag

使用 If-* 標頭可以將標準 GET 請求轉換為條件 GET 請求。If-None-Match” 和 “If-Match” 是使用的兩個 If-* 標頭,每個標頭都有其各自的語義,如本文稍後所述。

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

就這樣 – ETag 在野外,節省帶寬。

4. ETag 支持在 Spring

轉向 Spring 支持:在 Spring 中使用 ETag 極其容易設置,並且對應用程序完全透明。我們可以通過添加一個簡單的 Filterweb.xml


<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 方式的配置會是什麼樣子,通過聲明一個 ShallowEtagHeaderFilter bean 在我們的 Spring 上下文中:

@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 狀態,如果請求的條件標頭與緩存數據匹配。

@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")); }

Next, we verify the happy path of the ETag behavior. If the Request to retrieve the Resource from the server uses the correct ETag value, then the server doesn’t retrieve the Resource:

@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);
}

Step by step:

  • we create and retrieve a Resource, storing the ETag value
  • send a new retrieve request, this time with the “If-None-Match” header specifying the ETag value previously stored
  • on this second request, the server simply returns a 304 Not Modified, since the Resource itself has indeed not beeing modified between the two retrieval operations

Finally, we verify the case where the Resource is changed between the first and the second retrieval requests:

@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);
}

Step by step:

  • we first create and retrieve a Resource – and store the ETag value for further use
  • then we update the same Resource
  • send a new GET request, this time with the “If-None-Match” header specifying the ETag that we previously stored
  • on this second request, the server will return a 200 OK along with the full Resource, since the ETag value is no longer correct, as we updated the Resource in the meantime

Finally, the last test – which is not going to work because the functionality has not yet been implemented in Spring – is @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); }

Step by step:

  • we create a Resource
  • then retrieve it using the “If-Match” header specifying an incorrect ETag value – this is a conditional GET request
  • the server should return a 412 Precondition Failed

6. ETags 很大

我們僅對讀取操作使用過 ETags。一個 RFC 試圖明確如何處理 ETags 在寫入操作中的實現 – 這不是標準,但這是一個有趣的閲讀材料。

當然,ETags 機制還有其他可能的用途,例如作為樂觀鎖機制,以及處理 相關的“丟失更新問題”

此外,在使用 ETags 時,還有一些已知的 潛在的陷阱和注意事項

7. 結論

本文只觸及了 Spring 和 ETags 的可能性冰山一角。

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

發佈 評論

Some HTML is okay.