ETags voor REST met Spring

REST Top

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS

1. Overzicht

Dit artikel zal zich concentreren op werken met ETags in het voorjaar, integratietesten van de REST API en verbruiksscenario's met krullen.

2. REST en ETags

Van de officiële Spring-documentatie over ETag-ondersteuning:

Een ETag (entiteitstag) is een HTTP-antwoordheader die wordt geretourneerd door een HTTP / 1.1-compatibele webserver die wordt gebruikt om verandering in inhoud op een bepaalde URL te bepalen.

We kunnen ETags gebruiken voor twee dingen: caching en voorwaardelijke verzoeken. De ETag-waarde kan worden gezien als een hash berekend uit de bytes van de hoofdtekst van de reactie. Omdat de service waarschijnlijk een cryptografische hash-functie gebruikt, zal zelfs de kleinste wijziging van de body de output en dus de waarde van de ETag drastisch veranderen. Dit geldt alleen voor sterke ETags - het protocol biedt ook een zwakke Etag.

Met behulp van een Als-* header verandert een standaard GET-verzoek in een voorwaardelijke GET. De twee Als-* headers die worden gebruikt met ETags zijn "If-None-Match" en "If-Match" - elk met zijn eigen semantiek, zoals later in dit artikel wordt besproken.

3. Client-server communicatie met krullen

We kunnen een eenvoudige client-server-communicatie met ETags opsplitsen in de volgende stappen:

Eerst doet de klant een REST API-aanroep - het antwoord bevat de ETag-header die worden opgeslagen voor verder gebruik:

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

Voor het volgende verzoek zal de klant het If-None-Match request header met de ETag-waarde uit de vorige stap. Als de bron op de server niet is gewijzigd, bevat de respons geen hoofdtekst en een statuscode van 304 - Niet gewijzigd:

curl -H "Accept: application / json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"' -i // localhost: 8080 / spring-boot-rest / foos / 1
HTTP / 1.1 304 niet gewijzigd ETag: "f88dd058fe004909615a64f01be66a7"

Laten we nu, voordat we de bron opnieuw ophalen, deze wijzigen door een update uit te voeren:

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" Inhoudslengte: 0

Ten slotte sturen we het laatste verzoek om de Foo opnieuw op te halen. Houd er rekening mee dat we het hebben bijgewerkt sinds de laatste keer dat we erom hebben gevraagd, dus de vorige ETag-waarde zou niet langer moeten werken. Het antwoord bevat de nieuwe gegevens en een nieuwe ETag die opnieuw kan worden opgeslagen voor verder gebruik:

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

En daar heb je het - ETags in het wild en bandbreedte besparen.

4. ETag-ondersteuning in het voorjaar

Op naar de Spring-ondersteuning: het gebruik van ETag in Spring is uiterst eenvoudig op te zetten en volledig transparant voor de toepassing. We kunnen de ondersteuning inschakelen door een eenvoudig Filter in de web.xml:

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

We brengen het filter in kaart op hetzelfde URI-patroon als de RESTful API zelf. Het filter zelf is de standaardimplementatie van ETag-functionaliteit sinds Spring 3.0.

De implementatie is oppervlakkig - de applicatie berekent de ETag op basis van de respons, wat bandbreedte bespaart, maar niet serverprestaties.

Dus een verzoek dat zal profiteren van de ETag-ondersteuning zal nog steeds worden verwerkt als een standaardverzoek, elke bron verbruiken die het normaal zou verbruiken (databaseverbindingen, enz.) En alleen voordat het antwoord wordt teruggestuurd naar de klant, zal de ETag-ondersteuning een kickstart geven. in.

Op dat moment wordt de ETag berekend uit de Response body en op de Resource zelf gezet; ook als het If-None-Match header is ingesteld op het verzoek, zal het ook worden afgehandeld.

Een diepere implementatie van het ETag-mechanisme zou in potentie veel grotere voordelen kunnen opleveren - zoals het bedienen van een aantal verzoeken vanuit de cache en de berekening helemaal niet hoeven uit te voeren - maar de implementatie zou zeker niet zo eenvoudig, noch zo pluggable zijn als de oppervlakkige benadering. hier beschreven.

4.1. Op Java gebaseerde configuratie

Laten we eens kijken hoe de op Java gebaseerde configuratie eruit zou zien door het verklaren van een ShallowEtagHeaderFilter bean in onze lentecontext:

@Bean openbaar ShallowEtagHeaderFilter shallowEtagHeaderFilter () {retourneer nieuwe ShallowEtagHeaderFilter (); }

Houd er rekening mee dat als we meer filterconfiguraties moeten bieden, we in plaats daarvan een FilterRegistrationBean voorbeeld:

@Bean openbaar FilterRegistrationBean shallowEtagHeaderFilter () {FilterRegistrationBean filterRegistrationBean = nieuw FilterRegistrationBean (nieuw ShallowEtagHeaderFilter ()); filterRegistrationBean.addUrlPatterns ("/ foos / *"); filterRegistrationBean.setName ("etagFilter"); retour filterRegistrationBean; }

Ten slotte, als we Spring Boot niet gebruiken, kunnen we het filter instellen met de AbstractAnnotationConfigDispatcherServletInitializer‘S getServletFilters methode.

4.2. Met behulp van de ResponseEntity's eTag () Methode

Deze methode is geïntroduceerd in Spring framework 4.1, en we kunnen het gebruiken om de ETag-waarde te beheren die een enkel eindpunt ophaalt.

Stel je bijvoorbeeld voor dat we entiteiten met versiebeheer gebruiken als een Optimist Locking-mechanisme om toegang te krijgen tot onze database-informatie.

We kunnen de versie zelf gebruiken als de ETag om aan te geven of de entiteit is gewijzigd:

@GetMapping (value = "/ {id} / custom-etag") openbare ResponseEntity findByIdWithCustomEtag (@PathVariable ("id") laatste lange id) {// ... Foo foo = ... return ResponseEntity.ok (). eTag (Long.toString (foo.getVersion ())) .body (foo); }

De service haalt het bijbehorende 304-niet gewijzigd geef aan of de voorwaardelijke header van het verzoek overeenkomt met de cachegegevens.

5. ETags testen

Laten we eenvoudig beginnen - we moeten controleren of het antwoord van een eenvoudig verzoek om een ​​enkele bron op te halen, daadwerkelijk de 'ETag ' koptekst:

@Test openbare leegte gegevenResourceExists_whenRetrievingResource_thenEtagIsAlsoReturned () {// Gegeven String uriOfResource = createAsUri (); // When Response findOneResponse = RestAssured.given (). header ("Accept", "application / json"). get (uriOfResource); // Vervolgens assertNotNull (findOneResponse.getHeader ("ETag")); }

De volgende, we verifiëren het gelukkige pad van het ETag-gedrag. Als het verzoek om het Bron van de server gebruikt het juiste ETag waarde, dan haalt de server de bron niet op:

@Test openbare leegte gegevenResourceWasRetrieved_whenRetrievingAgainWithEtag_thenNotModifiedReturned () {// Gegeven string uriOfResource = createAsUri (); Antwoord 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); // Vervolgens assertTrue (secondFindOneResponse.getStatusCode () == 304); }

Stap voor stap:

  • we creëren en halen een bron op, opbergen de ETag waarde
  • stuur een nieuw ophaalverzoek, dit keer met de "If-None-Match”-Kop die de ETag waarde eerder opgeslagen
  • op dit tweede verzoek retourneert de server eenvoudig een 304 Niet gewijzigd, aangezien de bron zelf inderdaad niet is gewijzigd tussen de twee ophaalbewerkingen

Ten slotte verifiëren we het geval waarin de bron wordt gewijzigd tussen het eerste en het tweede ophaalverzoek:

@Test openbare leegte gegevenResourceWasRetrievedThenModified_whenRetrievingAgainWithEtag_thenResourceIsReturned () {// Gegeven String uriOfResource = createAsUri (); Antwoord findOneResponse = RestAssured.given (). header ("Accept", "application / json"). get (uriOfResource); String etagValue = findOneResponse.getHeader (HttpHeaders.ETAG); bestaandeResource.setName (randomAlphabetic (6)); update (bestaandeResource); // When Response secondFindOneResponse = RestAssured.given (). header ("Accept", "application / json"). headers ("If-None-Match", etagValue) .get (uriOfResource); // Vervolgens assertTrue (secondFindOneResponse.getStatusCode () == 200); }

Stap voor stap:

  • we maken en halen eerst een Bron - en bewaar het ETag waarde voor verder gebruik
  • dan werken we hetzelfde bij Bron
  • stuur een nieuw GET-verzoek, dit keer met de "If-None-Match”-Kop die de ETag die we eerder hebben opgeslagen
  • op dit tweede verzoek zal de server een 200 OK samen met de volledige bron, aangezien het ETag waarde is niet langer correct, aangezien we de bron in de tussentijd hebben bijgewerkt

Ten slotte is de laatste test - die niet gaat werken omdat de functionaliteit in Spring nog niet is geïmplementeerd - de ondersteuning voor de If-Match HTTP-header:

@Test openbare leegte gegevenResourceExists_whenRetrievedWithIfMatchIncorrectEtag_then412IsReceived () {// Gegeven T bestaandeResource = getApi (). Create (createNewEntity ()); // When String uriOfResource = baseUri + "/" + bestaandeResource.getId (); Antwoord findOneResponse = RestAssured.given (). Header ("Accepteren", "application / json"). headers ("If-Match", randomAlphabetic (8)). get (uriOfResource); // Dan assertTrue (findOneResponse.getStatusCode () == 412); }

Stap voor stap:

  • we creëren een bron
  • haal het vervolgens op met de "If-Match”Koptekst die een incorrect specificeert ETag waarde - dit is een voorwaardelijk GET-verzoek
  • de server zou een 412 Voorwaarde mislukt

6. ETags zijn groot

We hebben alleen ETags gebruikt voor leesbewerkingen. Er bestaat een RFC die probeert te verduidelijken hoe implementaties moeten omgaan met ETags bij schrijfbewerkingen - dit is niet standaard, maar het is interessant om te lezen.

Er zijn natuurlijk andere mogelijke toepassingen van het ETag-mechanisme, zoals voor een optimistisch vergrendelingsmechanisme en om het gerelateerde "Lost Update-probleem" aan te pakken.

Er zijn ook een aantal bekende potentiële valkuilen en kanttekeningen waarmee u rekening moet houden bij het gebruik van ETags.

7. Conclusie

Dit artikel heeft alleen de oppervlakte bekrast met wat mogelijk is met Spring en ETags.

Voor een volledige implementatie van een ETag-enabled RESTful-service, samen met integratietests die het ETag-gedrag verifiëren, bekijk je het GitHub-project.

REST onder

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS

$config[zx-auto] not found$config[zx-overlay] not found