Inhaltsverzeichnis

Moderne Webservices werden immer mehr als REST-Webservices entworfen und es scheint fast so, als wäre SOAP vielerorts gar kein Thema mehr. In dieser Stelle gehen wir daher näher auf REST ein und zeigen, wie es mit Java funktioniert.

Lernziele dieser Einheit

Nach Abschluss dieser Einheit kannst du …

🏁

SOAP vs. REST

Bildnachweis: Pixabay: rawpixel

Warum REST so anders ist

SOAP

🐘 Eine URL für den gesamten Webservice
🐘 Ermöglicht den Aufruf beliebiger Methoden
🐘 Nutzt von HTTP lediglich das Verb POST
🐘 SOAP-XML als fest vorgegebenes Datenformat
🐘 Verbindliche WSDL-Beschreibung je Service
🐘 Häufigste Verwendung im Unternehmenskontext

REST

🐇 Je eine URL für eine Ressource¹
🐇 Bildet die üblichen CRUD-Operationen² ab
🐇 Nutzt alle Verben des HTTP-Protokolls
🐇 Beliebige Datenformate, in der Regel JSON
🐇 Optionale WADL-Beschreibung möglich
🐇 Häufigste Verwendung für Cloud, Web und Mobile

¹ Im Sinne eines einzelnen Datensatzes, zum Beispiel ein Artikel oder eine Bestellung.

² Create, Read, Update und Delete

Bildnachweise: Pixabay: ronbd, Pixabay: Bru-nO,
SOAP is so much more polite than REST

Bildnachweis: Geek & Poke: 30.11.2009

Technische Hintergründe zu REST

Bei den URLs für einen REST-Webservice wird häufig zwischen Collections und Ressourcen unterschieden. Eine Collection ist dabei immer eine Sammlung von Einträgen, die als Ganzes abgerufen werden kann. Oftmals können beim Abruf dann noch Query Parameter mitgegeben werden, um die Liste einzugrenzen.

Eine einzelne Ressource erkennt man daran, dass in der URL ihr Schlüsselwert enthalten ist. Im Gegensatz zu den Collections schickt der Server dann genau ein Objekt mit den Daten der angeforderten Ressource.

Die Anlage von Ressourcen erfolgt häufig, indem ihre JSON-Repräsentation als POST-Anfrage an die Collection geschickt wird. Der Server speichert daraufhin den Eintrag und schickt die gespeicherten Daten, nun um die Key-Werte angereichert, zurück.

Im Gegensatz zu SOAP besitzen REST-Webservices meistens keine formale Beschreibung. Viele REST-API-Anbieter liefern daher vorgefertigte SDKs zum Aufruf ihrer Services mit verschiedenen Sprachen. Im Java-Umfeld hat man sich stattdessen das WADL-Format als eine Art „WSDL für beliebige Webanwendungen” überlegt. Es beschreibt die URLs eines Webservices und mit welchen HTTP-Methoden sie aufgerufen werden.

⚛️ Welche URLs besitzt der Webservice?
⚛️ Welche URL-Parameter können einer URL mitgegeben werden?
⚛️ Welche HTML-Formularfelder können an die URL geschickt werden?
⚛️ Welche Daten können mit POST, PUT oder PATCH an die URL geschickt werden?
⚛️ Welche HTTP-Statuscodes kann die Antwort enthalten?
⚛️ Welche Daten werden bei welchem Statuscode zurückgeschickt?

Beispiel

<application xmlns="http://wadl.dev.java.net/2009/02" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <!-- Hier könnten XML-Schema-Anweisungen stehen, mit welchen die Struktur der ausgetauschten Daten formal definiert wird, wenn der Webservice XML als Datenformat verwendet. Dieser hier nutzt allerdings JSON. 🙄 --> <grammars/> <!-- Die Basis-URL des Webservice lautet http://localhost:8080/TheXFiles/rest/ --> <resources base="http://localhost:8080/TheXFiles/rest/"> <!-- Nachfolgend wird die URL http://localhost:8080/TheXFiles/rest/xfiles2018/ beschrieben. --> <resource path="xfiles2018"> <!-- Schicken wir eine GET-Anfrage an die URL, wird dadurch die Methode findEpisodes() aufgerufen. --> <method id="findEpisodes" name="GET"> <!-- Optional können wir der Methode einen Parameter namens "query" mitgeben. Der Parameter ist ein String und muss als URL-Parameter an die URL angehängt werden. Zum Beispiel so: http://localhost:8080/TheXFiles/rest/xfiles2018/?query=My%20Struggle --> <request> <param name="query" style="query" type="xs:string"/> </request> <!-- Als Antwort erhalten wir dann einen JSON-String. --> <response> <representation mediaType="application/json"/> </response> </method> <!-- Eine POST-Anfrage würde hingegen die Methode saveNewEpisode() zur Anlage eines neuen Datensatzes aufrufen. --> <method id="saveNewEpisode" name="POST"> <!-- Als Anfrage müssen wir einen JSON-String mitschicken. --> <request> <representation mediaType="application/json"/> </request> <!-- Im Gegenzug schickt uns der Server ein JSON zurück. --> <response> <representation mediaType="application/json"/> </response> </method> <!-- Mit der URL http://localhost:8080/TheXFiles/rest/xfiles2018/{id}/ können wir auf einzelne Datensätze zugreifen. {id} ist dabei ein Platzhalter, der mit der ID des Datensatzes ersetzt werden muss. --> <resource path="{id}"> <!-- Die ID kann ein beliebiger String sein. Häufig kommt es aber auch vor, dass es sich um einen Long oder irgend einen anderen, primitiven Datentypen handeln muss. --> <param name="id" style="template" type="xs:string"/> <!-- Schicken wir eine GET-Anfrage an die URL, wird dadurch die Methode getEpisode() aufgerufen. Sie schickt uns eine einzelne Episode im JSON-Format zurück. --> <method id="getEpisode" name="GET"> <response> <representation mediaType="application/json"/> </response> </method> <!-- Schicken wir eine DELETE-Anfrage, wird stattdessen die Methode deleteEpisode() aufgerufen. Auch sie schickt uns die gelöschte Episode im JSON-Format zurück. --> <method id="deleteEpisode" name="DELETE"> <response> <representation mediaType="application/json"/> </response> </method> <!-- Schicken wir eine PUT-Anfrage, können wir dadurch die Methode updateEpisode() zum Aktualisieren eines Datensatzes aufrufen. Hierfür müssen wir ein JSON mit den Episodendaten hinschicken und bekommen ein JSON mit den gespeicherten Daten zurück. --> <method id="updateEpisode" name="PUT"> <request> <representation mediaType="application/json"/> </request> <response> <representation mediaType="application/json"/> </response> </method> </resource> <!-- http://localhost:8080/TheXFiles/rest/xfiles2018/{id}/ --> </resource> <!-- http://localhost:8080/TheXFiles/rest/xfiles2018/ --> </resources> <!-- http://localhost:8080/TheXFiles/rest/ --> </application>

REST-Webservices testen mit SoapUI

Bildnachweis für das Endesymbol: Pixabay: janf93

Aufgabe 1: SoapUI kann auch REST

Nun, welche Musik hörst du gerne? 🎼 Hier hast du die einmalige Gelegenheit, sie über einen REST-Webservice in eine Datenbank einzutragen. 😎 Hierfür musst du dir lediglich das Beispielprojekt von Moodle herunterladen und in Netbeans zum Laufen bekommen. Anschließend öffnet sich eine kleine Seite im Browser, mit der du erste Versuche unternehmen kannst. Gehe dabei wie folgt vor:

  1. Klicke jeden Link einmal an und schaue, was passiert.
  2. Falls die Links mit der ID 151 keine Daten liefern, tausche die ID in der URL aus.
  3. Schaue dir die WADL-Datei an und versuche, sie zu verstehen.
  4. Importiere die WADL-Datei in SoapUI und teste die verschiedenen Webservice-Operationen.

Um einen neuen Song anzulegen, kannst du folgende JSON-Daten an den Server schicken:

{ "name": "Circle of Life", "artist": "Elton John", "songwriters": "Tim Rice", "releaseYear": 1994 }
Screenshot des Webservices im Browser
Screenshot des Webservices in SoapUI

Aufgabe 2: Ein kleines REST-Quiz

Aufgabe 2.1: Allgemeine Fragen

a) REST-Webservices besitzen eine einzige URL für alle Operationen.

  1. Wahr
  2. Falsch

b) REST-Webservices nutzen häufig JSON als Datenformat, andere Formate können aber auch vorkommen.

  1. Wahr
  2. Falsch

c) REST-Webservices bieten einen ähnlich großen Funktionsumfang, wie SOAP mit seinen WS-*-Standards.

  1. Wahr
  2. Falsch

Aufgabe 2.2: Fragen zur Technik

a) Welche Aufgabe hat das Accept-Header-Field bei einem REST-Aufruf?

  1. Es beschreibt das Datenformat, dass der Server an den Client sendet.
  2. Es beschreibt das Datenformat, dass der Client an den Server sendet.
  3. Es beschreibt das Datenformat, dass der Client gerne vom Server empfangen möchte.

b) Welche Aufgabe hat das Content-Type-Header-Field bei einem REST-Aufruf?

  1. Es beschreibt das Datenformat, dass der Server an den Client sendet.
  2. Es beschreibt das Datenformat, dass der Client an den Server sendet.
  3. Es beschreibt das Datenformat, dass der Client gerne vom Server empfangen möchte.

c) Welche der folgenden URLs dient dem Abruf einer Collection mit mehreren Songs?

  1. https://example.com/api/songs/elton-john/your-song/
  2. https://example.com/api/songs/
  3. https://example.com/api/songs/511/
  4. https://example.com/songs/your-song/?details=all

d) Welche Aufgabe haben die HTTP-Verben PUT und POST bei REST?

  1. Neue Daten anlegen bzw. vorhandene Daten aktualisieren
  2. Eine Liste mit mehreren Datensätzen auf einmal löschen
  3. Daten in einem anderen Format als JSON vom Server abrufen

e) Wie kann man bei manchen REST-Webservices einzelne Felder anstatt allen Daten ändern?

  1. Das geht nicht, da es kein passendes HTTP-Verb hierfür gibt.
  2. Mit dem HTTP-Verb PUT, indem nur die zu ändernden Felder an den Server geschickt werden.
  3. Mit dem HTTP-Verb PATCH, indem nur die zu ändernden Felder an den Server geschickt werden.
  4. Mit dem HTTP-Verb POST, indem nur die zu ändernden Felder an den Server geschickt werden.

Lösung: Aufgabe 2.1: 2, 1, 2
Aufgabe 2.2: 3, 1 + 2, 2, 1, 3

REST-Webservices mit Java

Bildnachweis: Pixabay: rawpixel

Eigene Webservices definieren

Application-Klasse

Ein REST-Webservice besitzt in Java immer mindestens zwei Klassen. Eine zentrale Application-Klasse als Einstieg, in der der URL-Prefix definiert wird, sowie mindestens eine Klasse für die eigentlichen Collections und Resourcen. Die Applikation-Klasse bindet dabei alle anderen Klassen in ihrem Quellcode ein.

@ApplicationPath

Steht vor der Application-Klasse und definiert dort die Basis-URL aller Ressourcen.

Webservice-Klassen

Jede Collection sowie jede Ressource wird in einer eigenen Klasse ausprogrammiert. Sie beinhaltet die einzelnen Methoden des Webservices und legt fest, mit welchen URL-Mustern und HTTP-Verben die Methoden aufgerufen werden.

@GET

Kennzeichnet eine Methode, die bei einer GET-Anfrage aufgerufen wird und dem Abruf von Daten dient.

@POST

Kennzeichnet eine Methode, die bei einer POST-Anfrage aufgerufen wird und dem Speichern von dient.

@PUT

Kennzeichnet eine Methode, die bei einer PUT-Anfrage aufgerufen wird und der Änderung von Daten dient.

@DELETE

Kennzeichnet eine Methode, die bei einer DELETE-Anfrage aufgerufen wird und der Löschung von Daten dient.


@Produces

Steht vor einer Methode und definiert den MIME-Type der zurückgelieferten Daten.

@Consumes

Steht vor einer Methode und definiert den MIME-Type der durch die Methode verarbeiteten Daten.


@Path

Kann vor einer Klasse oder Methode stehen und definiert den Teil der URL, der zum Aufruf der jeweiligen Klasse oder Methode führt. In geschweiften Klammern können dabei Platzhalter stehen, die als Parameter an die aufgerufene Methode übergeben werden. Zum Beispiel: @Path("Songs/{id}")

@PathParam

Kennzeichnet einen Methodenparameter, dessen Wert über einen Platzhaler in der URL ermittelt wird. Zum Beispiel: @PathParam("id") String id

@QueryParam

Steht vor einem Methodenparameter und legt fest, dass dieser in Form eines URL-Parameters übergeben wird. Zum Beispiel muss bei @QueryParam("search") String search der String ?search=Queen an die URL abgehängt werden, um den Wert Queen zu übergeben.

Wie immer benötigen wir erst einmal ein paar Persistence Entities und Enterprise Java Beans. Zumindest, wenn wir Daten aus einer Datenbank lesen und schreiben wollen. 🛢️ Für die EJB nutzen wir wie immer die EntityBean aus den JPA-Folien.

/** * Einfache Entity-Klasse für einen Song. */ @Entity public class Song implements Serializable { @Id @GeneratedValue(generator = "song_ids") @TableGenerator(name = "song_ids", initialValue = 0, allocationSize = 50) private final long id = 0L; private String name = ""; private String artist = ""; private String songwriters = ""; private int releaseYear = 0; // Konstruktoren // Setter und Getter } /** * Einfache EJB mit den üblichen Methoden zum Lesen und Schreiben von Songs. * Diese Klasse nutzt unsere altbekannte EntityBean aus den JPA-Folien, um * einen Grundstock an Standardmethoden anzubieten. */ @Stateless public class SongBean extends EntityBean { public SongBean() { super(Song.class); } }

Im Gegensatz zu SOAP benötigt jeder REST-Webservice in Java eine Application-Klasse, welche den URL-Prefix des Webservices definiert und in welche alle weiteren Klassen aufgenommen werden müssen. Wichtig sind hierbei die Annotation @ApplicationPath für die URL sowie die Methode getClasses(), die eine Liste aller abhängigen Klassen zurückliefert.

/** * Einstiegspunkt für unseren REST-Webservice. Hier werden der URL-Prefix aller * Aufrufe definiert (über @ApplicationPath), sowie alle Collections und Ressourcen * dem Webservice hinzugefügt. Diese Klasse muss daher immer angepasst werden, * wenn weitere Collections oder Ressourcen hinzukommen. */ @ApplicationPath("api") public class MusicalRestAPI extends Application { @Override public Set<Class<?>> getClasses() { Set<Class<?>> resources = new HashSet<>(); // Hier für jede Webservice-Klasse eine Zeile hinzufügen resources.add(SongCollection.class); resources.add(SongResource.class); return resources; } }

Bei den eigentlichen Webservice-Klassen, die jeweils für eine Collection oder Ressource des Webservices stehen, handelt es sich wieder um altbekannte Stateless Session Beans. 👴🏽 Ihre Methoden werden durch die übrigen Annotationen auf die verschiedenen URLs und HTTP-Verben gemappt. Fangen wir also mit der Klasse für eine Collection an, um eine ganze Liste auszulesen (bzw. Einträge suchen) und neue Einträge anlegen zu können:

/** * Collection "Songs" zum Suchen von Songs und Speichern neuer Songs. */ @Stateless @Path("Songs") public class SongCollection { @EJB SongBean songBean; /** * GET /api/Songs/List/ * Auslesen einer Liste von Musikstücken. */ @GET @Produces(MediaType.APPLICATION_JSON) public List<Song> findSongs() { return this.songBean.findAll(); } /** * POST /api/Songs/ * Speichern eines neuen Songs. */ @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public String saveNewSong(Song song) { return this.songBean.saveNew(song); } }

Anschließend legen wir eine weitere Klasse für einzelne Ressourcen an. Streng genommen könnten die Methoden zwar auch alle in einer Klasse stehen (@Path kann auch vor einer Methode stehen), so ist es jedoch übersichtlicher und damit wartbarer.

/** * Ressource für einen einzelnen Song. Hier kann ein einzelner Song abgerufen, * aktualisiert oder gelöscht werden. */ @Stateless @Path("Songs/{id}") public class SongResource { @EJB SongBean songBean; /** * GET /api/Songs/{id}/ * Auslesen eines einzelnen Songs anhand seiner ID. */ @GET @Produces(MediaType.APPLICATION_JSON) public Song getSong(@PathParam("id") long id) { return this.songBean.findById(id); } /** * PUT /api/Songs/{id}/ * Aktualisieren eines vorhandenen Songs. */ @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Song updateSong(Song song) { return this.songBean.update(song); } /** * DELETE /api/Songs/{id}/ * Löschen eines vorhandenen Songs. */ @DELETE @Produces(MediaType.APPLICATION_JSON) public Song deleteSong(@PathParam("id") long id) { Song song = this.songBean.findById(id); if (response.song != null) { this.songBean.delete(response.song); } return song; } }

Die Antwortdaten flexibler gestalten

Damit der hier beschriebene Trick funktioniert, ist es wichtig, das Projekt als Maven-Projekt anzulegen. Dadurch wird das Build-Werkzeug Maven anstelle von Ant verwendet. Dies ermöglicht es uns, ganz einfach fremde Abhängigkeiten wie die GSON-Bibliothek hinzufügen können, ohne die hierfür notwendigen Daten manuell herunterladen zu müssen. Außerdem lässt sich das Projekt somit auch in anderen IDEs öffnen oder gar völlig ohne IDE weiterentwickeln.

Maven / Web Application ist der richtige Projekttyp

Innerhalb des Ordners Project Files findet sich dann die Datei pom.xml. Sie gehört zu Maven und beschreibt das gesamte Projekt, unter anderem auch, welche Abhängigkeiten es besitzt. Die nachfolgenden Zeilen müssen im Bereich <dependencies>…</dependencies> eingefügt werden:

<project> … <dependencies> … <!-- Google GSON =========== Damit lässt sich ein Javaobjekt nach JSON umwandeln und umgekehrt. https://mvnrepository.com/artifact/com.google.code.gson/gson --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.2</version> </dependency> </dependencies> … </project>

Und hier nun die angepasste Version der Klasse SongCollection. Innerhalb der Klasse befindet sich ein GSON-Objekt, das zum (De)serialisieren der JSON-Daten verwendet werden kann. Diese werden den Methoden daher nun als String übergeben und auch der Rückgabewert aller Methoden ist ein String. Zusätzlich beinhaltet die Klasse eine innere Klasse namens Response, die alle für eine Antwort benötigten Felder enthält.

  • song: RÜckgabedaten eines einzelnen Songs, falls die Methode immer nur einen Song liefern kann.
  • songs: Rückgabedaten einer ganzen Liste von Songs, falls die Methode potentiell mehrere Einträge liefern kann.
  • status: Ein einfacher Statuscode wie OK oder ERROR, anhand dessen der Client erkennen kann, ob der Aufruf erfolgreich war.
  • exception: Der Name der aufgetretenen Exception, falls es zu einem Fehler kam.
  • message: Eine für den Anwender verständliche Status- oder Fehlermeldung, die der Client bei Bedarf anzeigen kann.

Die veränderte SongCollection-Klasse sieht dadurch wie folgt aus. Natürlich muss die Klasse SongResource analog dazu angepasst werden, auch wenn das hier nicht gezeigt wird.

/** * Collection "Songs" zum Suchen von Songs und Speichern neuer Songs. */ @Stateless @Path("Songs") public class SongCollection { @EJB SongBean songBean; // GSON-Objekt zur (De)serialisierung von JSON Gson gson = this.gson = new GsonBuilder().create(); /** * Datencontainer für die Antwortdaten, die von den Methoden unten jeweils * zurückgeschickt werden können. */ public class Response { public Song song; public List<Song> songs; public String status; public String exception; public String message; } /** * GET /api/Songs/List/ * Auslesen einer Liste von Musikstücken. */ @GET @Produces(MediaType.APPLICATION_JSON) public String findSongs() { Response response = new Response(); try { response.songs = this.songBean.findAll(); response.status = "OK"; } catch (Exception ex) { response.status = "ERROR"; response.exception = ex.getClass().getName(); response.message = ex.getMessage(); } return this.gson.toJson(response); } /** * POST /api/Songs/ * Speichern eines neuen Songs. */ @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public String saveNewSong(String json) { Response response = new Response(); try { Song song = this.gson.fromJson(json, Song.class); response.song = this.songBean.saveNew(song); response.status = "OK"; } catch (Exception ex) { response.status = "ERROR"; response.exception = ex.getClass().getName(); response.message = ex.getMessage(); } return this.gson.toJson(response); } }

REST-Webservices mit Java aufrufen

🏗️ Eigentlich ist es ganz leicht, wir müssen aber alles selbst programmieren.
🏗️ Selbst mit der WADL können wir keinen sinnvollen Code automatisch generieren.
🏗️ Mit Google Gson und Unirest for Java ist es aber gar nicht so schwer.
🏗️ Zumindest schon mal nicht schwerer als direkt mit den clientseitigen JAX-RS-Klassen.

Damit wir die abhängigen Bibliotheken einbinden können, müssen wir wieder ein Maven-Projekt anlegen. Dieses mal eine Java Application.

Neue Java-Anwendung mit Maven in Netbeans

Die Abhängigkeiten werden dann wie folgt in der Maven-Datei pom.xml eingetragen:

<project> … <dependencies> … <!-- Google GSON =========== Damit lässt sich ein Javaobjekt nach JSON umwandeln und umgekehrt. https://mvnrepository.com/artifact/com.google.code.gson/gson --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.2</version> </dependency> <!-- Unirest for Java ================ Damit lassen sich ganz leicht, HTTP-Anfragen absetzen. http://unirest.io/java.html --> <dependency> <groupId>com.mashape.unirest</groupId> <artifactId>unirest-java</artifactId> <version>1.4.9</version> </dependency> </dependencies> … </project>

So soll es später aussehen. Ein lokales Stub-Objekt dient als „Stellvertreter” für die entfernten Methoden des Servers. Wir rufen einfach die Methoden des Stubs auf, um in Wirklichkeit eine Anfrage an den Server zu schicken. ➡️

// Zugriff auf den Webservice ermöglichen SongCollection songCollection = new SongCollection(); songCollecion.setAuthData("username", "password"); // Alle Songs der Dire Straits abrufen SongCollection.Response response = songCollection.findSongs("Dire Straits"); if (response.code.equals("OK")) { for (Song song : response.songs) { // Empfangene Daten anzeigen … } } // Neuen Song anlegen Song song = new Song(); song.name = "Money For Nothing"; song.artist = "Dire Straits"; song.songwriters = "Mark Knopfler"; song.releaseYear = 1985; songCollection.saveNewSong(song);

Ach ja, Money For Nothing … 💰

Bei den Webservice-Klassen orientieren wir uns dabei komplett am Server und programmieren seine Klassen im Grunde genommen nach. Nur, dass wir sie ein wenig vereinfachen und dass wir natürlich an den entscheidenden Stellen HTTP-Anfragen an den Server schicken. 🔌

/** * Datentransferklasse für einen Song */ public class Song { public final long id = 0L; public String name = ""; public String artist = ""; public String songwriters = ""; public int releaseYear = 0; } /** * Webservice-Stub für die Song Collection */ public class SongCollection { public static final URL = "https://localhost:8080/REST_Server_Beispiel/api/Songs/"; public String url = URL; public String username = ""; public String password = ""; // Konstruktoren public SongCollection() { } public SongCollection(String url) { this.url = url; } // Benutzername und Passwort setzen public setAuthData(String username, String password) { this.username = username; this.password = password; } // Hilfsklasse für die Antwortdaten des Webservices public class Response { public Song song; public List<Song> songs; public String status; public String exception; public String message; } // Methoden des Webservices public Response findSongs(String query) { // Anfrage an den Server senden HttpResponse<String> httpResponse = Unirest.get(this.url) .queryString("query", query) .header("accept", "application/json") .asString(); // Antwort als Response-Objekt zurückgeben return this.gson.fromJson(httpResponse.getBody(), Response.class); } public Response saveNewSong(Song song) { // Anfrage an den Server senden HttpResponse<String> httpResponse = Unirest.post(this.url) .header("accept", "application/json") .header("content-type", "application/json") .basicAuth(this.username, this.password) .body(this.gson.toJson(song)) .asString(); // Antwort als Response-Objekt zurückgeben return this.gson.fromJson(httpResponse.getBody(), Response.class); } } /** * Webservice-Stub für die Song Ressource */ public class SongResource { public static final URL = "https://localhost:8080/REST_Server_Beispiel/api/Songs/"; public String url = URL; public String username = ""; public String password = ""; // Konstruktoren public SongResource() { } public SongResource(String url) { this.url = url; } // Benutzername und Passwort setzen public setAuthData(String username, String password) { this.username = username; this.password = password; } // Hilfsklasse für die Antwortdaten des Webservices public class Response { public Song song; public String status; public String exception; public String message; } // Methoden des Webservices public Response getSong(long id) throws UnirestException { // Anfrage an den Server senden HttpResponse<String> httpResponse = Unirest.get(this.url + id + "/") .header("accept", "application/json") .asString(); // Antwort als Response-Objekt zurückgeben return this.gson.fromJson(httpResponse.getBody(), Response.class); } public Response updateSong(Song song) throws UnirestException { // Anfrage an den Server senden HttpResponse<String> httpResponse = Unirest.post(this.url + song.id + "/") .header("accept", "application/json") .header("content-type", "application/json") .basicAuth(this.username, this.password) .body(this.gson.toJson(song)) .asString(); // Antwort als Response-Objekt zurückgeben return this.gson.fromJson(httpResponse.getBody(), Response.class); } public Response deleteSong(long id) throws UnirestException { // Anfrage an den Server senden HttpResponse<String> httpResponse = Unirest.delete(this.url + id + "/") .header("accept", "application/json") .basicAuth(this.username, this.password) .asString(); // Antwort als Response-Objekt zurückgeben return this.gson.fromJson(httpResponse.getBody(), Response.class); } }

Aufgabe 3: 𝄞 4/4 Half-Bar REST ♫

Dann wollen wir doch gleich mal Hand anlegen 🤚🏽 und den in Moodle hinterlegten Beispiel-Webservice erweitern. Und zwar, um beim Beispiel mit der Musik zu bleiben, um eine neue Eintragsart für Songtexte. Diese muss zunächst als Entity und EJB hinzugefügt und anschließend über einen REST-Webservice aufrufbar gemacht werden. 🎙️

a) Öffne das Projekt in Netbeans und erweitere es um eine neue Entity zur Verwaltung von Songtexten:

  • Id (Schlüsselfeld, automatisch hochgezählt)
  • Song (Fremdschlüssel)
  • Songtext

b) Programmiere eine dazu passende EJB auf Grundalge der EntityBean aus den JPA-Folien.

c) Erweitere den Webservice um eine Collection und eine Ressource für den Zugriff auf Songtexte.

Persistence Entity

@Entity public class Songtext { @Id @generatedValue private long id; @ManyToOne private Song song; @Lob private String songtext = ""; // Konstruktoren … // Setter und Getter … }

Enterprise Java Bean

@Stateless public class SongtextBean extends EntityBean<Songtext, Long> { SongtextBean() { super(Songtext.class); } }

Webservice-Konfiguration

@ApplicationPath("api") public class MusicalRestAPI extends Application { @Override public Set<Class<?>> getClasses() { Set<Class<?>> resources = new HashSet<>(); // Bereits zuvor enthaltene Klassen resources.add(SignUpUser.class); resources.add(CreateDemoData.class); resources.add(SongCollection.class); resources.add(SongResource.class); // Für diese Aufgabe hinzugefügte Klassen resources.add(SongtextCollection.class); resources.add(SongtextResource.class); return resources; } }

Songtext Collection

@Stateless @Path("Songtexts") public class SongtextCollection { @EJB SongtextBean songtextBean; Gson gson = new GsonBuilder().create(); public class Response { public Songtext songtext; public List<Songtext> songtexts; public String status; public String exception; public String message; } @GET @Produces(MediaType.APPLICATION_JSON) public String findSongtexts() { Response response = new Response(); try { response.songtexts = this.songtextBean.findAll(); response.status = "OK"; } catch (Exception ex) { response.status = "ERROR"; response.exception = ex.getClass().getName(); response.message = ex.getMessage(); } return this.gson.toJson(response); } @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public String saveNewSongtext(String json) { Response response = new Response(); try { Songtext songtext = this.gson.fromJson(json, Songtext.class); response.songtext = this.songtextBean.saveNew(songtext); response.status = "OK"; } catch (Exception ex) { response.status = "ERROR"; response.exception = ex.getClass().getName(); response.message = ex.getMessage(); } return this.gson.toJson(response); } }

Songtext Resource

@Stateless @Path("Songtexts/{id}") public class SongtextResource { @EJB SongtextBean songtextBean; Gson gson = new GsonBuilder().create(); public class Response { public Songtext songtext; public String status; public String exception; public String message; } @GET @Produces(MediaType.APPLICATION_JSON) public String getSongtext(@PathParam("id") long id) { Response response = new Response(); try { response.songtext = this.songtextBean.findById(id); response.status = "OK"; } catch (Exception ex) { response.status = "ERROR"; response.exception = ex.getClass().getName(); response.message = ex.getMessage(); } return this.gson.toJson(response); } @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public String updateSong(String json) { Response response = new Response(); try { Songtext songtext = this.gson.fromJson(json, Songtext.class); response.songtext = this.songtextBean.update(songtext); response.status = "OK"; } catch (Exception ex) { response.status = "ERROR"; response.exception = ex.getClass().getName(); response.message = ex.getMessage(); } return this.gson.toJson(response); } @DELETE @Produces(MediaType.APPLICATION_JSON) public String deleteSong(@PathParam("id") long id) { Response response = new Response(); try { response.songtext = this.songtextBean.findById(id); if (response.song != null) { this.songtextBean.delete(response.songtext); } response.status = "OK"; } catch (Exception ex) { response.status = "ERROR"; response.exception = ex.getClass().getName(); response.message = ex.getMessage(); } return this.gson.toJson(response); } }

Aufgabe 4: Ein kleines JAX-RS-Quiz

Aufgabe 4.1: Eigene REST-Webservices entwickeln

a) Welche Aufgaben hat die Application-Klasse vpm JAX-RS?

  1. Keine, so eine Klasse gibt es nur bei SOAP-Webservices.
  2. Sie definiert den URL-Prefix eines REST-Webservices.
  3. Die definiert die Zugriffsberechtigungen des Webservices.
  4. Sie verweist auf die eigentlichen Webservice-Klassen.

b) An welchen Stellen kann die Annotation @Path stehen?

  1. VOr einer Webservice-Klasse
  2. Vor einem Klassenattribut
  3. Vor einer Methode
  4. Vor einem Methodenparameter

c) Welche Aufgabe hat die Annotation @Consumes?

  1. Sie definiert das Datenformat der von einer Methode zurückgelieferten Daten.
  2. Sie definiert das Datenformat der an eine Methode übergebbaren Daten.
  3. Sie definiert das Datenformat einer lokalen Variable.
  4. Sie definiert ein einheitliches Datenformat für den gesamten Webservice.

d) Welche Aufgabe hat die Annotation @Produces?

  1. Sie definiert das Datenformat der von einer Methode zurückgelieferten Daten.
  2. Sie definiert das Datenformat der an eine Methode übergebbaren Daten.
  3. Sie definiert das Datenformat einer lokalen Variable.
  4. Sie definiert ein einheitliches Datenformat für den gesamten Webservice.

e) Wie kann eine Methode sowohl Nutzdaten als auch Fehlermeldungen an den Client senden?

  1. Durch Rückgabe eines JSON-Strings oder Werfen einer Exception
  2. Durch Rückgabe eines JSON-Strings oder eines Exception-Objekts
  3. Durch Verpacken der Nutzdaten und der Fehlermeldung in einem Datentransforobjekt
  4. Durch Rückgabe von null bei einem Fehler

Aufgabe 4.2: Fremde REST-Webservices aufrufen

a) Warum ist die Entwicklung eines REST-Clients aufwändiger als bei SOAP?

  1. Weil man alle Klassen von Hand ausprogrammieren muss.
  2. Weil die WADL-Beschreibung keine Beschreibung der JSON-Strukturen beinhaltet.
  3. Weil nicht alle REST-Webservices eine WSDL-Beschreibung besitzen.

b) Wofür werden Gson und Unirest for Java bei der Entwicklung benötigt?

  1. Gson wird zum Senden der HTTP-Anfrage und Unirest für die JSON-(De)Serialisierung genutzt.
  2. Gson wird zur Authentifizierung am Server und Unirest für die Autorisierung genutzt.
  3. Gson wird für die JSON-(De)Serialisierung und Unirest zum Senden der HTTP-Anfragen genutzt.
  4. Gson wird zum Importierend der WADL-Beschreibung genutzt und Unirest für WSDLs.

c) Warum müssen clientseitig eigene Datentransferklassen für den Webservice-Aufruf programmiert werden?

  1. Weil damit eine besonders einfache Umwandlung von Java nach JSON und zurück möglich ist.
  2. Weil die Entity-Klassen aufgrund ihrer Annotationen nicht per JSON an den Client übertragen werden können.
  3. Weil sich der Webservice sonst nicht aufrufen lässt, wenn er anstelle eines Strings ein Objekt erwartet.
  4. Weil nur so strukturierte Antwortdaten an den Client gesndet werden können.

d) Warum sollten alle Webservice-Aufrufe in einer eigenen Klasse gekappselt werden.

  1. Weil somit viele kleine Anfragen zu einer großen Anfrage zusammengefasst werden könnnen.
  2. Weil sich manche Webservice-Methoden sonst gar nicht ausführen lassen.
  3. Weil dadurch der Quellcode der Serveranwendung wesentlich übersichtlicher wird.
  4. Weil dadurch der Quellcode der Clientanwendung wesentlich übersichtlicher wird.

e) Mit welchem Header Field signalisiert der Client, welches Datenformat er abrufen möchte?

  1. Content-Type
  2. Accept
  3. Accept-Content
  4. Media-Type

f) Mit welchem Header Field geben Client und Server an, welches Datenformat sie übertragen werden?

  1. Content-Type
  2. Mime-Type
  3. Media-Type
  4. Data-Type

Lösung: Aufgabe 4.1: 2 + 4, 1 + 3, 2, 1, 3
Aufgabe 4.2: 1, 3, 1, 4, 2, 1

Exkurse

Bildnachweis: Pixabay: rawpixel

Absicherung der Webservice-Zugriffe

🔥 REST-Webservices lassen sich mit Java viel einfacher absichern, als SOAP-Webservices.
🔥 Wir müssen lediglich die Datei WEB-INF\web.xml und die Serverkonfiguration anpassen.

Zunächst benötigen wir eine Entity, um Benutzer in der Datenbank speichern zu können. Sie entspricht im Wesentlichen dem Beispiel, dass wir auch schon in den EJB-Aufgaben gesehen haben.

/** * Datenbankklasse für einen Benutzer. Dies ist eine Variation der User-Klasse * von jTodo. */ @Entity @Table(name = "REST_USER") public class User implements Serializable { @Id @Column(name = "USERNAME", length = 64) private String username; @Column(name = "PASSWORD_HASH", length = 64) private String passwordHash; @ElementCollection @CollectionTable( name = "REST_USER_GROUP", joinColumns = @JoinColumn(name = "USERNAME") ) @Column(name = "GROUPNAME") List<String> groups = new ArrayList<>(); // Konstruktoren … // Setter und Getter … // Methoden zum Hashen des Passworts … // Methoden für die Zuordnung zu Benutzergruppen … } /** * Spezielle EJB zum Anlegen eines Benutzers. Dies ist im Prinzip dieselbe EJB, * wie im jTodo-Beispiel. */ @Stateless public class UserBean { @PersistenceContext EntityManager em; /** * Registrieren eines neuen Benutzers. */ public void signup(String username, String password, String... groups) throws UserAlreadyExistsException { if (em.find(User.class, username) != null) { throw new UserAlreadyExistsException("Der Benutzername $B ist bereits vergeben.".replace("$B", username)); } User user = new User(username, password); for (String group : groups) { user.addToGroup(group); } em.persist(user); } /** * Fehler: Der Benutzername ist bereits vergeben. */ public class UserAlreadyExistsException extends Exception { public UserAlreadyExistsException(String message) { super(message); } } }

Die eigentliche Absicherung erfolgt dann über die Datei WEB-INF\web.xml. Hier legen wir fest, wie sich ein Anwender authentifizieren kann und welche Rollen er haben muss, um bestimmte HTTP-Verben an bestimmte URLs senden zu dürfen.

Vorsicht! Java ist hier zickig mit der Reihenfolge! 🦄

<web-app> <security-constraint> <!-- Ändernde Zugriffe auf Songs --> <web-resource-collection> <web-resource-name>Songs</web-resource-name> <url-pattern>/api/Songs/*</url-pattern> <http-method>PUT</http-method> <http-method>POST</http-method> <http-method>PATCH</http-method> <http-method>DELETE</http-method> </web-resource-collection> <!-- Benötigte Rolle --> <auth-constraint> <role-name>rest-beispiel-user</role-name> </auth-constraint> <!-- Zugriff nur via HTTPS erlauben --> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint> <!-- Definition, dass es die Rolle überhaupt gibt --> <security-role> <role-name>rest-beispiel-user</role-name> </security-role> <!-- Art der Authentifzierung --> <login-config> <auth-method>BASIC</auth-method> <realm-name>rest-beispiel</realm-name> </login-config> </web-app>

Diese Datei, die ebenfalls im Verzeichnis WEB-INF liegen muss, gehört nicht zu Java, sondern zum Glassfish. Hier wird jeder Benutzergruppe, so wie sie in der Datenbank gespeichert ist, eine Rolle zugeordnet, die der Server prüfen kann.

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd"> <glassfish-web-app error-url=""> <security-role-mapping> <role-name>rest-beispiel-user</role-name> <group-name>rest-beispiel-user</group-name> </security-role-mapping> <parameter-encoding default-charset="UTF-8" /> </glassfish-web-app>

Und damit der Server wirklich weiß, in welchen Tabellen die Benutzerdaten liegen, müssen wir wieder eine neues „Realm” in der Glassfish-Adminkonsole anlegen. Wie immer musst du hier besonders auf Tippfehler achten.

Realm-Konfiguration im Glassfish

AJAX-Aufrufe mit JavaScript

  • Das Beste beider Welten: Browser und Server beinhalten je einen Teil der Anwendungslogik.
  • Der initiale HTML-Code kann vom Server generiert werden oder in einer einfachen HTML-Datei liegen.
  • Zu einem späteren Zeitpunkt werden dann die REST-Webservices des Servers aufgerufen.
  • Die empfangenen Daten werden dabei durch geschickte DOM-Manipulation sichtbar gemacht.

Jetzt schauen wir uns das endlich mal an.

Um die Verwendung des Webservices so einfach wie möglich zu machen, solltest du für jede Webservice-Klasse auf dem Server eine ähnliche Klasse mit den gleichen Methoden in JavaScript anlegen. Innerhalb der Methoden kannst du dann fetch(…) bzw. die Fetch API zum Absetzen der HTTP-Aufrufe verwenden.

"use strict"; /** * Von der Klasse SongCollection des Servers abgeleitete Klasse, die im Prinzip * dieselben Methoden besitzt. Hier rufen wir jedoch den REST-Webservice des * Servers auf, anstelle direkt auf eine Datenbank zuzugreifen. */ class SongCollection { /** * Konstruktor. */ constructor(url) { this.url = url || "http://localhost:8080/REST_Server_Beispiel/api/Songs/"; this.username = ""; this.password = ""; } /** * Benutzername und Passwort für die Authentifizierung merken. */ setAuthData(username, password) { this.username = username; this.password = password; } /** * Songs suchen. */ async findSongs(query) { let url = this.url; if (query !== undefined) { url += "?query=" + encodeURI(query); } let response = await fetch(url, { headers: { "accept": "application/json" } }); if (response.ok) { return await response.json(); } else { return {}; } } /** * Neuen Song speichern. */ async saveNewSong(song) { let response = await fetch(this.url, { method: "POST", headers: { "accept": "application/json", "content-type": "application/json", "authorization": "Basic " + btoa(this.username + ":" + this.password) }, body: JSON.stringify(song) }); if (response.ok) { return await response.json(); } else { return {}; } } } /** * Von der Klasse SongResource des Servers abgeleitete Klasse, die im Prinzip * dieselben Methoden besitzt. Hier rufen wir jedoch den REST-Webservice des * Servers auf, anstelle direkt auf eine Datenbank zuzugreifen. */ class SongResource { /** * Konstruktor. */ constructor(url) { this.url = url || "http://localhost:8080/REST_Server_Beispiel/api/Songs/"; this.username = ""; this.password = ""; } /** * Benutzername und Passwort für die Authentifizierung merken. */ setAuthData(username, password) { this.username = username; this.password = password; } /** * Einzelnen Song auslesen. */ async getSong(id) { let response = await fetch(this.url + id + "/", { headers: { "accept": "application/json", "authorization": "Basic " + btoa(this.username + ":" + this.password) } }); if (response.ok) { return await response.json(); } else { return {}; } } /** * Aktualisieren eines Songs. */ async updateSong(song) { let response = await fetch(this.url + song.id + "/", { method: "POST", headers: { "accept": "application/json", "content-type": "application/json", "authorization": "Basic " + btoa(this.username + ":" + this.password) }, body: JSON.stringify(song) }); if (response.ok) { return await response.json(); } else { return {}; } } /** * Song löschen. */ async deleteSong(id) { let response = await fetch(this.url + id + "/", { method: "DELETE", headers: { "accept": "application/json", "authorization": "Basic " + btoa(this.username + ":" + this.password) } }); if (response.ok) { return await response.json(); } else { return {}; } } }

Ab diesem Moment ist der Aufruf des Webservices ein wahres Kinderspiel. ⚽ Durch die neuen Webservice-Klassen müssen nur noch die richtigen Methoden im richtigen Moment aufgerufen werden.

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="SongCollection.js"></script> <script src="SongResource.js"></script> … </head> <body> <!-- Platzhalter für die vorhandenen Songs --> <div id="songs"></div> <!-- Ab hier fängt es an, Spaß zu machen 🤩 --> <script> songCollection = new SongCollection(); songResource = new SongResource(); //songCollection.setAuthData("username", "password"); //songResource.setAuthData("username", "password"); // Abruf und Anzeige aller Songs, nach Laden der Seite async function reloadSongs() { let response = await songCollection.findSongs(""); if (response.status === "OK") { let songsElement = document.getElementById("songs"); songsElement.innerHTML = ""; response.songs.forEach(song => { // Empfangene Daten anzeigen let songElement = document.createElement("div"); songElement.classList.add("song"); songsElement.appendChild(songElement); songElement.innerHTML = `<b>${song.name}</b> <br/>` + `<span class="label">Künstler:</span> ${song.artist} <br/>` + `<span class="label">Songwriter:</span> ${song.songwriters} <br/>` + `<span class="label">Jahr:</span> ${song.releaseYear} <br/>`; }); } else { alert(response.message); } } window.addEventListener("load", () => reloadSongs()); </script> </body> </html>

Und so sieht die fertige Anwendung aus. In der Netzwerkanalyse sieht man schön, wie die angezeigten Daten zeitversetzt nachgeladen werden.

Screenshot der fertigen Webanwendung

Hinweise zum Schluss

Bildnachweis: Pixabay: rawpixel

Do & Don't

Definition eines Webservices

Sicherheit

Betrieb eines Webservices

Aufruf fremder Webservices

Rechtshinweise

Creative Commons Namensnennung 4.0 International

§