SpringによるRESTのETag

SpringでのRESTのETag

1. 概要

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

参考文献:

Spring RESTドキュメントの概要

この記事では、正確で読みやすいRESTfulサービスのドキュメントを生成するテスト駆動メカニズムであるSpring REST Docsを紹介します。

Spring REST APIのカスタムメディアタイプ

Spring REST APIでカスタムメディアタイプを使用するための簡単な紹介。

Spring RESTおよびAngularJSテーブルのページネーション

Springのページネーションを使用して簡単なAPIを実装する方法と、AngularJSとUI Gridでそれを使用する方法について詳細に説明します。

2. RESTおよびETag

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

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

キャッシングと条件付きリクエストの2つの目的でETagを使用できます。 応答本文のバイトから計算されたETag value can be thought of as a hash。 サービスは暗号化ハッシュ関数を使用する可能性が高いため、ボディのわずかな変更でも出力が大幅に変更されるため、ETagの値が大幅に変更されます。 これは強力なETagにのみ当てはまります–プロトコルはweak Etagも提供します。

Using an If-* header turns a standard GET request into a conditional GET. ETagで使用されている2つのIf-*ヘッダーは、「https://tools.ietf.org/html/rfc2616#section-14.26[If-None-Match]」と「https:/ /tools.ietf.org/html/rfc2616#section-14.24[If-Match]」–この記事の後半で説明するように、それぞれに独自のセマンティクスがあります。

3. curlとのクライアントサーバー通信

ETagを含む簡単なクライアントサーバー通信をステップに分解できます。

最初に、クライアントはREST API呼び出しを行います–the Response includes the ETag headerは、さらに使用するために保存されます。

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

For the next request, the Client will include the If-None-Match request header with the ETag value from the previous step.サーバー上でリソースが変更されていない場合、応答には本文とステータスコードが含まれませんof 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.xmlWe can enable the support by adding a simple Filter


   etagFilter
   org.springframework.web.filter.ShallowEtagHeaderFilter


   etagFilter
   /foos/*

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

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

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

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

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

4.1. Javaベースの構成

Javaベースの構成がdeclaring a ShallowEtagHeaderFilter bean in our Spring contextでどのように見えるかを見てみましょう。

@Bean
public ShallowEtagHeaderFilter shallowEtagHeaderFilter() {
    return new ShallowEtagHeaderFilter();
}

さらにフィルター構成を提供する必要がある場合は、代わりにFilterRegistrationBeanインスタンスを宣言できることに注意してください。

@Bean
public FilterRegistrationBean shallowEtagHeaderFilter() {
    FilterRegistrationBean filterRegistrationBean
      = new FilterRegistrationBean<>( new ShallowEtagHeaderFilter());
    filterRegistrationBean.addUrlPatterns("/foos/*");
    filterRegistrationBean.setName("etagFilter");
    return filterRegistrationBean;
}

最後に、Spring Bootを使用していない場合は、AbstractAnnotationConfigDispatcherServletInitializergetServletFilters メソッドを使用してフィルターを設定できます。

4.2. ResponseEntityのeTag()メソッドの使用

このメソッドは、Spring Framework 4.1およびwe can use it to control the ETag value that a single endpoint retrievesで導入されました。

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

エンティティが変更されたかどうかを示すために、バージョン自体をETagとして使用できます。

@GetMapping(value = "/{id}/custom-etag")
public ResponseEntity
  findByIdWithCustomEtag(@PathVariable("id") final Long id) {

    // ...Foo foo = ...

    return ResponseEntity.ok()
      .eTag(Long.toString(foo.getVersion()))
      .body(foo);
}

リクエストの条件付きヘッダーがキャッシュデータと一致する場合、サービスは対応する304-Not Modified状態を取得します。

5. ETagのテスト

簡単に始めましょう–we need to verify that the response of a simple request retrieving a single Resource will actually return the “ETag” header:

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

Nextwe verify the happy path of the ETag behavior.サーバーからResourceを取得する要求が、正しい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);
}

ステップバイステップ:

  • リソース、storingETag値を作成および取得します

  • 今回は、以前に保存されたETag値を指定する「If-None-Match」ヘッダーを使用して、新しい取得要求を送信します

  • この2番目のリクエストでは、リソース自体が2つの取得操作間で実際に変更されていないため、サーバーは単に304 Not Modifiedを返します。

最後に、最初の取得要求と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値を保存します

  • 次に、同じResourceを更新します

  • 新しいGETリクエストを送信します。今回は、以前に保存したETagを指定する「If-None-Match」ヘッダーを使用します

  • この2番目のリクエストでは、リソースを更新したため、ETagの値が正しくなくなったため、サーバーは完全なリソースとともに200 OKを返します。

最後に、機能にnot yet been implemented in Springがあるために機能しない最後のテストは、the support for the If-Match HTTP header:です。

@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 Precondition Failedを返す必要があります

6. ETagsは大きい

We have only used ETags for read operations.RFC existsは、実装が書き込み操作でETagをどのように処理するかを明確にしようとしています。これは標準ではありませんが、興味深い読み物です。

もちろん、楽観的ロックメカニズムやrelated “Lost Update Problem”の処理など、ETagメカニズムの他の可能な使用法もあります。

ETagを使用するときに注意すべき既知のpotential pitfalls and caveatsもいくつかあります。

7. 結論

この記事では、SpringとETagsで可能なことだけを紹介しました。

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