ETags pour REST avec Spring

ETags pour REST avec Spring

1. Vue d'ensemble

Cet article se concentrera surworking with ETags in Spring, les tests d'intégration de l'API REST et les scénarios de consommation aveccurl.

Lectures complémentaires:

Introduction aux documents REST Spring

Cet article présente Spring REST Docs, un mécanisme piloté par des tests permettant de générer une documentation des services RESTful à la fois précise et lisible.

Read more

Un type de support personnalisé pour une API REST Spring

Introduction rapide à l'utilisation d'un type de support personnalisé dans une API Spring REST.

Read more

Pagination avec table Spring REST et AngularJS

Un aperçu complet de la manière de mettre en œuvre une API simple avec la pagination avec Spring et de la consommer avec AngularJS et UI Grid.

Read more

2. REST et ETags

Dans la documentation officielle du printemps sur le support ETag:

UnETag (balise d'entité) est un en-tête de réponse HTTP renvoyé par un serveur Web compatible HTTP / 1.1 utilisé pour déterminer le changement de contenu à une URL donnée.

Nous pouvons utiliser ETags pour deux choses: la mise en cache et les demandes conditionnelles. LesETag value can be thought of as a hash calculés à partir des octets du corps de la réponse. Parce que le service utilise probablement une fonction de hachage cryptographique, même la plus petite modification du corps changera radicalement la sortie et donc la valeur de l'ETag. Ceci n'est vrai que pour les ETags forts - le protocole fournit également unweak Etag.

Using an If-* header turns a standard GET request into a conditional GET. Les deux en-têtesIf-* utilisés avec ETags sont «https://tools.ietf.org/html/rfc2616#section-14.26[If-None-Match]» et «https: / /tools.ietf.org/html/rfc2616#section-14.24[If-Match] »- chacun avec sa propre sémantique, comme indiqué plus loin dans cet article.

3. Communication client-serveur aveccurl

Nous pouvons décomposer une simple communication client-serveur impliquant des ETags en plusieurs étapes:

Tout d'abord, le client effectue un appel d'API REST -the Response includes the ETag header qui sera stocké pour une utilisation ultérieure:

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. Si la ressource n’a pas changé sur le serveur, la réponse ne contiendra aucun corps et un code d’étatof 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"

Maintenant, avant de récupérer à nouveau la ressource, modifions-la en effectuant une mise à jour:

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

Enfin, nous envoyons la dernière demande pour récupérer le Foo à nouveau. N'oubliez pas que nous l'avons mis à jour depuis la dernière fois que nous l'avons demandé. La valeur ETag précédente ne devrait donc plus fonctionner. La réponse contiendra les nouvelles données et un nouvel ETag qui, là encore, peuvent être stockés pour une utilisation ultérieure:

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

Et voilà - ETags dans la nature et économiser de la bande passante.

4. Support ETag au printemps

Prise en charge du support Spring: l’utilisation d’ETag in Spring est extrêmement facile à configurer et totalement transparente pour l’application. We can enable the support by adding a simple Filter dans lesweb.xml:


   etagFilter
   org.springframework.web.filter.ShallowEtagHeaderFilter


   etagFilter
   /foos/*

Nous mappons le filtre sur le même modèle d'URI que l'API RESTful elle-même. Le filtre lui-même est l'implémentation standard de la fonctionnalité ETag depuis Spring 3.0.

The implementation is a shallow one - l'application calcule l'ETag en fonction de la réponse, ce qui économisera la bande passante mais pas les performances du serveur.

Ainsi, une demande qui bénéficiera du support ETag sera toujours traitée en tant que demande standard, consommera toutes les ressources qu’elle consommerait normalement (connexions à une base de données, etc.) et ce n’est qu’après avoir renvoyé sa réponse au client que le support ETag dans.

À ce stade, l'ETag sera calculé à partir du corps de réponse et défini sur la ressource elle-même; De plus, si l'en-têteIf-None-Match a été défini sur la requête, il sera également traité.

Une implémentation plus approfondie du mécanisme ETag pourrait potentiellement offrir des avantages bien plus importants - par exemple, répondre à certaines requêtes à partir du cache et ne pas avoir à effectuer le calcul du tout - mais cette implémentation ne serait certainement pas aussi simple, ni aussi enfichable que l'approche superficielle décrit ici.

4.1. Configuration basée sur Java

Voyons à quoi ressemblerait la configuration basée sur Java pardeclaring a ShallowEtagHeaderFilter bean in our Spring context:

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

Gardez à l'esprit que si nous devons fournir d'autres configurations de filtre, nous pouvons à la place déclarer une instanceFilterRegistrationBean:

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

Enfin, si nous n'utilisons pas Spring Boot, nous pouvons configurer le filtre à l'aide de la méthodeAbstractAnnotationConfigDispatcherServletInitializer´sgetServletFilters .

4.2. Utilisation de la méthodeeTag() de ResponseEntity

Cette méthode a été introduite dans Spring Framework 4.1 etwe can use it to control the ETag value that a single endpoint retrieves.

Par exemple, imaginons que nous utilisons des entités versionnées en tant queOptimist Locking mechanism pour accéder aux informations de notre base de données.

Nous pouvons utiliser la version elle-même en tant qu'ETag pour indiquer si l'entité a été modifiée:

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

Le service récupérera l'état304-Not Modified correspondant si l'en-tête conditionnel de la demande correspond aux données de mise en cache.

5. Test des ETags

Commençons simplement -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. Si la demande de récupération desResource du serveur utilise la valeurETag correcte, alors le serveur ne récupère pas la ressource:

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

Pas à pas:

  • nous créons et récupérons une ressource,storing la valeurETag

  • envoyer une nouvelle demande de récupération, cette fois avec l'en-tête «If-None-Match» spécifiant la valeurETag précédemment stockée

  • sur cette seconde requête, le serveur renvoie simplement un304 Not Modified, puisque la Ressource elle-même n'a en effet pas été modifiée entre les deux opérations de récupération

Enfin, nous vérifions le cas où la Ressource est modifiée entre la première et la deuxième demande de récupération:

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

Pas à pas:

  • nous créons et récupérons d'abord unResource - et stockons la valeurETag pour une utilisation ultérieure

  • puis nous mettons à jour les mêmesResource

  • envoyer une nouvelle requête GET, cette fois avec l'en-tête «If-None-Match» spécifiant lesETag que nous avons précédemment stockés

  • sur cette deuxième requête, le serveur retournera un200 OK avec la ressource complète, car la valeur deETag n'est plus correcte, car nous avons mis à jour la ressource entre-temps

Enfin, le dernier test - qui ne fonctionnera pas car la fonctionnalité anot yet been implemented in Spring - estthe 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);
}

Pas à pas:

  • nous créons une ressource

  • puis récupérez-le en utilisant l'en-tête «If-Match» spécifiant une valeurETag incorrecte - il s'agit d'une requête GET conditionnelle

  • le serveur doit renvoyer un412 Precondition Failed

6. Les ETags sont GRANDS

We have only used ETags for read operations. UnRFC exists essayant de clarifier comment les implémentations doivent traiter les ETags sur les opérations d'écriture - ce n'est pas standard, mais c'est une lecture intéressante.

Il existe bien sûr d'autres utilisations possibles du mécanisme ETag, comme pour un mécanisme de verrouillage optimiste ainsi que pour traiter lesrelated “Lost Update Problem”.

Il existe également plusieurspotential pitfalls and caveatsconnus dont il faut tenir compte lors de l'utilisation d'ETags.

7. Conclusion

Cet article n'a fait qu'effleurer la surface avec ce qui est possible avec Spring et ETags.

Pour une implémentation complète d'un service RESTful compatible ETag, ainsi que des tests d'intégration vérifiant le comportement ETag, consultez lesGitHub project.