Java Blog

JAX-RS mit Jersey, Teil 2 (RESTful Web-Services)

Content-Handler, 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 so genannte 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/api/dating/serverinfo, der eine Serverinformation im XML-Format liefert. Das XML wird automatisch von JAXB generiert.

@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 eigenen JAX-RS-Methode serverinfo() wird dieses ServerInfo-Objekt aufgebaut, das Attribut gesetzt und dann zurückgegeben; 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 Pfadangabe an der Klasse gilt. Also ergibt sich der komplette Pfad aus: Basispfad + „dating“ + „/“ + „serverinfo“. Wir können http://localhost:8080/api/dating/serverinfo im Browser eingeben.

JSON-Serialisierung *

Ist der Client eines REST-Aufrufs ein JavaScript-Programm in einem Webbrowser, ist es in der Regel praktischer, statt XML das Datenformat JSON zu verwenden. JAX-RS bindet drei Möglichkeiten zum Senden von JSON:

  1. JSON-Übersetzungdes Java-Objekts über die JSON-Bibliothek („POJO based JSON binding support“). Es lassen sich so ziemlich alle Java-Objekte abbilden.
  2. JAXB-basierte JSON-Übersetz Die JSON-Bibliothek liest die JAXB-annotierten Objekte aus und führt anhand der Metadaten die Umsetzung durch. JAXB wird hier nicht nur für XML verwendet, sondern auch für JSON; natürlich ergeben nicht alle Eigenschaften einen Sinn.
  3. Automatisch ohne ein Mapping arbeitet eine Low-Level-API. Sie gibt maximale Flexibilität, aber erfordert viel Handarbeit. Seit Java EE 7 ist die API im Standard, aber nicht Teil der Java SE.

Schauen wir uns die zweite Lösung an. Jersey unterstützt von Haus aus Jackson, MOXy und Jettison als JSON-Objekt-Mapper. Um MOXy einzusetzen müssen weitere Java-Archive in den Klassenpfad aufgenommen werden. Wir können die Abhängigkeiten über Maven beschreiben, dann ist folgendes in der pom.xml aufzunehmen:

<dependency>

<groupId>org.glassfish.jersey.media</groupId>

<artifactId>jersey-media-moxy</artifactId>

<version>2.25</version>

</dependency>

Oder zu Fuß müssen die folgenden JAR-Dateien geladen und dann im Klassenpfad aufgenommen werden:

@GET

@Path( "jsonserverinfo" )

@Produces( MediaType.APPLICATION_JSON )

public ServerInfo jsonserverinfo() {

  return serverinfo();

}

Das reicht schon aus, und der Server sendet ein JSON-serialisiertes Objekt.

Alles Verhandlungssache

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:

@GET

@Path( "jsonxmlserverinfo" )

@Produces( { MediaType.TEXT_XML, MediaType.APPLICATION_JSON } )

public ServerInfo jsonxmlserverinfo() {

  return 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 request(String) bzw. request(MediaType… types) mit, was ihr Wunschtyp ist. (Dieser Typwunsch ist eine Eigenschaft von HTTP und nennt sich Content Negotiation.)

WebTarget wr1 = ClientBuilder.newClient().target( "http://localhost:8080/api" );

Builder b1 = wr1.path( "dating" ).path( "jsonxmlserverinfo" )

                .request( MediaType.APPLICATION_JSON );

System.out.println( b1.get( ServerInfo.class ).getServer() );  // Windows 10 10.0




WebTarget wr2 = ClientBuilder.newClient().target( "http://localhost:8080/api" );

Builder b2 = wr2.path( "dating" ).path( "jsonxmlserverinfo" )

                .request( MediaType.TEXT_XML );

System.out.println( b2.get( ServerInfo.class ).getServer() );  // Windows 10 10.0




WebTarget wr3 = ClientBuilder.newClient().target( "http://localhost:8080/api" );

Builder b3 = wr3.path( "dating" ).path( "jsonxmlserverinfo" )

                .request( MediaType.TEXT_PLAIN );

try {

  System.out.println( b3.get( ServerInfo.class ).getServer() );

}

catch ( Exception e ) {

  System.out.println( e );

  // javax.ws.rs.NotAcceptableException: HTTP 406 Not Acceptable

}

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 („javax.ws.rs.NotAcceptableException: HTTP 406 Not Acceptable“), da JSON und XML eben nicht purer Text sind.

REST-Parameter

Im Abschnitt „Wie sieht ein REST-URI aus?“ in Abschnitt 15.2.1 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-Wert-Paare 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 /api/dating/ so aus, dass einfach ein String zurückgegeben wird. Üblicherweise gibt es aber unterschiedliche URLs, die Operationen wie „finde alle“ oder „finde alle mit der Einschränkung X“ abbilden. Bei unseren Dating-Dienst wollen wir dem Client drei Varianten zur Abfrage anbieten (mit Beispiel):

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 dazukommen.

Parameter in JAX-RS kennzeichnen

Die JAX-RS-API erlaubt es, dass Parameter (wie eine ID oder ein Such-String) leicht eingefangen werden können. Für die drei möglichen URLs entstehen zum Beispiel drei überladene Methoden:

@GET @Produces( MediaType.TEXT_PLAIN )
 public String meet() ...
 


@GET

@Path( "gender/{gender}" )

@Produces( MediaType.TEXT_PLAIN )

public String meet( @PathParam( "gender" ) String gender ) {

  return String.format( "Geschlecht = %s", gender );

}




@GET

@Produces( MediaType.TEXT_PLAIN )

@Path( "gender/{gender}/age/{age}" )

public String meet( @PathParam( "gender" ) String gender,

                    @PathParam( "age" ) String age ) {

  return String.format( "Geschlecht = %s, Altersbereich = %s", gender, age );

}

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
/api/dating/ meet()
/api/dating/gender/nasi meet( String gender )
/api/dating/gender/nasi/age/18-28 meet( String gender, String age )

Welche URL zu welcher Methode führt

Die Implementierungen der Methoden würden jetzt an einen Daten-Service gehen und die selektierten Datensätze zurückgeben. Das zeigt das Beispiel nicht, da dies eine andere Baustelle ist.

Tipp: Wenn Parameter falsch sind, kann eine Methode eine besondere Ausnahme vom Typ javax.ws.rs.WebApplicationException (dies ist eine RuntimeException) erzeugen. Im Konstruktor von javax.ws.rs.WebApplicationException lässt sich ein Statuscode als int oder als Aufzählung vom Typ Response.Status übergeben, etwa new WebApplicationException(Response.Status. EXPECTATION_FAILED).

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 Aufbau einfach durch Kaskadierung der path(…)-Methoden umzusetzen:

System.out.println( ClientBuilder.newClient().target( "http://localhost:8080/api" )

  .path( "dating" ).path( "gender" ).path( "nasi" )

  .request().get( String.class ) ); // Geschlecht = nasi




System.out.println( ClientBuilder.newClient().target( "http://localhost:8080/api/dating" )

  .path( "gender" ).path( "nasi" ).path( "age" ).path( "18-28" )

  .request().get( String.class ) ); // Geschlecht = nasi, Altersbereich = 18-28

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:

Hinweis

Es gibt eine Reihe von Dingen, die in Methoden per Annotation übermittelt werden können, und nicht nur @PathParam und @QueryParam. Dazu kommen noch Dinge wie @HeaderParam für den HTTP-Request-Header, @CookieParam für Cookies, @Context für Informationsobjekte und weitere Objekte.

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:

@PUT

@Path( "message/{user}" )

@Consumes( MediaType.TEXT_PLAIN )

public Response postMessage( @PathParam( "user" ) String user, String message ) {

  System.out.printf( "%s sendet '%s'%n", user, message );

  return Response.noContent().build();

}

Zunächst gilt, dass statt @GET ein @PUT die Methode annotiert. @Consumes hält den MIME-Typ dieser gesendeten Daten fest. Ein zusätzlicher @PathParam fängt die Benutzerkennung ein, die dann mit der gesendeten PUT-Nachricht auf der Konsole ausgegeben wird.

Diese beiden Annotationen @PUT und @Consumes sind also nötig. Eine Rückgabe in dem Sinne hat die Methode nicht, und es ist umstritten, ob ein REST-PUT überhaupt neben dem Statuscode etwas zurückgeben soll. Daher ist die Rückgabe ein spezielles JAX-RS-Objekt vom Typ Response, das hier für 204, „No Content“, steht. Ein 204 kommt auch immer dann automatisch zurück, wenn eine Methode void als Rückgabe deklariert.

PUT/POST/DELETE-Sendungen mit der Jersey-Client-API absetzen

Ein Invocation.Builder bietet neben get(…) auch die anderen Java-Methoden für HTTP-Methoden, also delete(…), post(…), options(…), … und eben auch put(…) zum Schreiben.

Client client = ClientBuilder.newClient();

WebTarget target = client.target( "http://localhost:8080/api" );

Response response = target.path( "dating" ).path( "message" )

                          .path( "Ha" ).request().put( Entity.text("Hey Ha!") );

System.out.println( response );

Die put(…)-Methode erwartet als Argument den Typ Entity, und zum Objektaufbau gibt es diverse statische Methoden in Entity. Es ist Entity.text(„Hey Chris“) eine Abkürzung für Entity.entity(„Hey Chris“, MediaType.TEXT_PLAIN).

Versionierung einer REST-API

Die REST-Schnittstelle ist ein externer Endpunkt einer Software und unterliegt den gleichen Modifikationen wie übliche Software: Schnittstellen ändern sich, die ausgetauschten Objekte bekommen zusätzliche Felder, usw. Ändert sich die Schnittstelle, erzeugt das eine neue Version. Alte APIs sollte ein Server eine Zeit lang beibehalten, damit nicht von einem Tag auf dem anderen eine vielleicht große Anzahl von Klienten mit der älteren Schnittstelle kommunikationslos sind.

Prinzipiell gibt es drei Ansätze bei der Versionierung von REST-APIs; die ersten beiden Varianten nutzen die Möglichkeiten der URL bzw. des HTTP:

  1. Änderung der URL, dass etwa eine Versionskennung mit eingebaut wird. Zu finden ist das bei vielen großen Unternehmen, und zu erkennen etwa an der Versionsnummer bei https://api.twitter.com/1/ https://www.googleapis.com/youtube/v3/, https://api.pinterest.com/v1/, https://api.instagram.com/v1/.
  2. In einem HTTP-Header, wie Accepts. Dort lässt sich die Version in den Dateityp hineinkodieren. Normalerweise ist der Header mit einem MIME-Typ wie text/plain, text/html belegt, doch der RFC4288 sieht in Sektion 3.2 einen „Vendor Tree“ mit dem Präfix vor, sodass sich damit ein eigener Media-Typ inklusive Version formulieren lässt. Das kann so aussehen: application/vnd.tutego.app.api+json;version=2.1 oder application/vnd.tutego.app.api-v2.1+json.
  3. In den Rumpf einer Nachricht lässt sich ebenfalls eine Version einkodieren, zum Beispiel wenn JSON übermittelt wird: {„version“: „2.1“,…}. Nachteilig ist, dass URLs ohne Parameter und ohne Körper keinen Platz für eine Versionsnummer lassen.

Alle Lösungen lassen sich prinzipiell mit JAX-RS umsetzen, wobei die Lösung 1 am Einfachsten ist.