Javas alternatives Web Services API macht fast restlos glücklich

RESTful Web Services mit JAX-RS

Bernhard Löwenstein

Seit einiger Zeit steht mit RESTful Web Services eine zweite Technologie zur Realisierung von Webdiensten zur Verfügung. Genauso wie ihre SOAP-Pendants lassen sich diese Java-Komponenten auf Basis annotierter POJOs implementieren. Wie das konkret funktioniert, zeigt dieser Artikel.

Im ersten Teil dieser Serie ging es um die Entwicklung von SOAP Web Services mit JAX-WS (Java API for XML-based Web Services). Dieses Mal beschäftigen wir uns mit den speziell in der Web-2.0-Gemeinde sehr beliebten RESTful Web Services. Nach einer theoretischen Einführung in die Materie werden wir uns anhand eines Beispiels ansehen, wie der Entwickler mit JAX-RS (Java API for RESTful Web Services) solche Services erstellen kann.

RESTful Web Services

Der große Overhead bei der Abwicklung eines Methodenaufrufs führte dazu, dass neben den SOAP Web Services eine zweite, leichtgewichtigere Variante von Web Services aufkam: RESTful Web Services. Bei diesen stehen nun nicht mehr Dienste, sondern Ressourcen im Mittelpunkt des Geschehens. Jeder Service kapselt eine bestimmte Ressourcenklasse und bietet dem Nutzer über seine HTTP-Schnittstelle die Möglichkeit, den Zustand einzelner oder mehrerer Ressourcen abzufragen oder zu manipulieren. RESTful Web Services basieren auf dem von Roy Fielding geprägten Architekturstil REST [1] (Representational State Transfer), als dessen bedeutendster Vertreter sich das WWW nennen lässt. Das Web wird als eine Sammlung von Ressourcen angesehen, die untereinander verknüpft sein können. Über einen URI lässt sich jede Ressource eindeutig adressieren. Zum Transfer der Daten wird das zustandslose HTTP(S) verwendet. Die Operation, die serverseitig auf der adressierten Ressource ausgeführt wird, ist mitunter von der gewählten HTTP-Methode abhängig. Über den zurückgelieferten HTTP-Statuscode informiert der Web Service seinen Aufrufer über den Erfolg der Aktion. Gemäß Konvention kommt den HTTP-Methoden folgende Bedeutung zu:

  • GET liefert eine oder mehrere Ressourcen in einer bestimmten Repräsentation zurück
  • POST erzeugt eine Ressource
  • PUT aktualisiert eine bereits vorhandene Ressource
  • DELETE löscht eine Ressource

Jede Ressource kann verschiedene Repräsentationen aufweisen. In Verbindung mit RESTful Web Services trifft man die Formate JSON (JavaScript Object Notation) und XML häufig an. Der Nutzer kann über die Attribute Content-Type und Accept im HTTP-Header Angaben bezüglich des für die Anfrage gewählten bzw. für die Antwort akzeptierten Datenformats machen. Die Beschreibung der Web-Service-Schnittstelle erfolgt in Form der WADL (Web Application Description Language).

JAX-RS

JAX-RS [2] ermöglicht dem Java-Entwickler die Realisierung von RESTful Web Services mittels annotierter POJOs. Sie ist Teil der Java-Plattform EE 6, kann jedoch auch problemlos mit der Standardedition genutzt werden. Lediglich die fehlenden Bibliotheken müssen dann dem Klassenpfad hinzugefügt werden. Für die aktuell in der Version 1.1 vorliegende Spezifikation ist der JSR 311 verantwortlich. Jersey [3] setzt diese als Referenzimplementierung um. Die wichtigsten API-Klassen findet der Programmierer im Package javax.ws.rs.* vor.

Begleitendes Beispiel: Distributed Books

Den meisten Lesern ist wahrscheinlich das Problem bekannt, dass sich der Bücherbestand einer Softwarefirma schon recht bald auf unzählige Büros verteilt. Zur Erfassung aller Bücher und ihrer Aufbewahrungsorte wollen wir deshalb einen RESTful Web Service entwickeln. Auf das Frontend werden wir nur am Rande eingehen. Der nächste Ferienpraktikant soll schließlich auch noch etwas zu tun haben. Der Bücherdienst soll die typischen CRUD-Operationen [4] bereitstellen, da diese hinsichtlich des Formats mit XML verarbeitet werden können. Zusätzlich soll der Benutzer alle Bücher im JSON-Format abrufen sowie über einen Suchbegriff nach bestimmten Werken suchen können. In Verbindung mit den Suchergebnissen kann er zwischen den Formaten JSON und XML wählen.

[ header = Seite 2: Implementierung eines Web Service ]

Implementierung eines Web Service

Bei der Umsetzung eines RESTful Web Service reicht grundsätzlich eine einfache Klasse aus, trotzdem empfiehlt sich die Verwendung eines Interface, um dort alle Annotationen hineinzupacken. Der Entwickler hat jedenfalls alle Möglichkeiten: Er kann die Annotationen im Interface (sofern er eines verwendet) in der Klasse oder auf beide verteilt notieren. Die JAX-RS-Runtime ist dafür verantwortlich, zu einem eingehenden HTTP-Request die zugehörige Web-Service-Methode ausfindig zu machen, diese auszuführen und an den Aufrufer das Ergebnis im gewünschten Format zurückzuliefern. Als wesentliche Elemente eines solchen Requests lassen sich folgende ausmachen:

  • Request-URI (z. B. http://localhost:8080/resources/books)
  • HTTP-Methode (z. B. POST)
  • Medientyp der übermittelten Anfragedaten (z. B. application/xml)
  • Anfragedaten (z. B. <book title=“Mule in Action“ … />)
  • akzeptierte Medientypen für die zu übermittelnden Antwortdaten (z. B. application/json)

Die Aufgabe des Programmierers besteht darin, die Methoden seines RESTful Web Service mit den von JAX-RS bereitgestellten Annotationen auszuzeichnen. Beim Eingehen eines Requests kann die Runtime durch einen Vergleich der Request-Teile mit den Annotationswerten erkennen, welche Web-Service-Methode aufgerufen werden soll.

Welche Annotationen stehen nun zur Verfügung? @Path, auf das Interface (oder die Klasse) angewendet, verwandelt das POJO in einen RESTful Web Service. Hierüber erfolgt außerdem die Angabe des URI-Pfades, wobei dieser relativ zum Kontext des Web Service Deployments interpretiert wird. Die Annotation @Path lässt sich auch direkt auf Methoden anwenden. Die Interpretation des Pfades erfolgt dabei relativ zur Pfadangabe des Interface (oder der Klasse). Zu jeder Methode wird auf diese Weise ein Pfad(muster) festgelegt.

Mithilfe der Annotationen @GET, @POST, @PUT und @DELETE kann man die Web-Service-Methoden in Relation zu den HTTP-Methoden setzen.

Die Medientypen, die der Web Service oder eine seiner Methoden verarbeiten kann, lassen sich über @Consumes festlegen. @Produces stellt das Gegenstück dar und dient zur Angabe der Formate, in denen der Web Service oder eine seiner Methoden die Ergebnisdaten zurückliefern kann.

Sehen wir uns den Sachverhalt nun anhand unseres Beispiels an. Wir benötigen einerseits das Interface BookResource (Listing 1), in das alle Annotationen kommen, und andererseits die KlasseBookResourceImpl, die dieses Interface implementiert.

...
@Path("/books")
@Consumes(MediaType.APPLICATION_XML)
@Produces(MediaType.APPLICATION_XML)
public interface BookResource {
@POST
public Response create(@Context UriInfo uriInfo, Book book);

@GET
@Path("/{bookid}")
public Book read(@PathParam("bookid") long bookID);

@PUT
@Path("/{bookid}")
public void update(@PathParam("bookid") long bookID, Book book);

@DELETE
@Path("/{bookid}")
public void delete(@PathParam("bookid") long bookID) throws
BookNotFoundException;

@GET
@Path("/all")
@Produces(MediaType.APPLICATION_JSON)
public String getAll(@QueryParam("maxnumber") @DefaultValue("50")
int maxNumber);

@GET
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Path("/search")
public Collection<Book> search(@QueryParam("query") String query);
}

Durch @Path-Annotationen legt der Entwickler für jede Methode ihren zugehörigen URI fest. Annotiert er sie nicht derartig, wird automatisch der Pfad des Interface (oder der Klasse) zugeteilt. Die Methode create()wird z. B. unter …/books gemappt. Annotiert er sie mit @Path, wird dieser Wert relativ zum Pfad des Interface (oder der Klasse) interpretiert. Die Methode getAll()wird demnach z. B. unter …/books/all zur Verfügung gestellt. In jeder Pfadangabe können außerdem beliebig viele Platzhalter, so genannte Pfadparameter, angeführt werden. Diese erkennt man daran, dass sie innerhalb spitzer Klammern stehen. Via @PathParam lassen sich solche Parameter als Argumente beim Methodenaufruf injizieren. So führt z. B. der GET-Request …/books/123 intern zur Ausführung von read(123) .

Auch die im Rahmen der Request-URI übergebenen Query-Parameter lassen sich mittels @QueryParam als Methodenargumente injizieren. @DefaultValue erlaubt die Angabe eines Standardwerts für den Fall, dass der entsprechende Parameter in der Query fehlt. Der GET-Request …/books/all?maxnumber=10 würde intern z. B. den Aufruf von getAll(10) bewirken und …/books/all zur Ausführung von getAll(50) führen, weil eben 50 als Standardwert definiert wurde.

Mit @CookieParam, @FormParam, @HeaderParam und @MatrixParam stehen weitere Annotationen zum Injizieren von Methodenargumenten zur Verfügung. Darüber hinaus kann man sich mit@Context verschiedene Kontextvariablen übergeben lassen, die innerhalb der Methode wichtige Informationen bzw. Funktionalitäten bereitstellen [5]. Wir nutzen diese Möglichkeit z. B. beicreate(), um eine Kontextvariable vom Typ UriInfo zu injizieren.

In Verbindung mit @Consumes und @Produces gilt: Werden solche am Interface (oder an der Klasse) notiert, gelten sie für alle Methoden, die diesbezüglich keine eigene Regelung treffen. So können z. B. all unsere CRUD-Methoden XML konsumieren und produzieren, während getAll()die Daten ausschließlich in JSON und search(), die Suchergebnisse aber in beiden Formaten zurückliefern kann.

Zu klären ist noch, wie die Anfragedaten aus dem Request verarbeitet werden. Hierfür sind die so genannten Entity-Provider der JAX-RS-Runtime verantwortlich, die die textuell übermittelten Anfragedaten in ein passendes Java-Objekt umwandeln. Bestimmte Transformationen unterstützt [6] jede JAX-RS-Implementierung von Hause aus. Bei Bedarf lassen sich eigene Entity-Provider ergänzen. Solche basieren beim Unmarshalling auf einer mit @Provider annotierten Klasse, die das Interface MessageBodyReader implementiert. In Verbindung mit XML muss der Entwickler lediglich die Java-Klasse des zu transferierenden Objekts mit JAXB-Annotationen auszeichnen. Den Rest erledigt die Laufzeitumgebung dank JAXB (Java Architecture for XML Binding) vollkommen automatisch. Im Fall der Methode create()erwartet z. B. die Runtime, dass das Buch in den Anfragedaten in XML-Form übermittelt wird. Die textuelle Buchdarstellung wird dann intern in ein Book-Objekt umgewandelt und jenes beim Methodenaufruf injiziert. Damit alles wie beschrieben funktioniert, ist die Klasse Book mit JAXB-Annotationen zu versehen.

In Bezug auf die Rückgabewerte gibt es verschiedene Optionen. Der Programmierer kann einerseits ein Response-Objekt zurückliefern und darin den Statuscode und die zu übermittelnden Antwortdaten festlegen. Im Fall der Methode create()gehen wir z. B. folgendermaßen vor (Listing 2): Nach dem Speichern des Buches ermitteln wir mithilfe des injizierten UriInfo-Objekts den URI zum neu angelegten Buch und liefern diesen zusätzlich zum HTTP-Statuscode 201 (Created) als Antwortdaten zurück.

public Response create(UriInfo uriInfo, Book book) {
book = bookMgr.save(book);

UriBuilder builder = uriInfo.getAbsolutePathBuilder();
URI bookURI = builder.path(String.valueOf(book.getBookID())).build();

return Response.created(bookURI).build();
}

Andererseits kann man eine Methode auch als void deklarieren. In diesem Fall obliegt es der JAX-RS-Implementierung, einen passenden Statuscode an den Nutzer zurückzuliefern. Das muss jene auch machen, wenn die Methode direkt ein Objekt zurückliefert, wie das z. B. bei read()der Fall ist. JAX-RS hat dann ebenfalls dafür zu sorgen, dass das Java-Objekt in ein vom Aufrufer akzeptiertes Format transformiert wird. Hierzu greift die Applikation erneut auf die Entity-Provider zurück. In Verbindung mit XML reichen wiederum JAXB-Annotationen aus. Den Rest erledigt die JAX-RS Runtime. Im Fall der Methode read()reduziert sich der Code dadurch z. B. wie folgt:

public Book read(long bookID) {
return bookMgr.get(bookID);
}

Selbstverständlich kann eine Methode die zu liefernden Daten auch selbst in das vom Aufrufer gewünschte Format bringen und in Form einer Zeichenkette zurückliefern. Das machen wir z. B. bei der Methode getAll(), die in Listing 3 dargestellt ist. Wir bedienen uns dabei der frei verfügbaren Bibliothek JSON.simple [7]. Ganz dem objektorientierten Stil folgend, wurde der Klasse Book eine Methode toJSONObject()hinzugefügt.

public String getAll(int maxNumber) {
Collection<Book> books = bookMgr.get();

JSONArray jsonArray = new JSONArray();

for (Book book : books) {
if (jsonArray.size() < maxNumber) {
jsonArray.add(book.toJSONObject());
}
else {
break;
}
}

return jsonArray.toString();
}

Der Nachteil dieser Vorgehensweise ist, dass Applikationslogik und Datenformatierung miteinander vermischt werden. Anhand der Methode search(),die dem Aufrufer die Büchersammlung wahlweise in JSON- oder XML-Darstellung zurückliefern kann, wollen wir uns deshalb ansehen, wie sich diese Herausforderung elegant meistern lässt. Wie der folgende Codeausschnitt zeigt, nimmt die Methode selbst keinerlei Datenformatierung vor:

public Collection<Book> search(String query) {
return bookMgr.get(query);
}

Dank JAXB sind bei der Transformation der Collection<Book> in eine adäquate XML-Darstellung keine Probleme zu erwarten. Es bleibt nur zu klären, wie aus der Java-basierenden Büchersammlung die JSON-Darstellung gewonnen wird. Dazu müssen wir einen eigenen Entity-Provider implementieren. Beim Marshalling bedarf es hierzu einer mit @Provider annotierten Klasse, die das InterfaceMessageBodyWriter implementiert. Außerdem ist via @Produces anzugeben, welche Formate der Provider produzieren kann. Dessen Hauptmethoden stellen isWriteable()und writeTo()dar. Erstere wird von der JAX-RS-Implementierung aufgerufen, um zu erfahren, ob der Entity-Provider die übergebenen Daten transformieren kann. Liefert sie true, dann erfolgt die eigentliche Transformation mittels writeTo(). Die Klasse BookCollectionProvider, die die Transformation eines Collection<Book>-Objekts in eine passende JSON-Darstellung vornimmt, verdeutlicht den Sachverhalt (Listing 4).

...
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class BookCollectionProvider implements
MessageBodyWriter<Collection<Book>> {
public long getSize(Collection<Book> books, Class<?> type,
Type genericType, Annotation[] annotations, MediaType mediaType) {
return -1;
}

public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
boolean writeable = false;

if (Collection.class.isAssignableFrom(type) && genericType
instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) genericType;
Type[] typeArgs = paramType.getActualTypeArguments();

writeable = (typeArgs.length == 1 && typeArgs[0].equals(Book.class));
}

return writeable;
}

public void writeTo(Collection<Book> books, Class<?> type,
Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
throws IOException, WebApplicationException {
JSONArray jsonArray = new JSONArray();

for (Book book : books) {
jsonArray.add(book.toJSONObject());
}

entityStream.write(jsonArray.toJSONString().getBytes());
}
}

Abschließend noch ein paar Worte zu Exceptions. Jede Methode eines RESTful Web Service kann während ihrer Ausführung eine WebApplicationException oder eine davon abgeleitete Ausnahme werfen, ohne diese explizit im Methodenkopf deklarieren zu müssen. Der beim Instanziieren einer solchen Exception übergebene Statuscode wird dann als HTTP-Statuscode an den Aufrufer zurückgeliefert. Das Werfen einer davon abweichenden Ausnahme ist ebenfalls möglich, erfordert aber deren Deklaration im Methodenkopf sowie die Bereitstellung eines so genannten Exception Mappers. Es handelt sich dabei um eine mit @Provider annotierte Klasse, die das Interface ExceptionMapper implementiert. Beim Bücherdienst kann z. B. innerhalb der Methode delete()eineBookNotFoundException auftreten. Nachdem diese nach außen gereicht wird, müssen wir einen Exception Mapper bereitstellen. Wir nennen ihn BookNotFoundExceptionMapper und sorgen dafür, dass er den HTTP-Statuscode 404 (Not Found) zurückliefert (Listing 5).

@Provider
public class BookNotFoundExceptionMapper implements ExceptionMapper<BookNotFoundException> {
public Response toResponse(BookNotFoundException bnfe) {
return Response.status(Status.NOT_FOUND)
.entity(bnfe.getMessage())
.type(MediaType.TEXT_PLAIN)
.build();
}
}

[ header = Seite 3: Deployment eines Web Service ]

Deployment eines Web Service

Das Deployment eines RESTful Web Service erfolgt in Form einer so genannten JAX-RS-Applikation. Mittels einer solchen Applikation definiert der Entwickler die anwendungsspezifischen Komponenten und Metadaten. Er erstellt hierzu eine von Application abgeleitete Klasse und überschreibt in dieser die Methode getClasses(),sodass diese alle JAX-RS-spezifischen Klassen in Form eines Set-Objekts zurückliefert (Listing 6). Zu beachten ist, dass wir dort tatsächlich nur die Klassen anführen müssen. Das Interface mit den eigentlichen JAX-RS-Annotationen können wir uns ersparen.

public class BookResourceApplication extends Application {
public Set<Class<?>> getClasses() {
Set<Class<?>> classes = new HashSet<Class<?>>();

classes.add(BookResourceImpl.class);
classes.add(BookCollectionProvider.class);
classes.add(BookNotFoundExceptionMapper.class);

return classes;
}
}

Zum Starten einer solchen Applikation lässt sich der von Java SE 6 bereitgestellte HTTP-Server nutzen (Listing 7). Unser Bücherdienst wird dadurch unter http://localhost:8080/resources/… hochgezogen. Da die Standardedition keine JAX-RS-Implementierung enthält, darf nicht vergessen werden, die erforderlichen Bibliotheken dem Klassenpfad hinzuzufügen.

HttpServer server = HttpServer.create(new InetSocketAddress(8080), 25);
HttpContext context = server.createContext("/resources");

HttpHandler handler = RuntimeDelegate.getInstance().createEndpoint
(new BookResourceApplication(), HttpHandler.class);
context.setHandler(handler);

server.start();

Produktiv werden solche JAX-RS-Anwendungen jedoch meist als Webapplikationen ausgerollt. Bei Jersey muss hierzu in web.xml ein bestimmtes Servlet registriert werden und diesem der Name der Applikationsklasse als Parameter übergeben werden (Listing 8). Alle Klassen sind wie üblich in WEB-INF/classes und alle Bibliotheken in WEB-INF/lib einzuspielen.

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http: //java.sun.com/xml/ns/j2ee"
xmlns:xsi="http: //www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http: //java.sun.com/xml/ns/j2ee
http: //java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<servlet>
<servlet-name>RESTServlet</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.
ServletContainer</servlet-class>
<init-param>
<param-name>javax.ws.rs.Application</param-name>
<param-value>de.javamagazin.books.BookResourceApplication
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>RESTServlet</servlet-name>
<url-pattern>/resources/*</url-pattern>
</servlet-mapping>
</web-app>

Zum Deployment des Webarchivs bedarf es dann eines Webservers mit integriertem Jersey. GlassFish 3.1 stellt einen solchen dar. An die Schnittstellenbeschreibung eines Web Service gelangt man, indem man application.wadl unter dem Pfad des Web Service Deployments aufruft. Im Fall der Bücherverwaltung liefert http://localhost:8080/resources/application.wadl die gewünschte Information.

Nutzung eines Web Service

Nachdem sich die Funktionalitäten eines RESTful Web Service ganz einfach per HTTP nutzen lassen, kann natürlich jeder HTTP-Client zum Abfragen und Manipulieren der Ressourcen eingesetzt werden. Hardcore-Java-Fans können auch via HttpURLConnection auf einen solchen Webdienst zugreifen (Listing 9). Konkret führt die Ausführung dieser Codezeilen zum serverseitigen Aufruf der Methode create()und dadurch zum Anlegen des transferierten Buches.

URL url = new URL("http: //localhost:8080/resources/books");

HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/xml");
connection.setDoOutput(true);
connection.connect();

PrintStream out = new PrintStream(connection.getOutputStream());
out.print(Book.getBookExample().toXMLString());
out.close();

System.out.println("Status Code: " + connection.getResponseCode());
System.out.println("Location: " + connection.getHeaderField("Location"));

connection.disconnect();

Dass diese Vorgehensweise nur in Ausnahmefällen glücklich macht, ist wohl verständlich. In den meisten Fällen greift der Entwickler deshalb auf Frameworks zurück, die den Dienstzugriff auf wesentlich höherem Abstraktionslevel erlauben. Dank Ajax (Asynchronous JavaScript and XML) lassen sich RESTful Web Services mithilfe von JavaScript auch direkt aus dem Browser heraus abfragen [8]. Das wird dann aber die Aufgabe des nächsten Ferienpraktikanten sein.

Fazit und Ausblick

Nach einer kurzen theoretischen Einführung über RESTful Web Services und JAX-RS haben wir uns am Beispiel eines Bücherdienstes angesehen, wie man konkret einen solchen RESTful Web Service implementiert, ausrollt und schlussendlich nutzt. Dank des seit der Java-Version 5 eingeschlagenen Weges, bei der Komponentenentwicklung auf annotierte POJOs zu setzen, lassen sich derartige Services einfach umsetzen. Besondere Eleganz verleihen dem Ganzen die Entity-Provider und die Exception Mapper. Im dritten Teil dieser Serie werden wir uns Werkzeuge anschauen, die die Realisierung von SOAP Web Services weiter vereinfachen. Speziell in Bezug auf weiterführende Themen bieten sie ihrem Nutzer viele nützliche Features.

Geschrieben von
Bernhard Löwenstein
Bernhard Löwenstein
Bernhard Löwenstein (bernhard.loewenstein@java.at) ist als selbstständiger IT-Trainer und Consultant für javatraining.at tätig. Als Gründer und ehrenamtlicher Obmann des Instituts zur Förderung des IT-Nachwuchses führt er außerdem altersgerechte Roboterworkshops für Kinder und Jugendliche durch, um diese für IT und Technik zu begeistern.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.