Inhaltsverzeichnis

ETags für REST with Spring

1. Überblick

Dieser Artikel konzentriert sich aufworking with ETags in Spring, Integrationstests der REST-API und Verbrauchsszenarien mitcurl.

Weitere Lektüre:

Einführung in Spring REST Docs

In diesem Artikel wird Spring REST Docs vorgestellt, ein testgesteuerter Mechanismus zum Generieren einer Dokumentation für RESTful-Services, die genau und lesbar ist.

Read more

Ein benutzerdefinierter Medientyp für eine Spring REST-API

Eine kurze Einführung in die Verwendung eines benutzerdefinierten Medientyps in einer Spring REST-API.

Read more

Paginierung mit Federauflage und AngularJS-Tisch

Ein ausführlicher Einblick in die Implementierung einer einfachen API mit Paginierung in Spring und deren Verwendung in AngularJS und UI Grid.

Read more

2. REST und ETags

Aus der offiziellen Spring-Dokumentation zum ETag-Support:

EinETag (Entity-Tag) ist ein HTTP-Antwortheader, der von einem HTTP / 1.1-kompatiblen Webserver zurückgegeben wird, um die Änderung des Inhalts unter einer bestimmten URL zu bestimmen.

Wir können ETags für zwei Dinge verwenden - Caching und bedingte Anforderungen. DieETag value can be thought of as a hash werden aus den Bytes des Antwortkörpers berechnet. Da der Dienst wahrscheinlich eine kryptografische Hash-Funktion verwendet, wird selbst die kleinste Modifikation des Körpers die Ausgabe und damit den Wert des ETag drastisch verändern. Dies gilt nur für starke ETags - das Protokoll liefert auchweak Etag.

Using an If-* header turns a standard GET request into a conditional GET. Die beidenIf-*-Header, die mit ETags verwendet werden, sind "https://tools.ietf.org/html/rfc2616#section-14.26[If-None-Match]" und "https: / /tools.ietf.org/html/rfc2616#section-14.24[If-Match] ”- jede mit ihrer eigenen Semantik, wie später in diesem Artikel erläutert.

3. Client-Server-Kommunikation mitcurl

Wir können eine einfache Client-Server-Kommunikation mit ETags in die folgenden Schritte unterteilen:

Zunächst führt der Client einen REST-API-Aufruf durch -the Response includes the ETag header, der zur weiteren Verwendung gespeichert wird:

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. Wenn sich die Ressource auf dem Server nicht geändert hat, enthält die Antwort keinen Text und einen Statuscodeof 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"

Bevor Sie die Ressource erneut abrufen, ändern Sie sie, indem Sie ein Update durchführen:

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

Schließlich senden wir die letzte Anfrage, um das Foo wieder abzurufen. Beachten Sie, dass wir es seit der letzten Anforderung aktualisiert haben, sodass der vorherige ETag-Wert nicht mehr funktionieren sollte. Die Antwort enthält die neuen Daten und ein neues ETag, das wiederum zur weiteren Verwendung gespeichert werden kann:

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

Und da haben Sie es - ETags in der Wildnis und sparende Bandbreite.

4. ETag-Unterstützung im Frühjahr

Auf zur Federunterstützung: Die Verwendung von ETag in Spring ist äußerst einfach einzurichten und für die Anwendung vollständig transparent. We can enable the support by adding a simple Filter inweb.xml:


   etagFilter
   org.springframework.web.filter.ShallowEtagHeaderFilter


   etagFilter
   /foos/*

Wir ordnen den Filter demselben URI-Muster zu wie die RESTful-API. Der Filter selbst ist seit Spring 3.0 die Standardimplementierung der ETag-Funktionalität.

The implementation is a shallow one - Die Anwendung berechnet den ETag basierend auf der Antwort, wodurch Bandbreite, jedoch keine Serverleistung eingespart wird.

Daher wird eine Anfrage, die von der ETag-Unterstützung profitiert, weiterhin als Standardanfrage verarbeitet. Sie verbraucht alle Ressourcen, die normalerweise verbraucht werden (Datenbankverbindungen usw.), und erst bevor die Antwort an den Client zurückgesendet wird, wird der ETag-Support ausgelöst im.

Zu diesem Zeitpunkt wird der ETag aus dem Antworttext berechnet und auf die Ressource selbst festgelegt. Wenn der HeaderIf-None-Matchin der Anforderung festgelegt wurde, wird er ebenfalls behandelt.

Eine tiefere Implementierung des ETag-Mechanismus könnte möglicherweise viel größere Vorteile bieten - z. B. das Bedienen einiger Anforderungen aus dem Cache und die Notwendigkeit, die Berechnung überhaupt nicht durchführen zu müssen -, aber die Implementierung wäre definitiv nicht so einfach und steckbar wie der flache Ansatz hier beschrieben.

4.1. Java-basierte Konfiguration

Mal sehen, wie die Java-basierte Konfiguration indeclaring a ShallowEtagHeaderFilter bean in our Spring context aussehen würde:

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

Beachten Sie, dass wir stattdessen eineFilterRegistrationBean-Instanz deklarieren können, wenn wir weitere Filterkonfigurationen bereitstellen müssen:

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

Wenn wir Spring Boot nicht verwenden, können wir den Filter mithilfe derAbstractAnnotationConfigDispatcherServletInitializer-SgetServletFilters -Smethod vonAbstractAnnotationConfigDispatcherServletInitializereinrichten.

4.2. Verwenden dereTag()-Methode von ResponseEntity

Diese Methode wurde in Spring Framework 4.1 undwe can use it to control the ETag value that a single endpoint retrieves eingeführt.

Stellen Sie sich zum Beispiel vor, wir verwenden versionierte Entitäten alsOptimist Locking mechanism, um auf unsere Datenbankinformationen zuzugreifen.

Wir können die Version selbst als ETag verwenden, um anzuzeigen, ob die Entität geändert wurde:

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

Der Dienst ruft den entsprechenden304-Not Modified-Status ab, wenn der bedingte Header der Anforderung mit den Caching-Daten übereinstimmt.

5. ETags testen

Beginnen wir einfach -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"));
}

Next,we verify the happy path of the ETag behavior. Wenn die Anforderung zum Abrufen vonResource vom Server den korrekten Wert vonETagverwendet, ruft der Server die Ressource nicht ab:

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

Schritt für Schritt:

  • Wir erstellen und rufen eine Ressource ab,storingist der Wert vonETag

  • Senden Sie eine neue Abrufanforderung, diesmal mit dem Header "If-None-Match", der den zuvor gespeicherten Wert vonETag angibt

  • Bei dieser zweiten Anforderung gibt der Server einfach ein304 Not Modified zurück, da die Ressource selbst zwischen den beiden Abrufvorgängen tatsächlich nicht geändert wurde

Schließlich überprüfen wir den Fall, in dem die Ressource zwischen der ersten und der zweiten Abrufanforderung geändert wird:

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

Schritt für Schritt:

  • Wir erstellen und rufen zuerst einResource ab - und speichern denETag-Wert zur weiteren Verwendung

  • dann aktualisieren wir die gleichenResource

  • Senden Sie eine neue GET-Anforderung, diesmal mit dem Header "If-None-Match", der die zuvor gespeichertenETag angibt

  • Bei dieser zweiten Anforderung gibt der Server ein200 OK zusammen mit der vollständigen Ressource zurück, da der Wert vonETagnicht mehr korrekt ist, da wir die Ressource in der Zwischenzeit aktualisiert haben

Schließlich ist der letzte Test - der nicht funktionieren wird, weil die Funktionalitätnot yet been implemented in Spring hat -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);
}

Schritt für Schritt:

  • Wir erstellen eine Ressource

  • Rufen Sie es dann mit dem Header "If-Match" ab, der einen falschen Wert fürETagangibt. Dies ist eine bedingte GET-Anforderung

  • Der Server sollte ein412 Precondition Failed zurückgeben

6. ETags sind GROSS

We have only used ETags for read operations. EinRFC existsversucht zu klären, wie Implementierungen mit ETags bei Schreibvorgängen umgehen sollen - dies ist kein Standard, aber eine interessante Lektüre.

Es gibt natürlich auch andere Verwendungsmöglichkeiten des ETag-Mechanismus, beispielsweise für einen optimistischen Verriegelungsmechanismus sowie für den Umgang mitrelated “Lost Update Problem”.

Es sind auch einige bekanntepotential pitfalls and caveatsbekannt, die bei der Verwendung von ETags zu beachten sind.

7. Fazit

In diesem Artikel wurde die Oberfläche nur mit dem gekratzt, was mit Spring und ETags möglich ist.

Eine vollständige Implementierung eines ETag-fähigen RESTful-Dienstes sowie Integrationstests zur Überprüfung des ETag-Verhaltens finden Sie inGitHub project.