ETags für REST mit Frühling

REST Top

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs

1. Übersicht

Dieser Artikel konzentriert sich auf die Arbeit mit ETags im Frühjahr , Integrationstests der REST-API und Verbrauchsszenarien mit Curl .

2. REST und ETags

Aus der offiziellen Spring-Dokumentation zur ETag-Unterstützung:

Ein ETag (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. Der ETag-Wert kann als Hash betrachtet werden, der aus den Bytes des Antwortkörpers berechnet wird. Da der Dienst wahrscheinlich eine kryptografische Hash-Funktion verwendet, ändert selbst die kleinste Änderung des Körpers die Ausgabe und damit den Wert des ETag drastisch. Dies gilt nur für starke ETags - das Protokoll liefert auch einen schwachen Etag.

Die Verwendung eines If- * -Headers verwandelt eine Standard-GET-Anforderung in eine bedingte GET. Die beiden If- * -Header, die mit ETags verwendet werden, sind "If-None-Match" und "If-Match" - jeder mit seiner eigenen Semantik, wie später in diesem Artikel erläutert.

3. Client-Server-Kommunikation mit Curl

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. Die Antwort enthält den ETag-Header , der zur weiteren Verwendung gespeichert wird:

curl -H "Accept: application/json" -i //localhost:8080/spring-boot-rest/foos/1
HTTP/1.1 200 OK ETag: "f88dd058fe004909615a64f01be66a7" Content-Type: application/json;charset=UTF-8 Content-Length: 52

Für die nächste Anforderung enthält der Client den Anforderungsheader If-None-Match mit dem ETag-Wert aus dem vorherigen Schritt. Wenn sich die Ressource auf dem Server nicht geändert hat, enthält die Antwort keinen Text und den Statuscode 304 - Nicht geändert :

curl -H "Accept: application/json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"' -i //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"}' //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 erneut 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 //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 freier Wildbahn und spart Bandbreite.

4. ETag-Unterstützung im Frühjahr

Weiter zur Spring-Unterstützung: Die Verwendung von ETag in Spring ist äußerst einfach einzurichten und für die Anwendung vollständig transparent. Wir können die Unterstützung aktivieren, indem wir einen einfachen Filter in die Datei web.xml einfügen :

 etagFilter org.springframework.web.filter.ShallowEtagHeaderFilter   etagFilter /foos/* 

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

Die Implementierung ist flach - die Anwendung berechnet den ETag basierend auf der Antwort, wodurch Bandbreite, jedoch keine Serverleistung eingespart wird.

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

Zu diesem Zeitpunkt wird der ETag aus dem Antworttext berechnet und auf die Ressource selbst festgelegt. Wenn der If-None-Match- Header für die Anforderung festgelegt wurde, wird er ebenfalls behandelt.

Eine tiefere Implementierung des ETag-Mechanismus könnte möglicherweise viel größere Vorteile bieten - beispielsweise die Bearbeitung einiger Anforderungen aus dem Cache und die Tatsache, dass die Berechnung überhaupt nicht durchgeführt werden muss -, aber die Implementierung wäre definitiv nicht so einfach und nicht so steckbar wie der flache Ansatz hier beschrieben.

4.1. Java-basierte Konfiguration

Lassen Sie uns sehen, wie die Java-basierte Konfiguration aussehen würde, indem Sie eine ShallowEtagHeaderFilter- Bean in unserem Spring-Kontext deklarieren :

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

Beachten Sie , dass wir stattdessen eine FilterRegistrationBean- 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; }

Schließlich, wenn wir nicht Frühling - Boot verwenden , können wir unter Verwendung des Filters die Einrichtung AbstractAnnotationConfigDispatcherServletInitializer ‚s getServletFilters Methode.

4.2. Unter Verwendung der ResponseEntity der eTag () Methode

Diese Methode wurde in Spring Framework 4.1 eingeführt und kann verwendet werden, um den ETag-Wert zu steuern, den ein einzelner Endpunkt abruft .

Stellen Sie sich zum Beispiel vor, wir verwenden versionierte Entitäten als Optimist Locking-Mechanismus, 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 entsprechenden Status 304-Not Modified ab, wenn der bedingte Header der Anforderung mit den Caching-Daten übereinstimmt.

5. ETags testen

Fangen wir einfach an - wir müssen überprüfen, ob die Antwort einer einfachen Anfrage, die eine einzelne Ressource abruft, tatsächlich den " ETag" -Header zurückgibt :

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

Als nächstes , wir den glücklichen Weg des ETag Verhalten überprüfen. Wenn die Anforderung zum Abrufen der Ressource vom Server den richtigen ETag- Wert verwendet, 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 und speichern den ETag- Wert
  • Senden Sie eine neue Abrufanforderung, diesmal mit dem Header " If-None-Match ", der den zuvor gespeicherten ETag- Wert angibt
  • 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 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); }

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 Are Big

Wir haben ETags nur für Lesevorgänge verwendet. Es gibt einen RFC, der versucht 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 Sperrmechanismus sowie die Behandlung des damit verbundenen „Problems der verlorenen Aktualisierung“.

Es sind auch einige potenzielle Fallstricke und Vorbehalte bekannt, die bei der Verwendung von ETags zu beachten sind.

7. Fazit

Dieser Artikel hat nur die Oberfläche 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 im GitHub-Projekt.

REST unten

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs