1. 概要

この記事では、 Spring でのETagの操作、REST APIの統合テスト、およびcurlを使用した消費シナリオに焦点を当てます。

2. RESTとETag

ETagサポートに関するSpringの公式ドキュメントから:

ETag (エンティティタグ)は、特定のURLのコンテンツの変更を判別するために使用されるHTTP/1.1準拠のWebサーバーによって返されるHTTP応答ヘッダーです。

ETagは、キャッシュと条件付きリクエストの2つに使用できます。 ETag値は、応答本文のバイトから計算されたハッシュと考えることができます。 サービスは暗号化ハッシュ関数を使用する可能性が高いため、本文を少し変更しただけでも、出力が大幅に変更され、ETagの値が大幅に変更されます。 これは強力なETagにのみ当てはまります。プロトコルは弱いEtagも提供します。

If- *ヘッダーを使用すると、標準のGETリクエストが条件付きGETに変わります。ETagで使用されている2つのIf-* ヘッダーは、「 If-None-Match[ X161X]」および「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

次のリクエストでは、クライアントは前の手順のETag値を含むIf-None-Matchリクエストヘッダーを含めます。サーバーでリソースが変更されていない場合、レスポンスには本文が含まれず、 304のステータスコード–変更なし

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. 春の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>

RESTfulAPI自体と同じURIパターンにフィルターをマッピングしています。 フィルタ自体は、Spring3.0以降のETag機能の標準実装です。

実装は浅いものです–アプリケーションは応答に基づいてETagを計算します。これにより、帯域幅は節約されますが、サーバーのパフォーマンスは節約されません。

したがって、ETagサポートの恩恵を受けるリクエストは引き続き標準リクエストとして処理され、通常消費するリソース(データベース接続など)を消費し、応答がクライアントに返される前にのみETagサポートが開始されます。の。

その時点で、ETagは応答本文から計算され、リソース自体に設定されます。 また、リクエストに If-None-Match ヘッダーが設定されている場合は、それも処理されます。

ETagメカニズムのより深い実装は、キャッシュからの一部の要求を処理し、計算をまったく実行する必要がないなど、はるかに大きな利点を提供する可能性がありますが、実装は、浅いアプローチほど単純ではなく、プラグイン可能でもありません。ここで説明します。

4.1. Javaベースの構成

SpringコンテキストでShallowEtagHeaderFilterbeanを宣言することにより、Javaベースの構成がどのように見えるかを見てみましょう。

@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()メソッドの使用

このメソッドはSpringFramework4.1で導入され、単一のエンドポイントが取得するETag値を制御するために使用できます

たとえば、バージョン管理されたエンティティを Optimist Lockingメカニズムとして使用して、データベース情報にアクセスしているとします。

バージョン自体を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-変更なし状態を取得します。

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
  • 以前に保存されたETag値を指定する「If-None-Match」ヘッダーを使用して新しい取得リクエストを送信します
  • この2番目のリクエストでは、サーバーは 304 Not Modified を返すだけです。これは、リソース自体が2つの取得操作間で実際に変更されていないためです。

最後に、最初の取得要求と2番目の取得要求の間でリソースが変更された場合を確認します。

@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値を保存してさらに使用します
  • 次に、同じリソースを更新します
  • 新しいGETリクエストを送信します。今回は、以前に保存したETagを指定する「If-None-Match」ヘッダーを使用します
  • この2番目のリクエストでは、 ETag の値が正しくなくなったため、サーバーは完全なリソースとともに 200OKを返します。

最後に、機能が 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前提条件に失敗しましたを返す必要があります

6. ETagは大きい

読み取り操作にはETagのみを使用しました。RFCが存在し、実装が書き込み操作でETagを処理する方法を明確にしようとしています。これは標準ではありませんが、興味深い読み取りです。

もちろん、オプティミスティックロックメカニズムや関連の「LostUpdateProblem」の処理など、ETagメカニズムの他の可能な使用法もあります。

ETagを使用する際に注意すべき、いくつかの既知の潜在的な落とし穴と警告もあります。

7. 結論

この記事は、SpringとETagで可能なことで表面をかじっただけです。

ETag対応のRESTfulサービスの完全な実装と、ETagの動作を検証する統合テストについては、GitHubプロジェクトを確認してください。