HATEOAS pour un service Spring REST

Haut REST

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS

1. Vue d'ensemble

Cet article se concentrera sur l' implémentation de la découvrabilité dans un service Spring REST et sur la satisfaction de la contrainte HATEOAS.

Cet article se concentre sur Spring MVC. Notre article Une introduction à Spring HATEOAS décrit comment utiliser HATEOAS dans Spring Boot.

2. Découplage de la découvrabilité par les événements

La découvrabilité en tant qu'aspect ou préoccupation distinct de la couche Web doit être découplée du contrôleur gérant la requête HTTP. À cette fin, le contrôleur déclenchera des événements pour toutes les actions qui nécessitent une manipulation supplémentaire de la réponse.

Commençons par créer les événements:

public class SingleResourceRetrieved extends ApplicationEvent { private HttpServletResponse response; public SingleResourceRetrieved(Object source, HttpServletResponse response) { super(source); this.response = response; } public HttpServletResponse getResponse() { return response; } } public class ResourceCreated extends ApplicationEvent { private HttpServletResponse response; private long idOfNewResource; public ResourceCreated(Object source, HttpServletResponse response, long idOfNewResource) { super(source); this.response = response; this.idOfNewResource = idOfNewResource; } public HttpServletResponse getResponse() { return response; } public long getIdOfNewResource() { return idOfNewResource; } }

Ensuite, le contrôleur, avec 2 opérations simples - recherchez par identifiant et créez :

@RestController @RequestMapping(value = "/foos") public class FooController { @Autowired private ApplicationEventPublisher eventPublisher; @Autowired private IFooService service; @GetMapping(value = "foos/{id}") public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) { Foo resourceById = Preconditions.checkNotNull(service.findOne(id)); eventPublisher.publishEvent(new SingleResourceRetrieved(this, response)); return resourceById; } @PostMapping @ResponseStatus(HttpStatus.CREATED) public void create(@RequestBody Foo resource, HttpServletResponse response) { Preconditions.checkNotNull(resource); Long newId = service.create(resource).getId(); eventPublisher.publishEvent(new ResourceCreated(this, response, newId)); } }

Nous pouvons ensuite gérer ces événements avec n'importe quel nombre d'écouteurs découplés. Chacun de ceux-ci peut se concentrer sur son propre cas particulier et aider à satisfaire la contrainte globale HATEOAS.

Les écouteurs doivent être les derniers objets de la pile d'appels et aucun accès direct à eux n'est nécessaire; en tant que tels, ils ne sont pas publics.

3. Rendre l'URI d'une ressource nouvellement créée détectable

Comme indiqué dans l'article précédent sur HATEOAS, l'opération de création d'une nouvelle ressource doit renvoyer l'URI de cette ressource dans l' en-tête HTTP Location de la réponse.

Nous allons gérer cela en utilisant un auditeur:

@Component class ResourceCreatedDiscoverabilityListener implements ApplicationListener{ @Override public void onApplicationEvent(ResourceCreated resourceCreatedEvent){ Preconditions.checkNotNull(resourceCreatedEvent); HttpServletResponse response = resourceCreatedEvent.getResponse(); long idOfNewResource = resourceCreatedEvent.getIdOfNewResource(); addLinkHeaderOnResourceCreation(response, idOfNewResource); } void addLinkHeaderOnResourceCreation (HttpServletResponse response, long idOfNewResource){ URI uri = ServletUriComponentsBuilder.fromCurrentRequestUri(). path("/{idOfNewResource}").buildAndExpand(idOfNewResource).toUri(); response.setHeader("Location", uri.toASCIIString()); } }

Dans cet exemple, nous utilisons ServletUriComponentsBuilder - qui aide à utiliser la requête actuelle. De cette façon, nous n'avons pas besoin de transmettre quoi que ce soit et nous pouvons simplement y accéder de manière statique.

Si l'API renverrait ResponseEntity , nous pourrions également utiliser le support Location .

4. Obtenir une seule ressource

Lors de la récupération d'une seule ressource, le client doit être en mesure de découvrir l'URI pour obtenir toutes les ressources de ce type:

@Component class SingleResourceRetrievedDiscoverabilityListener implements ApplicationListener{ @Override public void onApplicationEvent(SingleResourceRetrieved resourceRetrievedEvent){ Preconditions.checkNotNull(resourceRetrievedEvent); HttpServletResponse response = resourceRetrievedEvent.getResponse(); addLinkHeaderOnSingleResourceRetrieval(request, response); } void addLinkHeaderOnSingleResourceRetrieval(HttpServletResponse response){ String requestURL = ServletUriComponentsBuilder.fromCurrentRequestUri(). build().toUri().toASCIIString(); int positionOfLastSlash = requestURL.lastIndexOf("/"); String uriForResourceCreation = requestURL.substring(0, positionOfLastSlash); String linkHeaderValue = LinkUtil .createLinkHeader(uriForResourceCreation, "collection"); response.addHeader(LINK_HEADER, linkHeaderValue); } }

Notez que la sémantique de la relation de lien utilise le type de relation «collection» , spécifié et utilisé dans plusieurs microformats, mais pas encore standardisé.

L'en- tête Link est l'un des en-têtes HTTP les plus utilisés à des fins de découverte. L'utilitaire pour créer cet en-tête est assez simple:

public class LinkUtil { public static String createLinkHeader(String uri, String rel) { return "; rel=\"" + rel + "\""; } }

5. Découvrabilité à la racine

La racine est le point d'entrée dans l'ensemble du service - c'est ce avec quoi le client entre en contact lorsqu'il utilise l'API pour la première fois.

Si la contrainte HATEOAS doit être prise en compte et mise en œuvre partout, c'est ici qu'il faut commencer. Par conséquent, tous les principaux URI du système doivent être détectables à partir de la racine.

Regardons maintenant le contrôleur pour cela:

@GetMapping("/") @ResponseStatus(value = HttpStatus.NO_CONTENT) public void adminRoot(final HttpServletRequest request, final HttpServletResponse response) { String rootUri = request.getRequestURL().toString(); URI fooUri = new UriTemplate("{rootUri}{resource}").expand(rootUri, "foos"); String linkToFoos = LinkUtil.createLinkHeader(fooUri.toASCIIString(), "collection"); response.addHeader("Link", linkToFoos); }

Ceci est, bien sûr, une illustration du concept, se concentrant sur un seul exemple d'URI pour Foo Resources. Une implémentation réelle doit ajouter, de la même manière, des URI pour toutes les ressources publiées sur le client.

5.1. La découvrabilité ne consiste pas à modifier les URI

Cela peut être un point controversé - d'une part, le but de HATEOAS est de permettre au client de découvrir les URI de l'API et de ne pas se fier à des valeurs codées en dur. D'un autre côté, ce n'est pas ainsi que le Web fonctionne: oui, les URI sont découverts, mais ils sont également mis en signet.

Une distinction subtile mais importante est l'évolution de l'API - les anciens URI devraient toujours fonctionner, mais tout client qui découvrira l'API devrait découvrir les nouveaux URI - ce qui permet à l'API de changer dynamiquement, et de bons clients de bien fonctionner même lorsque le Modifications de l'API.

En conclusion - ce n'est pas parce que tous les URI du service Web RESTful doivent être considérés comme des URI sympas (et que les URI sympas ne changent pas) que le respect de la contrainte HATEOAS n'est pas extrêmement utile lors de l'évolution de l'API.

6. Mises en garde concernant la découvrabilité

Comme l'indiquent certaines des discussions autour des articles précédents, le premier objectif de la découvrabilité est de n'utiliser que peu ou pas de documentation et que le client apprenne et comprenne comment utiliser l'API via les réponses qu'elle obtient.

En fait, cela ne devrait pas être considéré comme un idéal exagéré - c'est ainsi que nous consommons chaque nouvelle page Web - sans aucune documentation. Donc, si le concept est plus problématique dans le contexte de REST, alors il doit s'agir d'une mise en œuvre technique, pas d'une question de savoir si c'est possible ou non.

Cela étant dit, techniquement, nous sommes encore loin d'une solution pleinement opérationnelle - la spécification et le support du cadre évoluent encore, et à cause de cela, nous devons faire des compromis.

7. Conclusion

Cet article a couvert l'implémentation de certains des traits de découvrabilité dans le contexte d'un service RESTful avec Spring MVC et abordé le concept de découvrabilité à la racine.

L'implémentation de tous ces exemples et extraits de code peut être trouvée sur GitHub - il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.

REST bas

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS