HATEOAS für einen Spring REST Service

HATEOAS für einen Spring REST Service

1. Überblick

Dieser Artikel konzentriert sich aufimplementation of discoverability in a Spring REST Service und auf die Erfüllung der HATEOAS-Einschränkung.

2. Entkoppelbarkeit durch Ereignisse entkoppeln

Discoverability as a separate aspect or concern of the web layer should be decoupled from the controllerverarbeitet die HTTP-Anforderung. Zu diesem Zweck löst der Controller Ereignisse für alle Aktionen aus, die eine zusätzliche Bearbeitung der Antwort erfordern.

Zuerst erstellen wir die Ereignisse:

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

Dannthe 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. Jeder dieser Punkte kann sich auf seinen eigenen Fall konzentrieren und dazu beitragen, die allgemeine HATEOAS-Einschränkung zu erfüllen.

Die Listener sollten die letzten Objekte in der Aufrufliste sein, und es ist kein direkter Zugriff auf sie erforderlich. als solche sind sie nicht öffentlich.

3. Den URI einer neu erstellten Ressource erkennbar machen

Wie inprevious post on HATEOAS,the operation of creating a new Resource should return the URI of that resource in the Location HTTP header der Antwort erläutert.

Wir werden dies mit einem Listener erledigen:

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

In diesem Beispielwe’re making use of the ServletUriComponentsBuilder - Dies hilft bei der Verwendung der aktuellen Anforderung. Auf diese Weise müssen wir nichts weitergeben und können einfach statisch darauf zugreifen.

Wenn die APIResponseEntity zurückgeben würde, könnten wir auchLocation support verwenden.

4. Eine einzelne Ressource erhalten

Beim Abrufen einer einzelnen Ressourcethe client should be able to discover the URI to get all Resources dieses Typs:

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

Beachten Sie, dass die Semantik der Verknüpfungsbeziehung den Beziehungstyp“collection”verwendet, der inseveral microformatsangegeben und verwendet wird, jedoch noch nicht standardisiert ist.

The Link header is one of the most used HTTP headersfor the purposes of discoverability. Das Dienstprogramm zum Erstellen dieses Headers ist einfach genug:

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

5. Entdeckbarkeit an der Wurzel

Das Stammverzeichnis ist der Einstiegspunkt in den gesamten Service - damit kommt der Client in Kontakt, wenn er die API zum ersten Mal verwendet.

Wenn die HATEOAS-Einschränkung in Betracht gezogen und implementiert werden soll, ist dies der Ausgangspunkt. Daherall the main URIs of the system have to be discoverable from the root.

Schauen wir uns nun den Controller dafür an:

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

Dies ist natürlich eine Illustration des Konzepts, das sich auf einen einzelnen Beispiel-URI fürFoo Ressourcen konzentriert. Eine echte Implementierung sollte in ähnlicher Weise URIs für alle für den Client veröffentlichten Ressourcen hinzufügen.

5.1. Bei der Erkennbarkeit geht es nicht darum, URIs zu ändern

Dies kann ein kontroverser Punkt sein. Zum einen besteht der Zweck von HATEOAS darin, dass der Client die URIs der API erkennt und sich nicht auf fest codierte Werte verlässt. Auf der anderen Seite - so funktioniert das Web nicht: Ja, URIs werden erkannt, aber sie werden auch mit Lesezeichen versehen.

Eine subtile, aber wichtige Unterscheidung ist die Weiterentwicklung der API - die alten URIs sollten weiterhin funktionieren, aber jeder Client, der die API erkennt, sollte die neuen URIs erkennen -, wodurch sich die API dynamisch ändern kann und gute Clients auch dann gut funktionieren, wenn die API-Änderungen.

Fazit: Nur weil alle URIs des RESTful-Webdienstes alschttps betrachtet werden sollten: //www.w3.org/TR/cooluris/ [ool]URIs (und coole URIsdon’t change ) - Das bedeutet nicht, dass die Einhaltung der HATEOAS-Einschränkung bei der Weiterentwicklung der API nicht besonders nützlich ist.

6. Vorbehalte der Auffindbarkeit

In einigen Diskussionen zu den vorherigen Artikeln heißt es,the first goal of discoverability is to make minimal or no use of documentation und der Client soll anhand der erhaltenen Antworten lernen und verstehen, wie die API verwendet wird.

Tatsächlich sollte dies nicht als ein so weit hergeholtes Ideal angesehen werden - so konsumieren wir jede neue Webseite - ohne Dokumentation. Wenn das Konzept im Kontext von REST problematischer ist, muss es sich um eine technische Implementierung handeln, nicht um die Frage, ob dies möglich ist oder nicht.

Technisch gesehen sind wir jedoch noch weit von einer voll funktionsfähigen Lösung entfernt - die Spezifikation und die Unterstützung des Frameworks entwickeln sich noch weiter und aus diesem Grund müssen wir einige Kompromisse eingehen.

7. Fazit

Dieser Artikel befasste sich mit der Implementierung einiger Merkmale der Auffindbarkeit im Kontext eines RESTful Service mit Spring MVC und ging auf das Konzept der Auffindbarkeit an der Wurzel ein.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie inmy GitHub project - dies ist ein Maven-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.