HATEOAS pour un service REST Spring

HATEOAS pour un service REST de printemps

1. Vue d'ensemble

Cet article se concentrera sur lesimplementation of discoverability in a Spring REST Service et sur la satisfaction de la contrainte HATEOAS.

2. Découpler la découvrabilité grâce aux événements

Discoverability as a separate aspect or concern of the web layer should be decoupled from the controller gère la requête HTTP. À cette fin, le contrôleur déclenche des événements pour toutes les actions nécessitant 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,the Controller, with 2 simple operations – find by id and create:

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

We can then handle these events with any number of decoupled listeners. 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 à ceux-ci 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 lesprevious post on HATEOAS,the operation of creating a new Resource should return the URI of that resource in the Location HTTP header de la réponse.

Nous allons gérer cela à l'aide d'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,we’re making use of the 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 renverraitResponseEntity - nous pourrions également utiliser lesLocation support.

4. Obtenir une seule ressource

Lors de la récupération d'une seule ressource,the client should be able to discover the URI to get all Resources 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é dansseveral microformats, mais pas encore standardisé.

The Link header is one of the most used HTTP headersfor the purposes of discoverability. L'utilitaire pour créer cet en-tête est assez simple:

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

5. Découvrabilité à la racine

La racine est le point d'entrée de 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 implémentée dans son intégralité, c'est ici que vous devez commencer. Par conséquentall the main URIs of the system have to be discoverable from the root.

Regardons maintenant le contrôleur pour ceci:

@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, en se concentrant sur un unique URI d'échantillon, pourFoo Resources. Une implémentation réelle devrait 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 d'HATEOAS est de permettre au client de découvrir les adresses URI de l'API et de ne pas s'appuyer sur des valeurs codées en dur. D'autre part, ce n'est pas comme cela que le Web fonctionne: oui, les URI sont découverts, mais ils sont également marqués d'un signet.

L’évolution de l’API est une distinction subtile mais importante: les anciens URI doivent toujours fonctionner, mais tout client qui découvre l’API doit découvrir les nouveaux URI. Modifications de l'API.

En conclusion - juste parce que tous les URI du service Web RESTful devraient être considéréschttps: //www.w3.org/TR/cooluris/ [ool]URIs (et cool URIsdon’t change ) - cela ne signifie pas que l'adhésion à la contrainte HATEOAS n'est pas extrêmement utile lors de l'évolution de l'API.

6. Mises en garde de découvrabilité

Comme le disent certaines des discussions autour des articles précédents,the first goal of discoverability is to make minimal or no use of documentation et le client apprennent et comprennent comment utiliser l'API via les réponses qu'il obtient.

En fait, cela ne doit 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, il doit alors s'agir d’une mise en œuvre technique, et non de savoir si cela est possible ou non.

Cela étant dit, techniquement, nous sommes encore loin d'une solution totalement opérationnelle: les spécifications et la prise en charge de la structure évoluent toujours et, pour cette raison, nous devons faire des compromis.

7. Conclusion

Cet article traite de la mise en œuvre de certains des traits de découvrabilité dans le contexte d’un service RESTful avec Spring MVC et aborde le concept de découvrabilité à la racine.

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