ETag per REST con Spring

REST Top

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO

1. Panoramica

Questo articolo si concentrerà sull'utilizzo di ETag in primavera , sui test di integrazione dell'API REST e sugli scenari di consumo con curl .

2. REST e ETag

Dalla documentazione ufficiale di primavera sul supporto ETag:

Un ETag (tag di entità) è un'intestazione di risposta HTTP restituita da un server Web conforme a HTTP / 1.1 utilizzata per determinare la modifica del contenuto in un determinato URL.

Possiamo usare ETag per due cose: memorizzazione nella cache e richieste condizionali. Il valore ETag può essere considerato come un hash calcolato dai byte del corpo della risposta. Poiché il servizio probabilmente utilizza una funzione hash crittografica, anche la più piccola modifica del corpo cambierà drasticamente l'output e quindi il valore dell'ETag. Questo è vero solo per ETag forti: il protocollo fornisce anche un Etag debole.

L'utilizzo di un'intestazione If- * trasforma una richiesta GET standard in una GET condizionale. Le due intestazioni If- * che vengono utilizzate con ETags sono "If-None-Match" e "If-Match", ciascuna con la propria semantica, come discusso più avanti in questo articolo.

3. Comunicazione client-server con curl

Possiamo suddividere una semplice comunicazione client-server che coinvolge ETag nei passaggi:

Innanzitutto, il client effettua una chiamata API REST: la risposta include l'intestazione ETag che verrà archiviata per un ulteriore utilizzo:

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

Per la richiesta successiva, il Cliente includerà l' intestazione della richiesta If-None-Match con il valore ETag del passaggio precedente. Se la Risorsa non è cambiata sul Server, la Risposta non conterrà corpo e un codice di stato 304 - Non Modificato :

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"

Ora, prima di recuperare nuovamente la risorsa, cambiamola eseguendo un aggiornamento:

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

Infine, inviamo l'ultima richiesta per recuperare di nuovo il Foo. Tieni presente che lo abbiamo aggiornato dall'ultima volta che lo abbiamo richiesto, quindi il valore ETag precedente non dovrebbe più funzionare. La risposta conterrà i nuovi dati e un nuovo ETag che, ancora una volta, potrà essere memorizzato per un ulteriore utilizzo:

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

Ed ecco fatto: ETag in circolazione e risparmio di larghezza di banda.

4. Supporto ETag in primavera

Passiamo al supporto Spring: l'utilizzo di ETag in Spring è estremamente facile da configurare e completamente trasparente per l'applicazione. Possiamo abilitare il supporto aggiungendo un semplice Filtro nel web.xml :

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

Stiamo mappando il filtro sullo stesso pattern URI dell'API RESTful stessa. Il filtro stesso è l'implementazione standard della funzionalità ETag dalla Spring 3.0.

L'implementazione è superficiale : l'applicazione calcola l'ETag in base alla risposta, il che farà risparmiare larghezza di banda ma non le prestazioni del server.

Quindi, una richiesta che trarrà vantaggio dal supporto ETag verrà comunque elaborata come richiesta standard, consumerà qualsiasi risorsa che normalmente consumerebbe (connessioni al database, ecc.) E solo prima che la sua risposta venga restituita al client il supporto ETag verrà kick in.

A quel punto l'ETag sarà calcolato dal corpo della Risposta e impostato sulla Risorsa stessa; inoltre, se l' intestazione If-None-Match è stata impostata sulla richiesta, verrà gestita anch'essa.

Un'implementazione più profonda del meccanismo ETag potrebbe potenzialmente fornire vantaggi molto maggiori, come servire alcune richieste dalla cache e non dover eseguire affatto il calcolo, ma l'implementazione non sarebbe sicuramente così semplice, né collegabile come l'approccio superficiale descritto qui.

4.1. Configurazione basata su Java

Vediamo come apparirebbe la configurazione basata su Java dichiarando un bean ShallowEtagHeaderFilter nel nostro contesto Spring :

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

Tieni presente che se dobbiamo fornire ulteriori configurazioni di filtro, possiamo invece dichiarare un'istanza FilterRegistrationBean :

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

Infine, se non stiamo usando Primavera di avvio possiamo impostare il filtro utilizzando l'AbstractAnnotationConfigDispatcherServletInitializer s' getServletFilters metodo.

4.2. Utilizzo del metodo eTag () di ResponseEntity

Questo metodo è stato introdotto in Spring Framework 4.1 e possiamo usarlo per controllare il valore ETag recuperato da un singolo endpoint .

Ad esempio, immagina di utilizzare entità con versione come meccanismo di blocco Optimist per accedere alle informazioni del nostro database.

Possiamo usare la versione stessa come ETag per indicare se l'entità è stata modificata:

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

Il servizio recupererà lo stato 304-Not Modified corrispondente se l'intestazione condizionale della richiesta corrisponde ai dati della cache.

5. Test di ETag

Let's start simple – 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. If the Request to retrieve the Resource from the server uses the correct ETag value, then the server doesn't retrieve the Resource:

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

Step by step:

  • we create and retrieve a Resource, storing the ETag value
  • send a new retrieve request, this time with the “If-None-Match” header specifying the ETag value previously stored
  • 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

Abbiamo utilizzato ETag solo per le operazioni di lettura. Esiste un RFC che cerca di chiarire come le implementazioni dovrebbero trattare gli ETag sulle operazioni di scrittura - questo non è standard, ma è una lettura interessante.

Ci sono ovviamente altri possibili usi del meccanismo ETag, come per un meccanismo di blocco ottimistico, oltre alla gestione del relativo "problema di aggiornamento perso".

Ci sono anche diverse potenziali insidie ​​e avvertenze note di cui essere consapevoli quando si utilizzano ETag.

7. Conclusione

Questo articolo ha solo scalfito la superficie con ciò che è possibile fare con Spring ed ETags.

Per un'implementazione completa di un servizio RESTful abilitato ETag, insieme a test di integrazione che verificano il comportamento ETag, controlla il progetto GitHub.

REST fondo

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO