13.2 RESTful Web-Services
Dieser Abschnitt stellt das Architekturprinzip REST vor und anschließend den Java-Standard JAX-RS. Es folgen Beispiele mit der JAX-RS-Referenzimplementierung Jersey.
13.2.1 Aus Prinzip REST
Bei RESTful Services liegt das Konzept zugrunde, dass eine Ressource über einen Web-Server verfügbar ist und eindeutig über eine URI identifiziert wird.
Da unterschieden werden muss, ob eine Ressource neu angelegt, gelesen, aktualisiert, gelöscht oder aufgelistet werden soll, werden dafür die bekannten HTTP-Methoden verwendet.
HTTP-Methode | Operation |
GET |
Listet Ressourcen auf oder holt eine konkrete Ressource. |
PUT |
Aktualisiert eine Ressource. |
DELETE |
Löscht eine Ressource oder eine Sammlung von Ressourcen. |
POST |
Semantik kann variieren, in der Regel aber geht es um das Anlegen einer neuen Ressource. |
Anders als SOAP ist REST kein entfernter Methodenaufruf, sondern eher vergleichbar mit den Kommandos INSERT, UPDATE, DELETE und SELECT in SQL.
Ursprung von REST
Die Idee, Web-Services mit dem Web-Standard HTTP aufzubauen, beschrieb Roy Thomas Fielding im Jahr 2000 in seiner Dissertation »Architectural Styles and the Design of Network-based Software Architectures«. Das Architekturprinzip nennt er Representational State Transfer (REST) – der Begriff ist neu, aber das Konzept ist alt. Aber so wie im richtigen Leben setzten sich manche Dinge erst spät durch. Das liegt auch daran, dass SOAP-basierte Web-Services immer komplizierter wurden und sich die Welt nach etwas anderem sehnte. (Daher beginnen wir im Buch auch mit REST, und dann wird SOAP folgen.)
Wie sieht eine REST URI aus?
Auf der einen Seite stehen die HTTP-Methoden GET, PUT, POST, DELETE und auf der anderen Seite die URIs, die die Ressource kennzeichnen. Ein gutes Beispiel einer REST-URL ist der Blog vom Java-Buch:
http://www.tutego.de/blog/javainsel/category/java-7/page/2/
Das Ergebnis ist ein HTML-Dokument mit ausschließlich den Beiträgen aus der Kategorie Java 7 und nur denen auf der zweiten Seite.
Fast daneben ist auch vorbei
Da auf den ersten Blick jede HTTP-Anfrage an einen Web-Server wie ein REST-Aufruf aussieht, sollten wir uns im Klaren darüber sein, was denn kein REST-Auruf ist. Eine URL wie http://www.bing.com/search?q=tutego ist erst einmal nicht der typische REST-Stil, da es keine Ressource gibt, die angesprochen wird. Da REST als leichtgewichtig und cool gilt, geben sich natürlich gerne Dienste ein bisschen RESTig. Ein Beispiel ist Flickr, ein Fotoservice von Yahoo. Das Unternehmen wirbt mit einer REST-API, aber es ist alles andere als REST und kein gutes Beispiel.[93](Roy Fielding meint dazu nur: »Flickr obviously don’t have a clue what REST means since they just use it as an alias for HTTP. Perhaps that is because the Wikipedia entry is also confused. I don’t know.« (Quelle: http://roy.gbiv.com/untangled/2008/no-rest-in-cmis)) Das Gleiche gilt auch für Twitter, das lediglich einen rudimentären REST-Ansatz hat, aber in der Öffentlichkeit als REST pur wahrgenommen wird.
13.2.2 Jersey
Für Java gibt es mit JAX-RS einen Standard zum Deklarieren von REST-basierten Web-Services. Er wurde in der JSR-311, »JAX-RS: The Java API for RESTful Web Services«, definiert. Es fehlt noch eine Implementierung, damit wir Beispiele ausprobieren können.
- Jeder Applikationsserver ab Java EE 6 enthält standardmäßig eine JAX-RS-Implementierung.
- Da wir JAX-RS ohne einen Applikationsserver ausprobieren möchten, ist eine JAX-RS-Implementierung nötig, da Java 7 die JAX-RS-API nicht implementiert. Es ist naheliegend, die JAX-RS-Referenzimplementierung Jersey (https://jersey.dev.java.net/) zu nutzen, die auch intern von Applikationsservern verwendet wird.
Mit Jersey lässt sich entweder ein Servlet-Endpunkt definieren, sodass der RESTful Web-Service in einem Servlet-Container wie Tomcat läuft, oder der zusammen ab Java SE 6 eingebaute Mini-HTTP-Server nutzen. Wir werden im Folgenden mit dem eingebauten Server arbeiten.[94](Wer den ersten Weg einschlagen möchte, sollte einen Blick auf die API-Dokumentation http://jersey.java.net/nonav/apidocs/1.8/jersey/com/sun/jersey/spi/container/servlet/package-summary.html werfen, die erklärt, welches Servlet in der web.xml eingetragen werden soll.)
Download und Jar-Dateien
Beginnen wir mit dem Download der nötigen Java-Archive. Auf der Jersey-Homepage https://jersey.dev.java.net/ ist unter dem Punkt Download kein direktes Jar-Archiv hinterlegt, sondern die Dokumentation. Sie verweist auf drei Java-Archive (im Text ohne Versionsnummern):
- jersey-bundle.jar (http://download.java.net/maven/2/com/sun/jersey/jersey-bundle/1.8/jersey-bundle-1.8.jar)
- jsr311-api.jar (http://download.java.net/maven/2/javax/ws/rs/jsr311-api/1.1/jsr311-api-1.1.jar)
- asm.jar (http://repo1.maven.org/maven2/asm/asm/3.1/asm-3.1.jar)
Die drei Java-Archive müssen in den Klassenpfad aufgenommen werden.
13.2.3 JAX-RS-Annotationen für den ersten REST-Service
JAX-RS definierte einige Annotationen, die zentrale Konfigurationen bei RESTful Web-Services vornehmen, etwa Pfadangaben, Ausgabeformate oder Parameter. In den nächsten Kapiteln werden diese Annotationen an unterschiedlichen Beispielen vorgestellt.
Die Annotationen @Path und @GET
Beginnen wir mit einem REST-Service, der einen simplen Textstring liefert.
Listing 13.1: com/tutego/insel/rest/MessageResource.java
package com.tutego.insel.rest;
import javax.ws.rs.*;
@Path( "message" )
public class MessageResource
{
@GET
@Produces( MediaType.TEXT_PLAIN )
public String message()
{
return "Yea! ";
}
}
Die ersten beiden Annotationen sind zentral:
- @Path: Die Pfadangabe, die auf den Basispfad gesetzt wird.
- @GET: Die HTTP-Methode. Hier gibt es auch die Annotationen für die anderen HTTP-Methoden POST, PUT, ...
- @Produces: Die Annotation kann zwar grundsätzlich auch entfallen, aber besser ist es, deutlich einen MIME-Typ zu setzen. Es gibt String-Konstanten für die wichtigsten MIME-Typen, wie MediaType.TEXT_XML oder MediaType.TEXT_HTML, und auch Strings wie »application/pdf« können direkt gesetzt werden.
13.2.4 Test-Server starten
Ein Java EE-Application-Server würde die Klasse aufgrund der Annotationen gleich einlesen und als REST-Service anmelden. Da wir die Klasse jedoch in der Java SE ohne Application-Server verwenden, muss ein REST-Server von Hand aufgebaut werden. Das ist aber problemlos, denn Jersey integriert sich in den seit Java 6 eingebauten HTTP-Server.
Listing 13.2: com/tutego/insel/rest/StartRestServer.java, main()
HttpServer server = HttpServerFactory.create( "http://localhost:8080/rest" );
server.start();
JOptionPane.showMessageDialog( null, "Ende" );
server.stop( 0 );
Nach dem Start des Servers scannt Jersey alle Klassen im Klassenpfad daraufhin ab, ob sie passende JAX-RS-Annotationen tragen. Wenn ja, meldet der Server die Dienste an. Das zeigt auch die Ausgabe:
Aug 11, 2011 4:16:18 PM com.sun.jersey.api.core.ClasspathResourceConfig init
Information: Scanning for root resource and provider classes in the paths:
C:\Users\Christian\programme\2_13_Web-Services\bin
C:\Users\Christian\programme\2_13_Web-Services\lib\jersey-bundle-1.8.jar
C:\Users\Christian\programme\2_13_Web-Services\lib\jsr311-api-1.1.jar
C:\Users\Christian\programme\2_13_Web-Services\lib\asm-3.1.jar
Aug 11, 2011 4:16:19 PM com.sun.jersey.api.core.ScanningResourceConfig logClasses
Information: Root resource classes found:
class com.tutego.insel.rest.MessageResource
Aug 11, 2011 4:16:19 PM com.sun.jersey.api.core.ScanningResourceConfig init
Information: No provider classes found.
Aug 11, 2011 4:16:19 PM com.sun.jersey.server.impl.application.WebApplicationImpl _initiate
Information: Initiating Jersey application, version 'Jersey: 1.8 06/24/2011 12:39 PM'
Die statische Methode HttpServerFactory.create() liefert ein Objekt vom Typ com.sun.net. httpserver.HttpServer, das Teil der internen Java 6 API ist. Die HttpServerFactory stammt aus dem Paket com.sun.jersey.api.container.httpserver und somit von Jersey.
13.2.5 REST-Services konsumieren
Bei HttpServerFactory.create() ist als Testpfad "http://localhost:8080/rest" eingetragen, und zusammen mit @Path("message") an der Klasse ergibt sich der Pfad http://localhost:8080/rest/message für die Ressource.
REST im Browser
Wird die URL http://localhost:8080/rest/message in den Web-Browser eingesetzt, führt das zur Ausgabe von »Yea!«.
Abbildung 13.1: Screenshot vom Browser mit dem Ergebnis der Anfrage
REST-Client mit Jersey
Sich im Browser das Ergebnis eines REST-Aufrufs anzuschauen ist nicht das übliche Szenario. Oft sind es JavaScript-Programme im Browser, die REST-Aufrufe starten und die konsumierten Daten auf einer Web-Seite integrieren.
Ist es ein Java-Programm, was einen REST-Dienst anzapft, so kann dies mit einer einfachen URLConnection erledigt werden, was eine Standardklasse aus dem java.net-Paket ist, um auf HTTP-Ressourcen zuzugreifen. Doch Jersey definiert eine API – allerdings auch nur proprietär – zum einfachen Zugriff. Es reicht ein Einzeiler:
System.out.println(
Client.create().resource( "http://localhost:8080/rest/message" ).get( String.class ) );
Die Konfiguration mit der Client-API wird aber oftmals noch etwas explizier geschrieben, was uns zu den folgenden beiden Zeilen führt:
Listing 13.3: com/tutego/insel/rest/MessageJerseyClient.java, main()
WebResource service = Client.create().resource( "http://localhost:8080/rest" );
System.out.println( service.path( "message" ).accept( MediaType.TEXT_PLAIN ).get( String.class ) );
Der API-Stil, den die Bibliotheksautoren hier verwenden, nennt sich Fluent-API; Methodenaufrufe werden verkettet, um alle Parameter wie URL, MIME-Typ für die Anfrage zu setzen. Das ist eine Alternative zu den bekannten Settern auf JavaBeans.
Ist die URL falsch, gibt es eine RuntimeExcection – geprüfte Ausnahmen verwendet Jersey nicht.
Exception in thread "main" com.sun.jersey.api.client.UniformInterfaceException:
GET http://localhost:8080/rest/xxxxxxxxxxx returned a response status of 404
ClientResponse
Um vorher festzustellen, ob die Ressource verfügbar ist, lässt sich statt mit get(String.class) auch ein ClientResponse-Objekt erfragen, das dann den Status und auch ein Ergebnis liefert, falls eines verfügbar ist. Denn ist das Ergebnis verfügbar, liefert hasEntity() vom ClientResponse ein true.
ClientResponse clientResponse1 = Client.create().resource(
"http://localhost:8080/rest/message" ).get( ClientResponse.class );
System.out.println( clientResponse1.getStatus() ); // 200
if ( clientResponse1.hasEntity() )
System.out.println( clientResponse1.getEntity( String.class )); // Yea!
Ist es nicht verfügbar, ist der Status-Code in der Regel 404, und hasEntity() liefert false.
ClientResponse clientResponse2 = Client.create().resource(
"http://localhost:8080/rest/xxxxx" ).get( ClientResponse.class );
System.out.println( clientResponse2.getStatus() ); // 404
if ( clientResponse2.hasEntity() )
System.out.println( clientResponse2.getEntity( String.class ));
13.2.6 Content-Hander, Marshaller und verschiedene MIME-Typen
JAX-RS erlaubt grundsätzlich alle MIME-Typen, und die Daten selbst können auf verschiedene Java-Datentypen übertragen werden. So ist es egal, ob bei Textdokumenten zum Beispiel der Rückgabetyp String oder OutputStream ist; selbst ein File-Objekt lässt sich zurückgeben. Für einen Parametertyp – Parameter werden gleich vorgestellt – gilt das Gleiche: JAX-RS ist hier recht flexibel und kann über einen InputStream oder Writer einen String entgegennehmen. (Reicht das nicht, können sogenannte Provider angemeldet werden.)
Bei XML-Dokumenten kommt hinzu, dass JAX-RS wunderbar mit JAXB zusammenspielt.
XML mit JAXB
Dazu ein Beispiel für einen Dienst hinter der URL http://localhost:8080/rest/message/serverinfo, der eine Serverinformation im XML-Format liefert. Das XML wird automatisch von JAXB generiert.
Listing 13.4: com/tutego/insel/rest/MessageResource.java, MessageResource Ausschnitt
@Path( "message" )
public class MessageResource
{
@GET
@Path( "serverinfo" )
@Produces( MediaType.TEXT_XML )
public ServerInfo serverinfo()
{
ServerInfo info = new ServerInfo();
info.server = System.getProperty( "os.name" )+" "+System.getProperty( "os.version" );
return info;
}
}
@XmlRootElement
class ServerInfo
{
public String server;
}
Die Klasse ServerInfo ist eine JAXB-annotierte Klasse. In der JAX-RS-Methode serverinfo() wird dieses ServerInfo-Objekt aufgebaut, das Attribut gesetzt und dann zurückgegen; der Rückgabetyp ist also nicht String wie vorher, sondern explizit ServerInfo. Dass der MIME-Typ XML ist, sagt @Produces(MediaType.TEXT_XML). Und noch eine Annotation nutzt das Beispiel: @Path. Lokal an der Methode bedeutet es, dass der angegebene Pfad zusätzlich zur Pfadgabe an der Klasse gilt. Also ergibt sich der komplette Pfad aus:
Basispfad + "message" + "/" + "serverinfo"
Unter http://localhost:8080/rest/message/serverinfo ist im Browser Folgendes zu sehen:
Abbildung 13.2: Ergebnis des REST-Aufrufs im Browser
JSON-Serialisierung
Ist der Client eines REST-Aufrufs ein JavaScript-Programm in einem Web-Browser, ist es in der Regel günstiger, statt XML das Datenformat JSON zu verwenden. Bemerkenswerterweise ist Jersey mit JAXB in der Lage, direkt JSON zu erzeugen, ohne neue Bibliotheken einbinden zu müssen. Dazu muss lediglich eine kleine Änderung bei den MIME-Typen vorgenommen werden:
@Produces( MediaType.APPLICATION_JSON )
Das reicht schon aus, und der Client empfängt ein JSON-serialisiertes Objekt.
Abbildung 13.3: Der Internet Explorer hält die Datei für XML, Firefox möchte sie als unbekanntes Format abspeichern, und Chrome zeigt das JSON-Dokument einfach als Text an.
Jersey-Client
Die JAX-RS-API bietet mit dem MIME-Typ noch eine Besonderheit, dass der Server unterschiedliche Formate liefern kann, je nachdem, was der Client verarbeiten möchte oder kann. Der Server macht das mit @Produces klar, denn dort kann eine Liste von MIME-Typen stehen. Soll der Server XML und JSON generieren können, schreiben wir:
Listing 13.5: com/tutego/insel/rest/MessageResource.java, Ausschnitt
@GET
@Path( "serverinfo" )
@Produces( { MediaType.TEXT_XML, MediaType.APPLICATION_JSON } )
public ServerInfo serverinfo()
Kommt der Client mit dem Wunsch nach XML, bekommt er XML, möchte er JSON, bekommt er JSON. Die Jersey-Client-API teilt über accept() mit, was ihr Wunschtyp ist. (Dieser Typwunsch ist eine Eigenschaft von HTTP und nennt sich Content Negotiation.)
Listing 13.6: com/tutego/insel/rest/ XmlJsonMessageJerseyClient, main()
WebResource s1 = Client.create().resource( "http://localhost:8080/rest" );
Builder sb1 = s1.path( "message" ).path( "serverinfo" )
.accept( MediaType.APPLICATION_JSON );
System.out.println( sb1.get( ServerInfo.class ).server ); // Windows Vista 6.0
WebResource s2 = Client.create().resource( "http://localhost:8080/rest" );
Builder sb2 = s2.path( "message" ).path( "serverinfo" ).accept( MediaType.TEXT_XML );
System.out.println( sb2.get( ServerInfo.class ).server ); // Windows Vista 6.0
WebResource s3 = Client.create().resource( "http://localhost:8080/rest" );
Builder sb3 = s3.path( "message" ).path( "serverinfo" ).accept( MediaType.TEXT_PLAIN );
System.out.println( sb3.get( ServerInfo.class ).server ); // UniformInterfaceException
Passt die Anfrage auf den Typ von @Produces, ist alles prima, ohne Übereinstimmung gibt es einen Fehler. Bei der letzten Zeile gibt es eine Ausnahme, da JSON und XML eben nicht Text sind.
13.2.7 REST-Parameter
Im Abschnitt über REST-URLs wurde ein Beispiel vorgestellt, wie Pfadangaben aussehen, wenn sie einen RESTful Service bilden:
http://www.tutego.de/blog/javainsel/category/java-7/page/2/
Als Schlüssel-Werte-Paar lassen sich festhalten: category=java-7 und page=2. Der Server wird die URL auseinanderpflücken und genau die Blog-Einträge liefern, die zur Kategorie »java-7« gehören und sich auf der zweiten Seite befinden.
Bisher sah unser REST-Service auf dem Endpunkt /rest/message/ so aus, dass einfach ein String zurückgeben wird. Üblicherweise gibt es aber unterschiedliche URLs, die Operationen wie »finde alle« oder »finde alle mit der Einschränkung X« abbilden. Bei unseren Nachrichten wollen wir dem Client drei Varianten zur Abfrage anbieten (mit Beispiel):
- /rest/message/: alle Nachrichten aller Nutzer
- /rest/message/user/chris: alle Nachrichten von Benutzer »chris«
- /rest/message/user/chris/search/kitesurfing: alle Nachrichten von Benutzer »chris« mit dem Betreff »kitesurfing«
Das erste Beispiel macht deutlich, dass hier ohne explizite Angabe weiterer Einschränkungskriterien alle Nachrichten erfragt werden sollen, während mit zunehmend längerer URL weitere Einschränkungen dazu kommen.
Parameter in JAX-RS kennzeichnen
Die JAX-RS-API erlaubt es, dass diese Parameter (wie Benutzername oder Suchstring) leicht eingefangen werden können. Für die drei möglichen URLs entstehen drei überladene Methoden:
Listing 13.7: com/tutego/insel/rest/MessageResource.java, MessageResource Ausschnitt
@GET @Produces( MediaType.TEXT_PLAIN )
public String message() ...
@GET @Produces( MediaType.TEXT_PLAIN )
@Path("user/{user}")
public String message( @PathParam("user") String user )
{
return String.format( "Benutzer = %s", user );
}
@GET
@Produces( MediaType.TEXT_PLAIN )
@Path("user/{user}/search/{search}")
public String message( @PathParam("user") String user,
@PathParam("search") String search )
{
return String.format( "Benutzer = %s, Suchstring = %s", user, search );
}
Die bekannte @Path-Annotation enthält nicht einfach nur einen statischen Pfad, sondern beliebig viele Platzhalter in geschweiften Klammern. Der Name des Platzhalters taucht in der Methode wieder auf, nämlich dann, wenn er mit @PathParam an einen Parameter gebunden wird. Jersey parst für uns die URL und füllt die Parametervariablen passend auf bzw. ruft die richtige Methode auf. Da die JAX-RS-Implementierung den Wert füllt, nennt sich das auch JAX-RS-Injizierung.
URL-Endung | Aufgerufene Methode |
message() |
|
message( String user ) |
|
message( String user, String search ) |
Die Implementierungen der Methoden würden jetzt an einen Datenservice gehen und die selektierten Datensätze zurückgeben. Das zeigt das Beispiel nicht, da dies eine andere Baustelle ist.
13.2.8 REST-Services mit Parametern über die Jersey-Client-API aufrufen
Wenn die URLs in dem Format schlüssel1/wert1/schlüssel2/wert2 aufgebaut sind, dann ist ein Aufruf einfach mit der Jersey-Client-API umzusetzen:
Listing 13.8: com/tutego/insel/rest/ParameterizedMessageJerseyClient.java, main()
WebResource s1 = Client.create().resource( "http://localhost:8080/rest" );
Builder sb1 = s1.path("message").path("user").path("chris").accept(MediaType.TEXT_PLAIN);
System.out.println( sb1.get( String.class ) );
WebResource s2 = Client.create().resource( "http://localhost:8080/rest" );
Builder sb2 = s2.path("message").path("user").path("chris")
.path("search").path("kitesurfing")
.accept( MediaType.TEXT_PLAIN);
System.out.println( sb2.get( String.class ) );
URLs dieses Formats werden durch Kaskaden von path() abgebildet.
Multiwerte
Schlüssel-Wert-Paare lassen sich auch auf anderen Wegen übermitteln statt nur auf dem Weg über schlüssel1/wert1/schlüssel2/wert2. Besonders im Web und für Formularparameter ist die Kodierung über schlüssel1=wert1&schlüssel2=wert2 üblich. Auch das kann in JAX-RS und mit der Jersey-Client-API abgebildet werden.
- Anstatt Parameter mit @PathParam zu annotieren, kommt bei Multiwerten @QueryParam zum Einsatz.
- Anstatt mit path() zu arbeiten, wird bei dem Jersey-Client mit queryParam("schlüssel", "wert") gearbeitet. Alle Parameter lassen sich auch in einem Container vom Typ MultivaluedMap sammeln und dann bei queryParam() übergeben.
13.2.9 PUT-Anforderungen und das Senden von Daten
Zum Senden von Daten an einen REST-Service ist die HTTP-PUT-Methode gedacht. Die Implementierung einer Java-Methode kann so aussehen:
Listing 13.9: com/tutego/insel/rest/MessageResource.java, MessageResource Ausschnitt
@PUT
@Path( "user/{user}" )
@Consumes( MediaType.TEXT_PLAIN )
@Produces( MediaType.TEXT_PLAIN )
public String postMessage( @PathParam("user") String user, String message )
{
return String.format( "%s sendet '%s'%n", user, message );
}
Zunächst gilt, dass statt @GET ein @PUT die Methode annotiert. Da unserer Methode etwas geschickt wird, gibt @Consumes den MIME-Typ dieser gesendeten Daten an. Diese beiden Annotationen @PUT und @Consumes sind nötig, aber da postMessage() auch etwas liefert – die Rückgabe könnte natürlich auch void sein –, ist ebenfalls @Produces(MediaType.TEXT_PLAIN) gesetzt, und ein @PathParam fängt einen Parameter mit der Benutzerkennung ein, der die Nachricht sendet.
13.2.10 PUT/POST/DELETE-Sendungen mit der Jersey-Client-API absetzen
Die Jersey-Client-Klasse WebResource.Builder bietet neben get() auch die anderen Methoden für HTTP-Methoden, also delete(), post(), option(), ... und eben auch put() zum Schreiben.
Listing 13.10: com/tutego/insel/rest/MessageResource.java, Ausschnitt
PostMessageJerseyClient
Client create = Client.create();
WebResource service = create.resource( "http://localhost:8080/rest" );
System.out.println( service.path( "message" ).path( "user" ).path( "chris" )
.type( MediaType.TEXT_PLAIN )
.put( String.class, "Hey Chris" ) );
Neben put() ist die Methode type() neu. Sie setzt den MIME-Typ der Anforderung, was in unserem Fall auch einfacher Text ist.
Ihr Kommentar
Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.