Beziehungsfragen

REST und HATEOAS – Verlinkung von Resources

Michael Schäfer, Peter Huber

© Ingram Image

Schon wieder ein Artikel über REST, werden Sie jetzt sagen. Ja, schon wieder, sagen wir! Wir haben in diesem Artikel zwei interessante, praxisrelevante Aspekte herausgegriffen, auf die wir in Projekten immer wieder stoßen.

Zum einen ist hier der designtechnische Aspekt zu nennen, der sich hinter der Abkürzung HATEOAS versteckt. HATEOAS ist die Abkürzung für „Hypermedia as the Engine of Application State“. Wir zeigen Ihnen, wie HATEOAS dabei hilft, bessere REST-APIs zu entwerfen. Zum anderen ist der Aspekt der Technik, konkret das Spring Framework, von Interesse. Hier werden wir uns das leichtgewichtige Spring HATEOAS, das uns durch unsere Beispiele begleiten wird, genauer anschauen. Wir setzen dabei ein grundlegendes Verständnis sowohl von REST als auch von Spring voraus.

Wir müssen reden

Wir müssen reden! Keine Angst, das ist keine Beziehungsberatung. Stattdessen widmen wir uns einem wichtigen Aspekt beim Design eines guten REST-API, nämlich der Abbildung von Beziehungen zwischen einzelnen Ressourcen in unserem Datentransportformat. Man kennt das typischerweise aus der Datenbankwelt, in der wir ständig auf 1:1-, 1:n- und n:m-Beziehungen treffen. Durch diesen Artikel begleitet uns ein Beispiel einer 1:n-Beziehung, nämlich die zwischen einem Java Magazin und den Artikeln dieses Magazins (Abb. 1).

Abb. 1: Einfache Relation zwischen Magazin und Artikel

Abb. 1: Einfache Relation zwischen Magazin und Artikel

Stellen wir uns dazu eine REST-Anwendung vor, mit der man auf die Magazine und Artikel zugreifen kann. Da wir uns mit REST in der Webwelt bewegen, ist es naheliegend, bei diesen Beziehungen an die aus dem Web bekannten Links zu denken. Und das ist genau unsere Richtung, denn das primäre Ziel dieses Artikels ist es, eine Antwort auf die folgenden Fragen zu geben: Wie können wir die Beziehung von REST-Ressourcen zueinander mit Links abbilden? Und: Wie hilft uns Spring HATEOAS dabei?

Was aber verbirgt sich hinter dem Begriff HATEOAS? Und was hat das mit unserem Ziel zu tun? Studiert man die Definition in Wikipedia, sticht eine Aussage besonders heraus: „Ein REST-Client benötigt kein Wissen über die Applikation außer dem generischen Wissen über das Hypermedia-Protokoll“.

Dieser Aspekt der REST-Definition wird unserer Erfahrung nach häufig vernachlässigt. Denn das würde ja bedeuten, dass wir Applikationsclients bauen, die völlig generisch mit jeder Applikation zurechtkommen können. Der Praktiker im Projekt sagt: „Das geht doch gar nicht!“, schon gar nicht für komplexe Anwendungen. Die Forderung stammt ursprünglich vom „Vater“ von REST, Roy Fielding, und ist damit eigentlich integraler Bestandteil von REST. Im Detail können wir in diesem Artikel nicht auf diesen Punkt eingehen, denn dahinter verbirgt sich ein ganzes Universum an interessanten Themenstellungen, die ausreichend Stoff für hitzige Diskussionen zwischen Puristen und Pragmatikern in jedem neuen Projekt bieten. Sei es beispielsweise die Content-Type-Negoziation, die Return-Codes oder der Cache-Control, Ressourcenschnitt oder die URL-Gestaltung.

Wir alle kennen übrigens eine sehr mächtige Klasse von REST-Clients, die diese Anforderungen erfüllt – die der Webbrowser. Wie aber sieht die Situation bei einer typischen Businessanwendung aus, einem HTML-5-RIA-Client inklusive selbstgestricktem Java-REST-Server? Kann man in diesem Kontext dasselbe schaffen? Doch kommen wir zurück zur Beziehungsfrage bzw. zu Links: Was bedeutet HATEOAS für den Einsatz von Ressourcenverlinkung in REST-Anwendungen? Sie dürfen gespannt sein. Los geht’s.

„All Systems are functional“

Beginnen wir damit, dass wir unsere Applikation zum Laufen bekommen. An dieser Stelle gleich ein kurzer Hinweis auf unser Repository, in dem Sie den gesamten Quellcode der Beispiele finden. Im ersten Schritt wollen wir mit Spring die einfache Ressource JavaMagazine zum Leben erwecken, und zwar erst einmal als Standard-Spring-Ressource und noch ohne HATEOAS. Dazu benötigen wir eine Rahmenanwendung, die es uns ermöglicht, über einen REST-Client gestellte Anfragen entgegenzunehmen. Dank Spring Boot ist diese Aufgabe in wenigen Zeilen erledigt, und zwar ohne Applikation-Server-Deployment. Wir verwenden einfach eine main-Methode, siehe Listing 1 (an dieser Stelle verneigt sich der Autor vor Spring Boot und drückt tausendfachen Dank aus – so einfach kann und muss Entwicklung sein.). In unserer Rahmenanwendung haben wir auch eine Minipersistenzlösung mit Spring Data bereitgestellt.

Listing 1: „main“-Methode
@ComponentScan @EnableAutoConfiguration @EnableJpaRepositories
public class Application {
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

Unsere erste kleine Ressource ist eine einfache Versionsauskunft für unseren REST-Service. Mit curl (statt curl können Sie z. B. auch SoapUI verwenden) können wir das unkompliziert von der Kommandozeile aus testen:

curl localhost:8080/javamagazine/version

Voilà – unsere einfache Anwendung steht und läuft – All Systems are functional.

Fügen wir nun eine Abfragemöglichkeit für ein Java Magazin hinzu. In JavaMagazineResourceController#javamagazine (Listing 2) sehen wir einen, für Spring typischen, REST-Controller. Dieser bildet einen URI auf die Methode ab, die uns ein einzelnes Java Magazin zurückgibt. Die Klasse JavaMagazine ist in diesem Fall ein einfaches Entity POJO.

Listing 2: „JavaMagazineResourceController#javamagazine“
@RequestMapping(value = "{id}", method = RequestMethod.GET)
public JavaMagazine javamagazine(@PathVariable Long id) {
  return javaMagazineRepository.findOne(id);
}

Nach demselben Schema wird über den ArticleResourceController der Zugriff auf Article ermöglicht. Wenn Sie jetzt bei gestartetem Server mit curl localhost:8080/javamagazine/1 ein JavaMagazine abrufen, sehen Sie als Ergebnis eine JSON-Darstellung des Magazins und der eingebetteten Artikel. Hm? Eingebettet, also. Und wo bleiben die Links?

The missing Link

Es gibt mehrere Möglichkeiten, um die Beziehung zwischen dem Magazin und seinen Artikeln in REST abzubilden. Lassen Sie uns kurz gemeinsam überlegen, welche Anwendungsfälle wir uns in einer typischen Businessapplikation vorstellen können:

  1. „Das ganze Magazin lesen“ = In der Ressource JavaMagazine sind alle Article-Ressourcen eingebettet. Das haben wir beim ersten Test bereits gesehen. Diese Vorgehensweise kann aber beispielsweise im schmalbandigen Mobile-Umfeld ungeschickt sein.
  2. „Die Magazine überfliegen und einzelne, ausgesuchte Artikel lesen“ = Der Aufrufer erhält die Ressource JavaMagazine. Die Verbindungen zu den Article-Ressourcen sind mit Links abgebildet. Das ist die schlanke Variante – der Anwender muss sich selbst weiter fortbewegen. Stellen Sie sich vor, ein Artikel enthielte den gesamten Text inklusive Abbildungen usw. Beim selektiven Lesen werden mit dieser Variante eindeutig weniger Daten übertragen.
  3. JavaMagazine und ShortObjects für Artikel – Eine mögliche Mischform zwischen 1. und 2. Gemeint ist hier beispielsweise ein eingebetteter Abstract des Artikels mit einem Link auf den vollständigen Text.

Ich nehme Tor 2

Entschlacken wir jetzt JavaMagazine von den eingebetteten Artikeln. Wir wollen weiterhin die Antwort im JSON-Format erhalten, aber an der Stelle der eingebetteten Artikeldaten sollen nun Links auf die Artikel stehen. Dazu müssen wir einige kleinere Änderungen vornehmen, denn bisher wissen weder die REST-Controller noch die Entity POJOs etwas von den Links. Bei den Entity POJOs soll das auch so bleiben, denn wir wollen die fachliche Darstellung der Objekte nicht mit der Schnittstellenimplementierung vermischen. Wir wickeln daher einen Spring HATEOAS Resource Wrapper um die Entities und geben die gewrappten Entities aus den REST-Controller-Methoden zurück. Spring HATEOAS klinkt sich dabei so in das System ein, dass die Umwandlung der Ressourcen in JSON-Text transparent für den Entwickler im Hintergrund abläuft.

Allerdings muss dem JSON-Marshaller noch mitgeteilt werden, dass er die Artikel nicht mehr einbetten soll. Wir tun dies, indem wir die Annotation @JsonIgnoreProperties({„article“}) an der Klasse JavaMagazine hinzufügen.

Listing 3 zeigt das Wrapping für das JavaMagazine-Entity, mit seiner HATEOAS-Repräsentation in JavaMagazineResource. Die Änderung der Rückgabewerte der Controller-Methoden sehen wir im JavaMagazineResourceController (Listing 4).

Listing 3: Java Magazine mit Spring HATEOAS Resource Wrapper
@XmlRootElement
public class JavaMagazineResource extends Resource{
  public JavaMagazineResource(JavaMagazine content) { super(content); }
  public JavaMagazineResource() { super(null);  }
}
Listing 4: „JavaMagazineResourceController“ mit geänderten Rückgabewerten
@RequestMapping(value = "{id}", method = RequestMethod.GET, 
      produces = "application/json")
public JavaMagazineResource javamagazine(@PathVariable Long id) {
  JavaMagazine javaMagazine = javaMagazineRepository.findOne(id);
  return javaMagazineResourceAssembler.toResource(javaMagazine);
}

Noch sind wir nicht auf Links gestoßen, denn wir müssen uns noch eine weitere Neuerung ansehen. In den Controllern setzen wir so genannte ResourceAssembler ein. Deren Aufgabe besteht lediglich darin, aus der fachlichen Sicht der Daten die Resource-Sicht mittels einer toResource-Methode zu erstellen. In Listing 5 sehen wir den JavaMagazineResourceAssembler und endlich die Links.

Listing 5: Hinzufügen der Links im „JavaMagazineResourceAssembler“
public JavaMagazineResource toResource(JavaMagazine entity) {  
  JavaMagazineResource result = new JavaMagazineResource(entity);
  // 1. Link to self
  Link selfLink = linkTo(JavaMagazineResourceController.class)
.slash(entity.getId()).withSelfRel();
  result.add(selfLink);
  // 2. Links to articles instead of embedding
  for (Article article : entity.getArticle()) {
    Link linkToArticle = linkTo(methodOn(ArticleResourceController.class)
      .articleByJavaMagazine(entity.getId(),article.getId()))
      .withRel("_article");
      result.add(linkToArticle);
    }
  // 3. Link to list of all articles
  Link linkToArticles = linkTo()
   [...]
  return result; 
}

Es wird nun auch klar, warum wir den Wrapper brauchen: Der Assembler fügt Linkobjekte zum Resource-Objekt hinzu, das wir letztendlich als Ergebnis der Controllermethode zurückgeben werden. Ein sehr wichtiges Konstrukt fehlt jetzt noch, nämlich das des LinkBuilders. In Listing 5 werden nicht einfach hart kodierte Links verwendet, sondern die Links werden aus bereits vorhandenen Informationen, den Annotationen und Methodensignaturen der Controller, zusammengebaut. Bei den „Links to articles“ wird sogar auf einen anderen REST-Controller verwiesen, konkret auf die Methode articleByJavaMagazine in ArticleController (Listing 6). Über diesen Weg werden unsere Link-URLs automatisch richtig erstellt.

Listing 6: ArticleController
@RequestMapping(value = "/javamagazine/{mid}/article/{aid}", 
  method = RequestMethod.GET)
public ArticleResource articleByJavaMagazine(@PathVariable Long mid,
  @PathVariable Long aid) {
  [...]
}

Noch ein interessanter Aspekt ist, dass die Links nicht nur aus einem URL bestehen. Sie sehen im Beispiel in Listing 5, dass wir zwei Arten von Links hinzufügen:

  • Links auf die Artikel _article: Diese werden wie oben beschrieben verwendet, beispielsweise um vom Magazin auf die Artikel zu verweisen.
  • Ein Link auf mich selbst _self: Wozu denn das bitte? Denken wir an eine Kette von Aufrufern. Der endgültige Empfänger erfährt so, woher die Ressource ursprünglich stammte.

Was bringt diese Unterscheidung von Linktypen? Nun, ganz einfach: Die Links haben damit einen „Zweck“ erhalten – als Aussage formuliert: „Ein Link mit dem Tag Article im Kontext einer JavaMagazineResource verweist auf einen Artikel des umschließenden Magazins.“ Es handelt sich um so genannte „Relations“ (siehe http://www.iana.org/assignments/link-relations/link-relations.xhtml und Kasten: „Relations und Semantik“). Der Aufbau eines Links aus Relation plus Verweis folgt somit der ATOM-Spezifikation. Zwei tolle Features aus dem Spring-HATEOAS-Baukasten unterstützen uns dabei, mit Relations professionell umzugehen. Zum einen können wir über den RelProvider eigene Relations einführen und müssen diese nicht hart kodieren, und zum anderen ermöglicht es der CurieProvider, die von uns neu definierten Relations zu dokumentieren. Dadurch können sie sinnvoll verwendet werden.

Relations und Semantik

Bei den Links haben wir gesehen, dass wir über das IANA-Vokabular unseren ATOM-Links „Bedeutung“ hinzufügen können. Man könnte sogar noch weiter gehen und diese Relations mit bestehenden RDF-Vokabeln genauer beschreiben. Damit würde man noch ein Stück näher an das Ideal „Semantic Web“ heranrücken, die Beziehungen zwischen Informationen maschinenlesbar darzustellen. Facebook und andere verwenden beispielsweise in Freundschaftsgraphen den FOAF-Namensraum („Friend of a Friend“) und daraus das Prädikat foaf:knows, um Links zwischen Personen, die sich kennen, abzubilden. Um diese bei der IANA unbekannten Relations zu nutzen, unterstützt uns Spring HATEOAS durch so genannte Curies. Curies, RFD und JSON-LD sind einige Stichwörter für spannende Forschungsprojekte im Bereich „Semantic Web“.

Blicken wir zurück auf den oben aus der HATEOAS-Definition herausgesuchten Aspekt. Dafür muss es unter anderem einen Mechanismus der Selbstbeschreibung geben, der einzelnen Elementen „Semantik“ hinzufügt. Ein Link ist nicht mehr einfach nur ein Link „irgendwohin“, sondern ein Link auf einen Artikel. Wir sind also dem Ziel schon ein Stück näher gekommen. Wir zahlen dafür einen Preis, indem wir nicht einfach wie bisher Entity POJOs aus den REST-Controllern zurückgeben können. Dafür, dass wir die REST-Spezifikation jetzt vollständig erfüllen, ist dieser Preis aber gerechtfertigt – finden zumindest wir.

Andere „Seiten“ aufziehen

Unser oben dargestelltes Ziel haben wir somit erreicht. Gut, dass am Ende immer noch ein Bonbon kommt. Was hindert uns eigentlich daran, Links auch für mehr als lediglich für Beziehungen einzusetzen? Wer dem Link oben zu den Relations gefolgt ist, fand sich mit vielen vordefinierten Relations konfrontiert. Ich greife zwei davon heraus: next und prev. Wer jetzt denkt „Ach, kommt jetzt was zu Paging?“, der hat den Hauptpreis gewonnen.

Schauen wir uns das am Beispiel des Pagings durch den Katalog aller Java Magazine an. Wir beginnen mit der Controllermethode pagingSimple in Listing 7. In unserem URL wollen wir die aktuelle Seite und auch die Seitengröße mitgeben können, und zwar als gewöhnliche REST-Parameter. In der Methode sehen wir sowohl Altbekanntes als auch Neues.

Listing 7: Simpler Paging Controller
@RequestMapping(method = RequestMethod.GET, produces = "application/hal+json")
public PagedResources<Resource> pagingSimple(
  @RequestParam Integer page, @RequestParam Integer size) {
  Page pageResult = javaMagRepo
  .findAll(new PageRequest(page, size));
  PagedResources<Resource> pagedResources = 
  pagedResourceAssembler.toResource(pageResult);
  return pagedResources;
}
Ende

Schauen wir uns die Methode Schritt für Schritt an. Wir starten mit einer Abfrage gegen die Datenbank und sehen dort bereits in Spring Data das Konzept Paging auf der Datenbankseite verwirklicht. Dann delegieren wir wieder an einen ResourceAssembler (Listing 8). Dieser hier ist aber von PagedResourcesAssembler abgeleitet. Das heißt, dass die die Datenbank-Page gleich direkt in eine REST-Page umgewandelt wird. Und das mit kaum eigenem Code. Spring Data und Spring HATEOAS sind wirklich großartig aufeinander abgestimmt. Und das war es auch schon. Leichter geht’s nicht.

Wenn wir uns die Antwort ansehen, die wir über den URL http://localhost:8080/javamagazine/?page=0&size=3 erhalten, fällt auf, dass die Daten der Java Magazine weder Links auf die Artikel noch die Artikel eingebettet enthalten. Woran liegt das? Wir haben weiter oben beschlossen, per JsonIgnore die Einbettung zu verhindern und hatten stattdessen unseren eigenen JavaMagazineResourceAssembler verwendet, um die Links zu erzeugen. Um jetzt Paging und Links gleichzeitig zu erhalten, müssen wir einfach beim Aufruf von toResource des Paging ResourceAssemblers unseren JavaMagazineResourceAssembler mitgeben, siehe Listing 9. Beachten Sie, dass sich durch diese Änderung auch der Rückgabewert ändert.

Listing 8: „JavaMagazinePageResourceAssembler“
@Component @Scope(WebApplicationContext.SCOPE_REQUEST)
public class JavaMagazinePageResourceAssembler extends
PagedResourcesAssembler {
  public JavaMagazinePageResourceAssembler() {
    super(null, linkTo(JavaMagazineResourceController.class)
    .toUriComponentsBuilder().build());
  }
}
Listing 9: Paging mit eingebetteten Links
@RequestMapping(method = RequestMethod.GET, produces = "application/hal+json")
public PagedResources paging(
  @RequestParam Integer page, @RequestParam Integer size) {
  Page pageResult = javaMagRepo
  .findAll(new PageRequest(page, size));
  PagedResources pagedResources = 
    pagedResourceAssembler
  .toResource(pageResult, javaMagazineResourceAssembler);
  return pagedResources;
}

Do you read me, HAL?

Jetzt sind wir doch fertig, oder? Ja, zumindest fast, denn wir haben uns das Ausgabeformat bisher noch nicht näher angesehen. Wie wir in Listing 7 sehen, sind unsere Daten schön sauber in einzelne Sektionen unterteilt. Woher aber stammen die Identifier _links, _embedded? Dabei handelt es sich um einen Bestandteil der HAL-Spezifikation, kurz für „Hypertext Application Language“. HAL definiert sich selbst als „[…] a simple format that gives a consistent and easy way to hyperlink between resources in your API“. Und das Feine ist: Wir bekommen dieses strukturierte Format mit Spring HATEOAS geschenkt. In Listing 10 sehen wir die Darstellung einer PageResource im HAL-Format. Neben der Sektion für die Links mit einem next-Link und einem self-Link sehen wir eine Sektion _embedded mit einer Liste von JavaMagazine-Einträgen (hier gekürzt auf einen Eintrag). Die eingebetteten Java Magazine wiederum verwenden ihrerseits Links auf die Artikel. Die eigentlichen Daten der Page-Ressource stehen ganz am Ende. In diesem Beispiel haben wir somit alle wichtigen Features vereint. Noch einmal kurz zurück zum self-Link: Dieser wird als Template bezeichnet, und wir sehen über {?page,size,sort} einen Verweis auf Werte, die wir noch setzen müssen, damit tatsächlich ein Link daraus wird. Wir können dabei auf die Attribute page und size in der Sektion page zurückgreifen, das heißt, dass das self-Link-Template mit diesen Werten zu einem validen Link erweitert werden muss.

Listing 10: HAL-Ausgabe (gekürzt)
{
  "_links": {
    "next": {
      "href": "http://localhost:8080/javamagazine?page=1&size=3"
    },
    "self": {
      "href": "http://localhost:8080/javamagazine{?page,size,sort}",
      "templated": true
    }
  },
  "_embedded": {
    "javaMagazineList": [
      {
        "id": 1,
        "name": "Ausgabe 1",
        "_links": {
          "self": {
            "href": "http://localhost:8080/javamagazine/1"
          },
          "_article": [
            {
              "href": "[...]/article/javamagazine/1/article/1"
            },
            {
              "href": "[...]/article/javamagazine/1/article/2"
            },
          ],
        }
      }
    },
    "page": {
      "size": 3,
      "totalElements": 4,
      "totalPages": 2,
      "number": 0
    }
  }
}

Fazit und Ausblick

Was haben wir heute gelernt? Wenn wir uns in einem Projekt für das Gespann aus  HATEOAS und REST entscheiden, dann treffen wir auf immer wiederkehrende Problemstellungen, beispielsweise, was ich in einen Link abbilde. Hierbei kann uns Spring HATEOAS optimal unterstützen. Es wird eine Linkklasse angeboten, die die ATOM-Spezifikation erfüllt. Es wird ein ResourceSupport angeboten, der es ermöglicht, Links und Entitäten gemeinsam zu verwalten. Schlussendlich unterstützen uns verschiedene Builder und Assembler bei der Erstellung der Links und Ressourcen. Darüber hinaus wird nach außen das definierte Format HAL unterstützt, das über verschiedene Klassen angepasst und erweitert werden kann. In einem einfachen Beispiel konnten wir den Einsatz von Spring HATEOAS verdeutlichen. Und wer sich die Arbeit, noch Controller zu schreiben, sparen möchte, kann direkt seine Entitäten über Spring Data REST exportieren, denn Spring Data REST setzt Spring HATEOAS als Framework ein.

Wie geht es weiter für den interessierten, neugierigen Leser? Hier ein paar Anregungen für Forschungsprojekte:

  • Pagination-Links: seitenweises Lesen mit Seitennummern statt mit next und prev.
  • Sortierungs-Links: verschiedene Sortierungsvarianten.
  • Verteilung/Aggregation von Ressourcen aus verschiedenen Quellen; _self wird plötzlich sinnvoll.
  • Links mit dem Spring HATEOAS Objekt EntityLinks erstellen und @ExposesResourceFor verwenden.
  • Short-Objekte: Eine Mischform zwischen Einbetten und Verlinkung. Es werden wenige Basisinformationen eingebettet (z. B. „Artikelüberschrift“) und durch Verweise auf die vollständige Ressource ergänzt.
Geschrieben von
Michael Schäfer
Michael Schäfer
Michael Schäfer arbeitet als Lead IT Consultant im Bereich Applied Technology Research bei der msg systems ag in München. Sein Schwerpunkt liegt auf dem Spring-Ökosystem. In diesem Bereich ist er in Projekten, als Trainer und Autor unterwegs. Aktuell schreibt er an einem shortcut zum Thema JPA-2.1-Persistence-Features.
Peter Huber
Peter Huber
Peter Huber ist als Lead IT Consultant im Bereich Applied Technology Research bei der msg systems ag in München tätig. Seine Schwerpunkte liegen in der Architektur von Java-EE- und Spring-Anwendungen. Er arbeitet in diesem Bereich auch als Trainer. Seine Steckenpferde sind das Semantic Web und Data Science mit R.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: