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.
Ein benutzerdefinierter Medientyp für eine Spring REST-API
Eine kurze Einführung in die Verwendung eines benutzerdefinierten Medientyps in einer Spring REST-API.
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.
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.