Inhaltsverzeichnis

Moderne Webservices werden immer mehr als REST-Webservices entworfen und es scheint fast so, als wäre SOAP vielerorts gar kein Thema mehr. An dieser Stelle nehmen wir den Herausforderer daher gründlich unter die Lupe. 🔍

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 Javaumfeld 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 irgendeinen 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 10 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, das der Server an den Client sendet.
  2. Es beschreibt das Datenformat, das der Client an den Server sendet.
  3. Es beschreibt das Datenformat, das 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, das der Server an den Client sendet.
  2. Es beschreibt das Datenformat, das der Client an den Server sendet.
  3. Es beschreibt das Datenformat, das 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 Ressourcen. 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 dem Speichern 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 Klasse oder Methode und definiert den MIME-Type der vom Webservice zurückgelieferten Daten (in der Regel JSON und/oder XML).

@Consumes

Steht vor einer Klasse oder Methode und definiert den MIME-Type der vom Webservice verarbeiteten Daten (in der Regel JSON und/oder XML).


@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 oder ein Klassenattribut, dessen Wert über einen Platzhaler in der URL ermittelt wird. Zum Beispiel: @PathParam("id") String id

@QueryParam

Steht vor einem Methodenparameter oder Klassenattribut und legt fest, dass dieses 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.

@DefaultValue

Kann im Zusammenhang mit @PathParam und @QueryParam genutzt werden, um optionale Parameter zu definieren. Sendet der Client den auf diese Weise gekennzeichneten Parameter nicht mit, wird er mit dem hier angegebenen Defaultwert versorgt. Zum Beispiel: @DefaultValue("") @QueryParam("search") String search

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(strategy = GenerationType.TABLE, 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<Song, Long> { 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(SongResource.class); return resources; } }

Bei den eigentlichen Webservice-Klassen, die jeweils für eine Collection oder Ressource des Webservices stehen, handelt es sich dieses mal ausnahmsweise nicht um die altbekannten Stateless Session Beans. Zwar kann man vor die Klasse @Stateless schreiben, wenn man will, es bringt aber gar keinen Vorteil. Im Gegenteil: Die Annotationen @PathParam und @QueryParam funktionieren dann nicht vor Klassenattributen, da eine REST-Webservice-Klasse eigentlich nur für die Dauer einer HTTP-Anfrage instantiiert wird, was durch @Stateless unterbunden wird.

Stattdessen benötigen wir jedoch die unter „Benötigte Annotationen” gezeigten Annotationen, um die Eigenschaften des Webservices zu definieren. Fangen wir mit der URL des Webservices an. Die Annotationen @Consumes und @Produces zur Definition der unterstützten Datenformate können wir ebenfalls vorziehen, wenn wir wollen:

/** * REST-Webservice zum Suchen, Speichern und Löschen von Songs. */ @Path("Songs") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public class SongResource { @EJB private SongBean songBean;

Dann kommen die Methoden, die sich eher auf die „Liste der Einträge” beziehen und deshalb keine Id in der URL besitzen. Es handelt sich dabei um GET- und POST-Anfragen:

// <editor-fold defaultstate="collapsed" desc="Zugriff auf die Collection"> /** * GET /api/Songs/List/ * Auslesen einer Liste von Musikstücken. */ @GET public List<Song> findSongs() { return this.songBean.findAll(); } /** * POST /api/Songs/ * Speichern eines neuen Songs. */ @POST public String saveNewSong(@Valid Song song) { return this.songBean.saveNew(song); } // </editor-fold>

Weiter geht es mit den Methoden, die sich auf einen vorhandenen Datensatz mit bekannter Id beziehen. Hier benötigen wir die passenden Methoden, um auf GET-, PUT- und DELETE-Anfragen zu reagieren.

// <editor-fold defaultstate="collapsed" desc="Zugriff auf einzelne Ressourcen"> /** * GET /api/Songs/{id}/ * Auslesen eines einzelnen Songs anhand seiner ID. */ @GET @Path("{id}") public Song getSong(@PathParam("id") long id) { return this.songBean.findById(id); } /** * PUT /api/Songs/{id}/ * Aktualisieren eines vorhandenen Songs. */ @PUT @Path("{id}") public Song updateSong(@PathParam("id") long id, @Valid Song song) { song.setId(id); return this.songBean.update(song); } /** * DELETE /api/Songs/{id}/ * Löschen eines vorhandenen Songs. */ @DELETE @Path("{id}") public Song deleteSong(@PathParam("id") long id) { Song song = this.songBean.findById(id); if (response.song != null) { this.songBean.delete(response.song); } return song; } // </editor-fold> }

Und fertig ist unser REST-Webservice.

Über den Umgang mit Exceptions

Damit wir auf dem Server auftretende Fehler und Exceptions sauber an den Client melden können, definieren wir erst einmal eine Klasse mit den hierfür benötigten Werten:

/** * Antwortobjekt, das bei einer Exception an den Client gesendet wird. */ public class ExceptionResponse { public String exception; public String message; // Ggf. weitere Daten, die wir an den Client senden wollen }

Dann müssen wir nur noch einen so genannten Exception Mapper definieren, den Java bei Auftreten einer Exception automatisch aufruft. Er sorgt dafür, dass anstelle der normalen Antwort des Webservices die eben definierte Antwortstruktur mit einer Fehlerbeschreibung an den Client gesendet wird.

/** * JAX-RS Exception Mapper für beliebige Exceptions. Dieser sorgt dafür, dass * bei Auftreten einer Exception dennoch eine ordentliche Antwort an den Client * gesendet wird. */ @Provider public class RestExceptionMapper implements ExceptionMapper<Throwable> { @Override public Response toResponse(Throwable ex) { ExceptionResponse result = new ExceptionResponse(); result.exception = ex.getClass().getName(); result.message = ex.getMessage(); return Response.status(Response.Status.BAD_REQUEST).entity(result).build(); } }

Die hier gezeigte Klasse reagiert auf alle Throwable-Objekte, also wirklich auf jede Art von Fehler, die Java in Form einer Exception melden kann. Wenn wir wollen, können wir noch weitere Exception Mapper für konkretere Fehlerklassen definieren. Mindestens aber sollte jeder REST-Webservice die hier gezeigten beiden Klassen besitzen.

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.5</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 SongResource songResource = new SongResource("https://…"); // URL optional songCollecion.setAuthData("username", "password"); // Alle Songs der Dire Straits abrufen Song[] songs = songResource.findSongs("Dire Straits"); if (songs != null) { 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; songResource.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 innerhalb der Methoden HTTP-Anfragen an den Server schicken. 🔌 Zunächst benötigen wir aber zwei Hilfsklassen, um vom Server zurückgelieferte Fehler richtig erkennen zu können:

/** * Antwortstruktur, die der Server bei Auftreten einer Exception an den Client sendet. */ public class ExceptionResponse { public String exception; public String message; // Ggf. weitere Daten, die der Server im Fehlerfall übermittelt } /** * Exception für vom Server zurückgelieferte Fehler. */ public class WebAppException extends Exception { private final ExceptionResponse exceptionResponse; public WebAppException(ExceptionResponse exceptionResponse) { super(exceptionResponse.message); this.exceptionResponse = exceptionResponse; } public ExceptionResponse getExceptionResponse() { return this.exceptionResponse; } }

Und natürlich eine Klasse für die vom Server empfangenen Songdaten:

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

Dann können wir die eigentlichen Webservice-Klasse bauen:

/** * Webservice-Stub für den Song-Webservice */ 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; } // // Methoden für die gesamte Collection // /** * Songs suchen. */ public Song[] findSongs(String query) throws UnirestException, WebAppException { // HTTP-Anfrage senden HttpResponse<String> httpResponse = Unirest.get(this.url) .queryString("query", query) .header("accept", "application/json") .asString(); // Exception werfen, wenn der Server einen Fehler meldet try { ExceptionResponse er = this.gson.fromJson(httpResponse.getBody(), ExceptionResponse.class); if (er.exception != null) { throw new WebAppException(er); } } catch (JsonSyntaxException ex) { // Okay, keine Fehlerdaten empfangen } // Antwortdaten zurückgeben return this.gson.fromJson(httpResponse.getBody(), Song[].class); } /** * Neuen Song speichern. */ public Song saveNewSong(Song song) throws UnirestException, WebAppException { // HTTP-Anfrage 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(); // Exception werfen, wenn der Server einen Fehler meldet try { ExceptionResponse er = this.gson.fromJson(httpResponse.getBody(), ExceptionResponse.class); if (er.exception != null) { throw new WebAppException(er); } } catch (JsonSyntaxException ex) { // Okay, keine Fehlerdaten empfangen } // Antwortdaten zurückgeben return this.gson.fromJson(httpResponse.getBody(), Song.class); } // // Methoden für einzelne Resourcen // /** * Einzelnen Song auslesen. */ public Song getSong(long id) throws UnirestException, WebAppException { // HTTP-Anfrage senden HttpResponse<String> httpResponse = Unirest.get(this.url + id + "/") .header("accept", "application/json") .asString(); // Exception werfen, wenn der Server einen Fehler meldet try { ExceptionResponse er = this.gson.fromJson(httpResponse.getBody(), ExceptionResponse.class); if (er.exception != null) { throw new WebAppException(er); } } catch (JsonSyntaxException ex) { // Okay, keine Fehlerdaten empfangen } // Antwortdaten zurückgeben return this.gson.fromJson(httpResponse.getBody(), Song.class); } /** * Aktualisieren eines Songs. */ public Song updateSong(Song song) throws UnirestException, WebAppException { // HTTP-Anfrage 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(); // Exception werfen, wenn der Server einen Fehler meldet try { ExceptionResponse er = this.gson.fromJson(httpResponse.getBody(), ExceptionResponse.class); if (er.exception != null) { throw new WebAppException(er); } } catch (JsonSyntaxException ex) { // Okay, keine Fehlerdaten empfangen } // Antwortdaten zurückgeben return this.gson.fromJson(httpResponse.getBody(), Song.class); } /** * Song löschen. */ public Song deleteSong(long id) throws UnirestException, WebAppException { // HTTP-Anfrage senden HttpResponse<String> httpResponse = Unirest.delete(this.url + id + "/") .header("accept", "application/json") .basicAuth(this.username, this.password) .asString(); // Exception werfen, wenn der Server einen Fehler meldet try { ExceptionResponse er = this.gson.fromJson(httpResponse.getBody(), ExceptionResponse.class); if (er.exception != null) { throw new WebAppException(er); } } catch (JsonSyntaxException ex) { // Okay, keine Fehlerdaten empfangen } // Antwortdaten zurückgeben return this.gson.fromJson(httpResponse.getBody(), Song.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 bei der Musik zu bleiben) um eine neue Ressourcenart 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 entsprechende Methoden für den Zugriff auf Songtexte:

  • Auslesen aller Texte
  • Speichern eines neuen Textes
  • Auslesen eines Textes anhand seiner Id
  • Aktualisierung eines vorhandenen Textes
  • Löschen eines einzelnen Textes

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(SongResource.class); // Für diese Aufgabe hinzugefügte Klasse resources.add(SongtextResource.class); return resources; } }

Neue REST-Klasse

@Path("Songtexts") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public class SongtextResource { @EJB private SongtextBean songtextBean; @GET public List<Songtext> findSongtexts() { return this.songtextBean.findAll(); } @POST public Songtext saveNewSongtext(@Valid Songtext songtext) { return this.songtextBean.saveNew(songtext); } @GET @Path("{id}") public Songtext getSongtext(@PathParam("id") long id) { return this.songtextBean.findById(id); } @PUT @Path("{id}") public Songtext updateSong(@PathParam("id") long id, @Valid Songtext songtext) { songtext.setId(id); return this.songtextBean.update(songtext); } @DELETE @Path("{id}") public Songtext deleteSong(@PathParam("id") long id) { Songtext songtext = this.songtextBean.findById(id); if (songtext != null) { this.songtextBean.delete(songtext); } return songtext; } }

Aufgabe 4: Ein kleines JAX-RS-Quiz

Aufgabe 4.1: Eigene REST-Webservices entwickeln

a) Welche Aufgaben hat die Application-Klasse von 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 Importieren 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 gesendet 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, das 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; @ElementCollectionauth-realm @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 protected 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>auth-realm <role-name>rest-beispiel-user</role-name> </security-role> <!-- Art der Authentifizierung --> <login-config> <auth-method>BASIC</auth-method> <realm-name>rest-beispiel</realm-name> </login-config> </web-app>

Der TomEE-Server benötigt eine Datei namens context.xml, die im Verzeichnis META-INF der Webanwendung angelegt werden muss (im Bereich Web Pages in Netbeans). Diese Datei beinhaltet alle TomEE-spezifischen Einstellungen der App und besitzt deshalb folgenden Inhalt:

<?xml version="1.0" encoding="UTF-8"?> <!-- Tomcat/TomEE-spezifische Konfiguration --> <!-- Vgl. https://tomcat.apache.org/tomcat-9.0-doc/config/context.html --> <Context path="/REST_Server_Beispiel" swallowOutput="true"> <!-- LockOutRealm: Sperrt den Benutzer nach zu vielen Fehlversuchen aus --> <Realm className="org.apache.catalina.realm.LockOutRealm"> <!-- Anwendungsspezifischer Auth-Mechanismus --> <Realm className = "org.apache.catalina.realm.DataSourceRealm" dataSourceName = "Default-Database-Unmanaged" userTable = "rest_server_beispiel.rest_user" userNameCol = "username" userCredCol = "password_hash" userRoleTable = "rest_server_beispiel.rest_user_group" roleNameCol = "groupname" > <CredentialHandler className = "org.apache.catalina.realm.MessageDigestCredentialHandler" algorithm = "SHA-256" /> </Realm> </Realm> </Context>

Der Wert Default-Database-Unmanaged ist dabei der Namen einer Datenbankverbindung, die in der Serverkonfiguration in der Datei conf/tomee.xml (im Server selbst) definiert sein muss. Das haben wir in der Vorlesung bereits für dich erledigt, indem wir hier folgenden Inhalt eingetragen haben:

<?xml version="1.0" encoding="UTF-8"?> <tomee> <!-- Datenbankverbindungen --> <Resource id="Default-Database-Managed" type="javax.sql.DataSource"> JdbcDriver = org.apache.derby.jdbc.ClientDriver JdbcUrl = jdbc:derby://localhost:1527/sample UserName = app Password = app JtaManaged = true </Resource> <Resource id="Default-Database-Unmanaged" type="javax.sql.DataSource"> JdbcDriver = org.apache.derby.jdbc.ClientDriver JdbcUrl = jdbc:derby://localhost:1527/sample UserName = app Password = app JtaManaged = false </Resource> </tomee>

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 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. * @param {String} url Basis-URL des REST-Webservices (optional) */ constructor(url) { this.url = url || "http://localhost:8080/REST_Songs_Server/api/Songs/"; this.username = ""; this.password = ""; } /** * Benutzername und Passwort für die Authentifizierung merken. * @param {String} username Benutzername * @param {String} password Passwort */ setAuthData(username, password) { this.username = username; this.password = password; } /** * Songs suchen. * @param {String} query Suchparameter (optional) * @returns {Promise} Gefundene Songs */ async findSongs(query) { let url = this.url; if (query !== undefined) { url += "?query=" + encodeURI(query); } let response = await fetch(url, { headers: { "accept": "application/json" } }); return await response.json(); } /** * Neuen Song speichern. * @param {Object} song Zu speichernder Song * @returns {Promise} Gespeicherter Song */ 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) }); return await response.json(); } /** * Einzelnen Song auslesen. * @param {Number} id Song-ID * @returns {Promise} Gefundener Song */ async getSong(id) { let response = await fetch(this.url + id + "/", { headers: { "accept": "application/json", "authorization": "Basic " + btoa(this.username + ":" + this.password) } }); return await response.json(); } /** * Aktualisieren eines Songs. * @param {Object} song Zu speichernder Song * @returns {Promise} Gespeicherter Song */ 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) }); return await response.json(); } /** * Song löschen. * @param {Number} id Song ID * @returns {Promise} Gelöschter Song */ async deleteSong(id) { let response = await fetch(this.url + id + "/", { method: "DELETE", headers: { "accept": "application/json", "authorization": "Basic " + btoa(this.username + ":" + this.password) } }); return await response.json(); } }

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> songResource = new SongResource(); //songResource.setAuthData("username", "password"); // Abruf und Anzeige aller Songs, nach Laden der Seite let reloadSongs = async () => { let response = await songResource.findSongs(""); if ("exception" in response) { alert(`[${response.exception}]: ${response.message}`) } else { let songsElement = document.getElementById("songs"); songsElement.innerHTML = ""; response.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/>`; }); } }; 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

§