RESTful Web Services mit JAX-RS 1.1 und Jersey 1.19

+ andere TechDocs
+ REST mit Spring
+ REST mit JAX-RS 2
+ SOAP mit JAX-WS
+ SOA
+


Bitte beachten Sie: Dieses Dokument behandelt die nicht mehr aktuelle Version JAX-RS 1.1 mit Jersey 1.19.
Informationen zur aktuellen Version finden Sie unter REST mit JAX-RS 2.1 mit Jersey 2.x.

Der Begriff "REST" (Representational State Transfer) wurde im Jahr 2000 von Roy Thomas Fielding geprägt und definiert allgemeine Grundlagen eines Architekturstils, der auf identifizierbaren Ressourcen, verschiedenen Repräsentationen, Hypermedia und einheitlichen Schnittstellen basiert. Dieser Architekturstil ist nicht auf HTTP beschränkt, aber das auf HTTP basierende Web ist die bekannteste Implementierung. In Konkurrenz zu "SOA" (Service Oriented Architecture) wird auch manchmal von "ROA" (Resource Oriented Architecture) gesprochen. Eine Einführung zu REST finden Sie im englischsprachigen Wikipedia.

Während REST sehr allgemeine Grundlagen definiert, steht der Begriff "RESTful Web Services" für konkretere Definitionen für auf REST basierender Kommunikation, zum Beispiel das im JSR 311 definierte "JAX-RS: The Java API for RESTful Web Services".

Java EE 6 beinhaltet JAX-RS 1.1 (JSR 311). Die Referenzimplementierung hierfür ist Jersey 1.1.5.

Java EE 7 beinhaltet JAX-RS 2.0 (JSR 339). Die Referenzimplementierung hierfür ist Jersey 2.0.

Java EE 8 beinhaltet JAX-RS 2.1 (JSR 370). Die Referenzimplementierung hierfür ist Jersey 2.26.

Diese Webseite behandelt JAX-RS 1.1, meistens mit Jersey 1.19. Die meisten Demos funktionieren auch anderen Jersey 1.x-Versionen. Falls Sie Infos zu JAX-RS 2.x und Jersey 2.x suchen, sehen Sie sich bitte an: REST mit JAX-RS 2.1 mit Jersey 2.x.

RESTful Web Services konkurrieren mit SOAP Web Services, siehe hierzu den Vergleich weiter unten.



Inhalt

  1. Architekturstile
  2. Vergleich zwischen REST und SOAP
  3. REST-konforme Verwendung von GET, PUT, POST und DELETE sowie Assoziationen zu CRUD
  4. Infos zu JAX-RS und Jersey
  5. JAX-RS-REST-HelloWorld-Programmierbeispiel, vorerst ohne Maven
    Libs, HalloWeltService.java, HalloWeltTestServer.java, HalloWeltTestClient.java, Projektstruktur, Build und Test, WADL
  6. Kommandozeilen-Client-Tools
    cURL, Wget
  7. Webbrowser-Client-Tools
    Firebug, RESTClient, Poster
  8. TCP/IP-Monitore
    Wireshark, Apache TCPMon, Eclipse TCP/IP Monitor, TcpTrace, TCP/IP-Monitor-Konfiguration bei mehreren Rechnern
  9. JAX-RS-REST-HelloWorld-Programmierbeispiel mit Maven
  10. JAX-RS mit XML-Daten per JAXB
  11. "Contract-First"-REST-Service (ausgehend von Schema-XSD-Datei)
  12. "Contract-First"-REST-Client (ausgehend von Schema-XSD-Datei)
  13. Authentifizierung, Integrationstest und Tomcat
  14. Programmierbeispiel mit @GET, @PUT, @POST, @DELETE, @PathParam, @QueryParam, @FormParam, @XmlRootElement
    Maven-Webprojekt, pom.xml, web.xml, DO, TO, CRUD-DAO und Util, RESTful-Webservice, Webseite, Test, GET, PUT, POST und DELETE mit einem Java-Client, GET, PUT, POST und DELETE mit cURL, POST mit PostToUrl
  15. Versionierung von REST-Schnittstellen
  16. Performance-Vergleich zwischen REST (mit JAX-RS) und SOAP (mit JAX-WS)
  17. HelloWorld-Programmierbeispiel eines RESTful-Webservices mit Tomcat und Eclipse, ohne Maven
    Dynamic Web Project und Web Module Settings, Jersey, web.xml, HTML, RESTful-Webservice, Java-Client für den RESTful-Webservice
  18. JUnit-Test mit "embedded GlassFish"
  19. Atom Syndication Format (Atomfeed) mit JAX-RS und Abdera
  20. Links auf weiterführende Informationen



Architekturstile

Insbesondere im SOA-Umfeld wird oft zwischen folgenden Architekturstilen unterschieden, obwohl die Grenzen fließend sind:



Vergleich zwischen REST und SOAP

Vergleiche zwischen REST und anderen Architekturstilen oder Technologien sind nicht einfach und schwer objektivierbar. Es gibt sicherlich nicht eine pauschal "bessere" Technologie, sondern es kommt auf den Einsatzfall an. Auch der folgende Vergleich ist sicherlich diskussionswürdig:


 RESTful Web ServicesSOAP Web Services
Basis-StandardRESTW3C u.a.
Java-StandardJAX-RS (JSR 311)JAX-WS (JSR 224)
Beispiel für Java-ImplementierungJerseyMetro
Architekturstil"ROA" (Resource Oriented Architecture),
ressourcenorientiert mit generischer uniformer Schnittstelle (GET, PUT, POST, DELETE)
eher "SOA" (Service Oriented Architecture),
schnittstellen-/nachrichtenorientiert
Bevorzugtes Anwendungsgebietdatenorientierte synchrone kurz laufende Servicessowohl datenorientierte als auch lang laufende prozessorientierte Services, synchron und asynchron
Serverseitiger Zustand/Statuszustandsloseher zustandslos, kann aber auch zustandsbehaftet sein
Formale syntaktische Schnittstellenbeschreibungnur zum Teil standardisiert: Nachrichtenformate können durch XML Schema Definition und WADL beschrieben werdenvollständig durch WSDL
Nachrichtenformat, RepräsentationText, HTML, XML, JSON, binär, ...XML (plus Attachments)
Nachrichtenprotokoll, AnwendungsprotokollREST, HTTPSOAP
TransportprotokollHTTPHTTP, SMTP, JMS, ...
Asynchrone Kommunikationnicht direkt (nur über Umwege simuliert, z.B. über Atom-Feeds im Atom Syndication Format)ja, per JMS und WS-Notification (WSN)
Transaktion, sichere Zustellungkeine Unterstützung, stattdessen kann GET, PUT und DELETE idempotent gestaltet werden;
POST kann eventuell durch "POST Once Exactly (POE)" abgesichert werden;
verteilte Transaktionen über mehrere Ressourcen sind theoretisch durch den Einsatz von als Transaktionsmanager agierenden Ressourcen organisierbar
per WS Reliable Messaging (WSRM) und Web Services Transactions Framework (WSTF)
Routingmöglichper WS-Addressing
Operationsabhängiger Zugriffsschutzeinfach, per Webserver oder Firewallper WS-Security (WSS)
Bookmarks/Linksjanein
Cachingeinfachschwierig
Skalierbarkeitoptimalkann schwieriger sein
Performanceguteher schlechter
Lose Kopplung, Interoperabilität, Plattformunabhängigkeit, Internetfähigkeitjaja


REST-konforme Verwendung von GET, PUT, POST und DELETE sowie Assoziationen zu CRUD

Die vier am häufigsten benutzten HTTP-Verben sind GET, PUT, POST und DELETE. Seltener verwendet werden HEAD und OPTIONS. Die folgende Tabelle zeigt übliche Assoziationen zu CRUD (Create, Read, Update, Delete) und Beispiele für REST-konforme Verwendungen:


  HTTP    CRUD  Beispiel-URL und -BedeutungIdempotenz,
Sicherheit
GETRead http://xyz.de/Artikel/Buecher
--> Liste aller Bücher;
http://xyz.de/Artikel/Buecher/4711
--> Informationen zu dem per ID ausgewählten Buch;
http://xyz.de/Artikel/Buecher?isbn=1234567890
--> Informationen zu dem per Suchkriterium ausgewählten Buch
idempotent,
ohne Seiteneffekte,
cachefähig
PUTUpdate,
Create
http://xyz.de/Artikel/Buecher/4711
--> Update (oder Create) des per ID identifizierten Artikels
idempotent
POSTCreate http://xyz.de/Artikel/Buecher
--> Neuen Artikel hinzufügen (mit neuer ID)
(dabei wird üblicherweise die automatisch vergebene ID returniert)
nicht
idempotent
DELETE Delete http://xyz.de/Artikel/Buecher/4711
--> Diesen per ID identifizierten Artikel löschen
idempotent


Infos zu JAX-RS und Jersey

Jersey ist die JAX-RS (JSR 311) Reference Implementation und wird in den folgenden Programmierbeispielen verwendet. Infos zu JAX-RS, JSR 311 und Jersey gibt es unter:



JAX-RS-REST-HelloWorld-Programmierbeispiel, vorerst ohne Maven

Das folgende Beispiel zeigt eine minimale Implementierung eines RESTful-Webservices mit JAX-RS inklusive Server und Client.

Um das Beispiel einfach zu halten, werden der REST-Client und der REST-Service im selben Modul implementiert. In realen Anwendungen sind Client und Service in getrennten Systemen.

JAX-RS ist in Java EE (Enterprise Edition) enthalten. Aber für dieses erste simple Beispiel wollen wir uns auf Java SE (Standard Edition) beschränken, wo JAX-RS nicht enthalten ist. Deshalb wird eine JAX-RS-Implementierung benötigt: Wir wählen die Referenzimplementierung Jersey. Siehe hierzu auch den Jersey User Guide.

Als besonders einfacher Webserver wird Grizzly verwendet, der als embedded Server temporär ad-hoc gestartet und nach kurzer Zeit beendet wird.

In anderen Beispielen werden benötigte Bibliotheken automatisch über Maven hinzugefügt, aber in diesem ersten Beispiel soll alles manuell ohne Maven erfolgen.

  1. Installieren Sie ein aktuelles Java SE JDK.
  2. Legen Sie ein Projektverzeichnis an (z.B. \MeinWorkspace\JaxRsHelloWorld), und darunter mehrere Verzeichnisse:

    md \MeinWorkspace\JaxRsHelloWorld

    cd \MeinWorkspace\JaxRsHelloWorld

    md bin

    md lib

    md src\minirestwebservice

    Die Projektstruktur sieht jetzt so aus:

    [\MeinWorkspace\JaxRsHelloWorld]
     |- [bin]
     |- [lib]
     '- [src]
         '- [minirestwebservice]
    
  3. Downloaden Sie das "Jersey 1.19.1 ZIP bundle" (jersey-archive-1.19.1.zip) von http://jersey.java.net (Sie können auch eine aktuellere Version verwenden, aber wenn Sie später Ihre REST-Lösung auf Java EE Application Servern wie z.B. WebLogic einsetzen wollen, beinhaltet dieser eventuell nur diese Version).

    Entzippen Sie das Jersey-Archiv in ein temporäres Verzeichnis und kopieren Sie die .jar-Libraries jersey-client-1.19.1.jar, jersey-core-1.19.1.jar, jersey-server-1.19.1.jar und jsr311-api-1.1.1.jar (oder alle Libs) aus dem jersey-archive-1.19.1/lib-Verzeichnis in das JaxRsHelloWorld/lib-Verzeichnis.

  4. Downloaden Sie für den Grizzly-Server die beiden jar-Lib-Dateien jersey-grizzly2-1.19.1.jar und grizzly-http-all-2.2.16.jar, und kopieren Sie sie in das JaxRsHelloWorld/lib-Verzeichnis.
    Bitte beachten Sie, dass die Version der grizzly-http-all-...jar zur jersey-grizzly2-...jar passen muss, siehe hierzu auch das folgende Programmierbeispiel und das "mvn dependency:tree"-Kommando.

  5. Legen Sie im src\minirestwebservice-Verzeichnis die folgenden drei Java-Dateien an.

    Dienstimplementierung: HalloWeltService.java

    package minirestwebservice;
    
    import javax.ws.rs.*;
    import javax.ws.rs.core.MediaType;
    
    @Path( "/helloworld" )
    public class HalloWeltService
    {
       @GET @Produces( MediaType.TEXT_PLAIN )
       public String halloText( @QueryParam("name") String name )
       {
          return "Hallo " + name;
       }
    
       @GET @Produces( MediaType.TEXT_HTML )
       public String halloHtml( @QueryParam("name") String name )
       {
          return "<html><title>HelloWorld</title><body><h2>Html: Hallo " + name + "</h2></body></html>";
       }
    }
    

    Wenn Sie nicht verschiedene Repräsentationen (Ausgabeformate, hier: text/plain und text/html) unterstützen wollen, genügt nur eine der beiden GET-Methoden.
    Falls Sie weitere Repräsentationen benötigen (z.B. text/xml oder application/xml), können Sie weitere Methoden hinzufügen.

    Sehen Sie sich die Bedeutung der @Path-, @GET-, @Produces- und @QueryParam-Annotationen an unter:
    http://docs.oracle.com/javaee/7/api/javax/ws/rs/package-summary.html oder http://jsr311.java.net/nonav/releases/1.1/javax/ws/rs/package-summary.html

  6. RESTful-Webservice-Server: HalloWeltTestServer.java

    package minirestwebservice;
    
    import com.sun.jersey.api.container.grizzly2.GrizzlyServerFactory;
    import org.glassfish.grizzly.http.server.HttpServer;
    
    public class HalloWeltTestServer
    {
       public static void main( String[] args ) throws Exception
       {
          String url = ( args.length > 0 ) ? args[0] : "http://localhost:4434";
          String sec = ( args.length > 1 ) ? args[1] : "10";
    
          HttpServer srv = GrizzlyServerFactory.createHttpServer( url );
    
          System.out.println( "URL: " + url );
          Thread.sleep( 1000 * Integer.parseInt( sec ) );
          srv.stop();
       }
    }
    

    Sehen Sie sich die GrizzlyServerFactory-Klasse an. Bei der hier verwendeten besonders simplen createHttpServer()-Methode werden alle Klassen im Classpath (inkl. jars) nach JAX-RS-annotierten Klassen durchsucht. Sehen Sie sich auch die anderen createHttpServer()-Methoden an, über die Sie genauer die zu verwendenden Klassen und Eigenschaften definieren können, sowie die resultierende HttpServer-Klasse.

  7. RESTful-Webservice-Client: HalloWeltTestClient.java

    package minirestwebservice;
    
    import javax.ws.rs.core.MediaType;
    import com.sun.jersey.api.client.*;
    
    public class HalloWeltTestClient
    {
       public static void main( String[] args )
       {
          String url = ( args.length > 0 ) ? args[0] : "http://localhost:4434";
          String nam = ( args.length > 1 ) ? args[1] : "ich";
          url = url + "/helloworld?name=" + nam;
          System.out.println( "URL: " + url );
    
          WebResource wrs = Client.create().resource( url );
    
          System.out.println( "\nTextausgabe:" );
          System.out.println( wrs.accept( MediaType.TEXT_PLAIN ).get( String.class ) );
          System.out.println( "\nHTML-Ausgabe:" );
          System.out.println( wrs.accept( MediaType.TEXT_HTML  ).get( String.class ) );
       }
    }
    

    Sehen Sie sich die Client- und WebResource-Klassen an. Beachten Sie, dass auch asynchrone Clients erstellt werden können.

  8. Die Projektstruktur sieht jetzt so aus:

    cd \MeinWorkspace\JaxRsHelloWorld

    tree /F

    [\MeinWorkspace\JaxRsHelloWorld]
     |- [bin]
     |- [lib]
     |   |- grizzly-http-all-2.2.16.jar
     |   |- jersey-client-1.19.1.jar
     |   |- jersey-core-1.19.1.jar
     |   |- jersey-grizzly2-1.19.1.jar
     |   |- jersey-server-1.19.1.jar
     |   '- jsr311-api-1.1.1.jar
     '- [src]
         '- [minirestwebservice]
             |- HalloWeltService.java
             |- HalloWeltTestClient.java
             '- HalloWeltTestServer.java
    
  9. Öffnen Sie ein Kommandozeilenfenster ('Windows-Taste' + 'R', 'cmd'), bauen Sie das Projekt und starten Sie den RESTful-Webservice-Server und den -Client in verschiedenen Kommandozeilenfenstern (warten Sie nach dem dritten Kommando eine Sekunde, bis der Server gestartet ist, und ersetzen Sie im anschließenden Kommando ich durch Ihren Namen):

    cd \MeinWorkspace\JaxRsHelloWorld

    javac -cp bin;lib/* -d bin src/minirestwebservice/*.java

    start java -cp bin;lib/* minirestwebservice.HalloWeltTestServer http://localhost:4434 30

    java -cp bin;lib/* minirestwebservice.HalloWeltTestClient http://localhost:4434 ich

    start http://localhost:4434/helloworld?name=ich

    start http://localhost:4434/application.wadl

    Die letzten vier Kommandos müssen kurz hintereinander (z.B. in einer Batchdatei) innerhalb der angegebenen Zeit (im Beispiel: 30 Sekunden) ausgeführt werden, weil sich anschließend der TestServer beendet.

  10. Sie erhalten im Kommandozeilenfenster:

    URL: http://localhost:4434/helloworld?name=ich
    
    Textausgabe:
    Hallo ich
    
    HTML-Ausgabe:
    <html><title>HelloWorld</title><body><h2>Html: Hallo ich</h2></body></html>
    
  11. Im ersten Webbrowser-Fenster erscheint das Ergebnis der HTML-GET-Methode:

    Html: Hallo ich

  12. Im zweiten Webbrowser-Fenster bietet MS Internet Explorer den Download der WADL-Datei an, während einige Firefox-Versionen sie direkt anzeigen, und andere Firefox-Versionen zuerst nichts anzeigen, sondern erst beim Klick mit der rechten Maustaste und anschließend auf "Seitenquelltext anzeigen":

    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <application xmlns="http://wadl.dev.java.net/2009/02">
      <doc xmlns:jersey="http://jersey.java.net/" jersey:generatedBy="Jersey: 1.19.1"/>
      <grammars/>
      <resources base="http://localhost:4434/">
        <resource path="/helloworld">
          <method id="halloText" name="GET">
            <request>
              <param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="name" style="query" type="xs:string"/>
            </request>
            <response>
              <representation mediaType="text/plain"/>
            </response>
          </method>
          <method id="halloHtml" name="GET">
            <request>
              <param xmlns:xs="http://www.w3.org/2001/XMLSchema" name="name" style="query" type="xs:string"/>
            </request>
            <response>
              <representation mediaType="text/html"/>
            </response>
          </method>
        </resource>
      </resources>
    </application>
    

    WADL (Web Application Description Language) ist ein XML-basiertes Dateiformat zur Beschreibung von Schnittstellen von HTTP-basierten Anwendungen (besonders RESTful-Webservices) in maschinenlesbarer Form (teilweise vergleichbar mit WSDL).

    Sie können leicht die Beschreibung der zwei implementierten GET-Methoden halloText() und halloHtml() mit den unterschiedlichen Mediatypen erkennen.

    Falls Sie die WADL-Datei um weitere Informationen ergänzen wollen, sehen Sie sich Adding more information to the generated WADL an.

  13. Sie können auch andere URLs beim HalloWeltTestServer- und HalloWeltTestClient-Aufruf übergeben, auch inklusive eines Web-Root-ContextPath-Anteils, beispielsweise http://localhost:4711/xyz.

  14. Falls Sie eine sehr lange Laufzeit vorgegeben haben, können Sie den HalloWeltTestServer auch einfach vorzeitig mit "Strg + C" beenden.



Kommandozeilen-Client-Tools

cURL als Kommandozeilen-Client

cURL ist ein Kommandozeilentool zum Downloaden von Dateien und vielen anderen URL- und HTTP-basierenden Aufgaben.

  1. Doku zu cURL finden Sie unter http://curl.haxx.se/docs/manual.html.
  2. Falls Sie cURL noch nicht installiert haben: Downloaden Sie für Windows zum Beispiel Win64 - Generic, Win64 ia64 zip 7.33.0 binary, ohne SSL (curl-7.33.0-win64-nossl.zip),
    entzippen Sie die Datei und kopieren Sie die resultierende curl.exe entweder in ein Verzeichnis, welches sich im PATH befindet, oder in Ihr Projektverzeichnis.
  3. Rufen Sie cURL in einem Kommandozeilenfenster auf und sehen Sie sich die installierte Version und die Liste der vielfältigen Kommandozeilenoptionen an:

    curl --version

    curl --help

  4. Starten Sie obigen HalloWeltTestServer mit langer Laufzeit (z.B. 300 Sekunden) und lesen Sie folgendermaßen die Ausgabe des HelloWorld-Beispiels:

    cd \MeinWorkspace\JaxRsHelloWorld

    start java -cp bin;lib/* minirestwebservice.HalloWeltTestServer http://localhost:4434 300

    curl "http://localhost:4434/helloworld?name=ich"

    Sie erhalten entweder die Text- oder die HTML-Antwort, also beispielsweise:

    Hallo ich
    
  5. Mit der Option "-H" können Sie Header-Informationen zum Server senden und so den Content-Type vorgeben:

    curl -H "Accept:text/plain" "http://localhost:4434/helloworld?name=ich"

    -->

    Hallo ich
    

    und

    curl -H "Accept:text/html" "http://localhost:4434/helloworld?name=ich"

    -->

    <html><title>HelloWorld</title><body><h2>Html: Hallo ich</h2></body></html>
    
  6. Mit der Option "-i" geben Sie auch die empfangenen Header-Informationen aus:

    curl -i "http://localhost:4434/helloworld?name=ich"

    Sie erhalten u.a.:

    HTTP/1.1 200 OK
    Content-Type: text/plain
    
    Hallo ich
    
  7. Lassen Sie sich die WADL-Datei ausgeben:

    curl -i "http://localhost:4434/application.wadl"

    Beachten Sie den im Header ausgegebenen Content-Type: application/vnd.sun.wadl+xml: Das "vnd" steht für "Vendor-spezifisch" (also proprietär).

  8. Wie Sie mit cURL PUT-, POST- und DELETE-Kommandos absetzen können, finden Sie weiter unten erläutert.

Wget als Kommandozeilen-Client

Ein anderes Kommandozeilentool zum Downloaden von Dateien ist Wget.

  1. Doku zu Wget finden Sie unter: GNU Wget Manual.
  2. Lesen Sie folgendermaßen die Ausgabe des obigen HelloWorld-Beispiels (durch die Option "-O -" wird keine Datei angelegt, sondern nur auf StdOut ausgegeben):

    cd \MeinWorkspace\JaxRsHelloWorld

    start java -cp bin;lib/* minirestwebservice.HalloWeltTestServer http://localhost:4434 300

    wget -O - "http://localhost:4434/helloworld?name=ich"

    Sie erhalten:

    Hallo ich
    
  3. Mit der Option "--save-headers" geben Sie auch die empfangenen Header-Informationen aus:

    wget --save-headers -O - "http://localhost:4434/helloworld?name=ich"

    Sie erhalten u.a.:

    HTTP/1.1 200 OK
    Content-Type: text/plain
    
    Hallo ich
    

Grafische Tools

Falls Sie statt der Kommandozeilentools grafische Tools bevorzugen, sehen Sie sich die Webbrowser-Client-Tools an.



Webbrowser-Client-Tools

Für den Webbrowser Mozilla Firefox stehen diverse Plug-ins zur grafischen Analyse der Webservice-Kommunikationen zur Verfügung. Drei davon werden im Folgenden exemplarisch gezeigt.

Damit können Sie auch das obige simple HelloWorld-Programmierbeispiel untersuchen. Da dabei jedoch wenig passiert, beziehen sich die im Folgenden gezeigten konkreten Screenshots auf das weiter unten gezeigte JaxRsBuecherverwaltung-Programmierbeispiel.

Firebug

Den Firefox-Webbrowser können Sie um das Firebug-Plug-in erweitern. Klicken Sie dazu unter https://addons.mozilla.org/de/firefox/addon/1843 auf die Schaltfläche "Zu Firefox hinzufügen" bzw. "Add to Firefox". Nach der Installation und dem Neustart sehen Sie unten rechts einen kleinen Käfer.

Um damit das JaxRsBuecherverwaltung-Programmierbeispiel zu analysieren, starten Sie Tomcat, öffnen Sie http://localhost:8080/JaxRsBuecherverwaltung, klicken Sie auf den Firebug-Käfer, aktivieren Sie unter 'Netzwerk' die 'Netzwerk'-Checkbox, und klicken Sie auf der Bücherverwaltungs-Webseite auf den Button 'Neues Buch anlegen'.

Klicken Sie auf die drei Tabulatorreiter 'Header', 'Post' und 'Antwort', um in etwa Folgendes angezeigt zu bekommen:

Firebug

RESTClient

Den Firefox-Webbrowser können Sie um das RESTClient-Plug-in erweitern. Klicken Sie dazu unter https://addons.mozilla.org/en-US/firefox/addon/9780/ auf die Schaltfläche "Zu Firefox hinzufügen" bzw. "Add to Firefox".

Um damit das JaxRsBuecherverwaltung-Programmierbeispiel zu analysieren, starten Sie Tomcat, wählen Sie in Firefox "Extras" | "REST Client", tragen Sie unter REST Request "http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher" und bei Request Body "isbn=9876543219&titel=RESTClient-Titel&preis=777" ein, wählen Sie als Method "POST" und klicken Sie auf den "Send"-Button.

Sie erhalten in etwa Folgendes:

RESTClient

Poster

Den Firefox-Webbrowser können Sie um das Poster-Plug-in erweitern. Klicken Sie dazu unter https://addons.mozilla.org/de/firefox/addon/2691 auf die Schaltfläche "Zu Firefox hinzufügen" bzw. "Add to Firefox". Nach der Installation und dem Neustart sehen Sie unten rechts ein gelb hinterlegtes P.

Um damit das JaxRsBuecherverwaltung-Programmierbeispiel zu analysieren, starten Sie Tomcat, klicken Sie auf das P, tragen Sie bei URL "http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher", bei Content Type "text/plain" und bei Content to Send "isbn=9876543217&titel=Poster-Titel&preis=777" ein, wählen Sie bei Actions "POST" und klicken Sie auf das "GO" neben "POST".

Sie erhalten in etwa Folgendes:

Poster Poster



TCP/IP-Monitore

Sowohl mit den oben genannten Kommandozeilen-Client-Tools (cURL und Wget) als auch mit den Webbrowser-Client-Tools (Firebug, RESTClient und Poster) lassen sich sehr gut die Antworten von REST-Services analysieren.

Wenn Sie nicht nur die REST-Antworten, sondern zusätzlich auch die von einem Client versendeten Anfragen analysieren wollen, sind so genannte HTTP-Monitore oder TCP/IP-Monitore besser geeignet, die als Proxy, Tunnel oder "Intermediate Listener" zwischen Client und Server geschaltet werden. Dies kann insbesondere interessant sein, wenn die HTTP-Header untersucht werden sollen, oder wenn nicht nur die REST-Antwort, sondern auch die REST-Anfrage komplexe Daten enthält, etwa als XML.

Außer für Wireshark ist für die anderen genannten Tools Voraussetzung, dass Sie die Möglichkeit haben, für die Dauer des Tests entweder beim Client oder beim Server eine andere Portnummer zu konfigurieren.

Wireshark

Wireshark ist eigentlich kein TCP/IP-Monitor, sondern ein universeller Netzwerk-Sniffer, also ein Programm zur Analyse von Netzwerk-Kommunikation. Wireshark wird hier nur deshalb aufgeführt, weil auch damit die REST-Kommunikation beobachtet werden kann, sogar ohne dass eine Portnummer umgestellt werden muss. Sehen Sie sich den Wireshark User’s Guide an.

Apache TCPMon

Ein schon etwas älterer aber immer noch guter TCP/IP-Monitor ist Apache TCPMon. TCPMon wird zwar nicht länger supportet, funktioniert aber trotzdem hervorragend. Hinweise zur Benutzung finden Sie im TCPMon Tutorial.

Downloaden Sie die Datei tcpmon-1.0.jar, beispielsweise von http://mirrors.ibiblio.org/maven2/ws-commons/tcpmon/1.0/. Verwenden Sie nicht das sehr ähnliche Tool tcpmon-1.1.jar von https://code.google.com/p/tcpmon/, weil dieses weniger Features hat, z.B. keine XML-Darstellung.

Wenn Sie beispielsweise beim weiter unten gezeigten Projekt "Contract-First"-REST-Service (JaxRsContractFirstService) zur Klasse ContractfirstServiceTestMitHttpMonitor die HTTP-Kommunikation analysieren wollen, starten Sie TCPMon mit folgenden Parametern:

java -cp ./tcpmon-1.0.jar org.apache.ws.commons.tcpmon.TCPMon 4435 localhost 4434

(Alternativ können Sie die drei Parameter Listen Port, Target Hostname und Target Port auch innerhalb des TCPMon-GUIs setzen.)

Klicken Sie im TCPMon-GUI oben auf den Port-4435-Tabulatorreiter, aktivieren Sie unten die Darstellung im XML-Format, und starten Sie die zu analysierende Anwendung, im Beispiel ContractfirstServiceTestMitHttpMonitor. Sie erhalten das folgende Ergebnis, welches die beiden HTTP-Header und sowohl die Anfrage-XML-Daten als auch die Antwort-XML-Daten anzeigt:

TCPMon

Eclipse TCP/IP Monitor

Eclipse (mit installierten Web Tools) beinhaltet den Eclipse TCP/IP Monitor. Hinweise zur Benutzung finden Sie im Web Tools Platform User Guide.

Aktivieren Sie den TCP/IP Monitor folgendermaßen:
Wählen Sie in Eclipse: Window | Show View | Other... | Debug | TCP/IP Monitor.
In der TCP/IP Monitor View wählen Sie oben rechts das dritte Icon von rechts, ein nach unten zeigendes Dreieck:

Eclipse-TCPIP-Monitor

Zuerst aktivieren Sie darin die Option Show Header. Anschließend wählen Sie über dasselbe Icon den Menüpunkt Properties. Im folgenden Dialog fügen Sie mit Add... einen neuen Eintrag hinzu, beispielsweise wie hier gezeigt (passen Sie die Einträge an):

  Local monitoring port: 4435
  Host name:             localhost
  Port:                  4434
  Type:                  HTTP

Betätigen Sie Start:

Eclipse-TCPIP-Monitor

Wenn Sie jetzt beispielsweise wieder aus dem weiter unten gezeigten Projekt Contract-First"-REST-Service (JaxRsContractFirstService) die Klasse ContractfirstServiceTestMitHttpMonitor ausführen, dann erhalten Sie folgendes Ergebnis, welches die beiden HTTP-Header und sowohl die Anfrage-XML-Daten als auch die Antwort-XML-Daten anzeigt:

Eclipse-TCPIP-Monitor

TcpTrace

Ein auch schon etwas älteres Tool ist TcpTrace von Simon Fell. Hinweise zur Benutzung finden Sie bei PocketSOAP.

Downloaden Sie tcpTrace081.zip von der genannten Webseite, entzippen Sie die Datei, und rufen Sie TcpTrace auf über:

tcpTrace.exe /listen 4435 /serverName localhost /serverPort 4434

Wenn Sie jetzt beispielsweise wieder aus dem weiter unten gezeigten Projekt Contract-First"-REST-Service (JaxRsContractFirstService) die Klasse ContractfirstServiceTestMitHttpMonitor ausführen, dann erhalten Sie:

TcpTrace

TCP/IP-Monitor-Konfiguration bei mehreren Rechnern

In den obigen Beispielen wurde der Einfachheit halber davon ausgegangen, dass sich der REST-Client, der TCP/IP-Monitor und der REST-Server alle drei auf demselben PC befinden (localhost). Das ist aber nicht notwendig. Es können auch drei verschiedene Rechner sein, was eher der Realität entspricht.

Angenommen der REST-Client würde den REST-Server normalerweise über die URL http://resthost:4434/meinrestservice/... ansprechen, beim REST-Client besteht die Möglichkeit, eine andere REST-URL zu konfigurieren, und der TCP/IP-Monitor läuft auf einem PC mit dem Namen monitorpc. Dann muss im TCP/IP-Monitor beispielsweise konfiguriert werden (falls die Ports 4435 und 4434 verwendet werden sollen):

  Local/Listen Port: 4435
  Target Hostname:   resthost
  Target Port:       4434

Und der REST-Client muss während des Monitorings die URL http://monitorpc:4435/meinrestservice/... verwenden.

Diese Vorgehensweise gilt unabhängig davon, welchen TCP/IP-Monitor Sie verwenden (TCPMon, Eclipse TCP/IP Monitor, TcpTrace, ...).

TCPIP-Monitor



JAX-RS-REST-HelloWorld-Programmierbeispiel mit Maven

Das folgende Programmierbeispiel erweitert obiges JAX-RS-REST-HelloWorld-Programmierbeispiel, vorerst ohne Maven um das Build-Tool Maven, so dass benötigte Libs automatisch hinzugefügt werden.

Es wird wieder ein RESTful-Webservice mit JAX-RS, Jersey und Grizzly erstellt. Auch hier sind wieder der Einfachheit halber REST-Client und REST-Service im selben Modul implementiert (weiter unten folgt ein Beispiel mit getrennten Modulen).

Sie können die Programmierbeispiele entweder als Zipdatei downloaden oder Schritt für Schritt aufbauen, wie im Folgenden beschrieben wird.

  1. Installieren Sie Maven wie beschrieben in: maven.htm#Installation.

  2. Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace), in dem sich auch das JaxRsHelloWorld-Projekt befindet, und führen Sie folgende Kommandos aus:

    cd \MeinWorkspace

    md JaxRsMitMaven

    cd JaxRsMitMaven

    md src\main\webapp\WEB-INF

    md src\main\java\minirestwebservice

    md src\test\java\minirestwebservice

    xcopy ..\JaxRsHelloWorld\src\minirestwebservice\*.java src\main\java\minirestwebservice\

    tree /F

  3. Erstellen Sie im JaxRsMitMaven-Projektverzeichnis die Maven-Projektkonfigurationsdatei: pom.xml

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>minirestwebservice</groupId>
      <artifactId>JaxRsMitMaven</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
      <name>${project.artifactId}</name>
      <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
              <source>1.7</source>
              <target>1.7</target>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-client</artifactId>
          <version>1.19.1</version>
        </dependency>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-server</artifactId>
          <version>1.19.1</version>
        </dependency>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-grizzly2</artifactId>
          <version>1.19.1</version>
        </dependency>
        <dependency>
          <!-- Version muss zu "com.sun.jersey:jersey-grizzly2" passen, siehe "mvn dependency:tree" -->
          <groupId>org.glassfish.grizzly</groupId>
          <artifactId>grizzly-http-all</artifactId>
          <version>2.2.16</version>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    </project>
    
  4. Erstellen Sie im src\main\webapp\WEB-INF-Verzeichnis die Servlet-Web-Konfiguration: web.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             id="WebApp_ID" version="2.5">
      <display-name>REST</display-name>
      <servlet>
        <display-name>JAX-RS REST Servlet</display-name>
        <servlet-name>REST-Servlet</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
      </servlet>
      <servlet-mapping>
        <servlet-name>REST-Servlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
      </servlet-mapping>
    </web-app>
    
  5. Fügen Sie im src\test\java\minirestwebservice-Testverzeichnis eine JUnit-Modultestklasse hinzu: HalloWeltServiceTest.java

    package minirestwebservice;
    
    import javax.ws.rs.core.MediaType;
    import com.sun.jersey.api.client.Client;
    import com.sun.jersey.api.client.WebResource;
    import com.sun.jersey.api.container.grizzly2.GrizzlyServerFactory;
    import org.glassfish.grizzly.http.server.HttpServer;
    import org.junit.*;
    
    public class HalloWeltServiceTest
    {
       @Test
       public void testRESTfulWebService() throws Exception
       {
          String urlSrvr = "http://localhost:4434";
          String urlRest = urlSrvr + "/helloworld?name=MeinName";
    
          // Testserver:
          HttpServer srv = GrizzlyServerFactory.createHttpServer( urlSrvr );
    
          try {
             // Testclient:
             WebResource wrs = Client.create().resource( urlRest );
             String      txt = wrs.accept( MediaType.TEXT_PLAIN ).get( String.class );
             String      htm = wrs.accept( MediaType.TEXT_HTML  ).get( String.class );
    
             // Pruefungen:
             Assert.assertEquals( "Hallo MeinName", txt );
             Assert.assertEquals( "<html><title>HelloWorld</title><body><h2>Html: Hallo MeinName</h2></body></html>", htm );
    
          } finally {
             // Testserver beenden:
             srv.stop();
          }
       }
    }
    
  6. Bitte beachten Sie, dass Sie diesmal nicht manuell Libs zum Projekt hinzukopieren müssen, weil sich darum Maven kümmert.

  7. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\JaxRsMitMaven]
     |- [src]
     |   |- [main]
     |   |   |- [java]
     |   |   |   '- [minirestwebservice]
     |   |   |       |- HalloWeltService.java
     |   |   |       |- HalloWeltTestClient.java
     |   |   |       '- HalloWeltTestServer.java
     |   |   '- [webapp]
     |   |       '- [WEB-INF]
     |   |           '- web.xml
     |   '- [test]
     |       '- [java]
     |           '- [minirestwebservice]
     |               '- HalloWeltServiceTest.java
     '- pom.xml
    
  8. Führen Sie den JUnit-Modultest aus:

    mvn test

  9. Beachten Sie, dass Jersey automatisch nach JAX-RS-annotierten Klassen sucht und meldet:
    "INFORMATION: Root resource classes found: class minirestwebservice.HalloWeltService".

  10. Beachten Sie, dass im JUnit-Modultest während der Dauer des Tests ein Grizzly-Webserver als embedded Server temporär gestartet und anschließend beendet wird.

  11. Sie können auch weiterhin den HalloWeltTestServer und den HalloWeltTestClient über die Kommandozeile betreiben:

    cd \MeinWorkspace\JaxRsMitMaven

    mvn package

    start java -cp target/JaxRsMitMaven/WEB-INF/classes;target/JaxRsMitMaven/WEB-INF/lib/* minirestwebservice.HalloWeltTestServer http://localhost:4434 30

    java -cp target/JaxRsMitMaven/WEB-INF/classes;target/JaxRsMitMaven/WEB-INF/lib/* minirestwebservice.HalloWeltTestClient http://localhost:4434 ich

    start http://localhost:4434/helloworld?name=ich

    start http://localhost:4434/application.wadl

    Die letzten vier Kommandos müssen kurz hintereinander (z.B. in einer Batchdatei) innerhalb der angegebenen Zeit (im Beispiel: 30 Sekunden) ausgeführt werden, weil sich anschließend der TestServer beendet.

  12. Auch cURL können Sie weiter verwenden (bei laufendem HalloWeltTestServer):

    curl -i -H "Accept:text/plain" "http://localhost:4434/helloworld?name=ich"

    curl -i -H "Accept:text/html" "http://localhost:4434/helloworld?name=ich"

    curl -i "http://localhost:4434/application.wadl"

  13. Die WAR-Datei im target-Verzeichnis können Sie in beliebige Java EE Webserver oder Application Server deployen (z.B. durch Kopieren ins autodeploy-Verzeichnis). Beispielsweise im Falle von WebLogic erreichen Sie das Ergebnis über:

    http://localhost:7001/JaxRsMitMaven/rest/helloworld?name=ich

    http://localhost:7001/JaxRsMitMaven/rest/application.wadl

    curl -i -H "Accept:text/plain" "http://localhost:7001/JaxRsMitMaven/rest/helloworld?name=ich"

    curl -i -H "Accept:text/html" "http://localhost:7001/JaxRsMitMaven/rest/helloworld?name=ich"

    curl -i "http://localhost:7001/JaxRsMitMaven/rest/application.wadl"

  14. Falls Sie Eclipse einsetzen wollen, bereiten Sie Eclipse vor wie unter maven.htm#Eclipse beschrieben: Führen Sie entweder das Kommando "mvn eclipse:eclipse" aus oder verwenden Sie M2Eclipse, und laden Sie das JaxRsMitMaven-Projekt in Eclipse, und führen Sie den JUnit-Modultest innerhalb von Eclipse aus.



JAX-RS mit XML-Daten per JAXB

Mit RESTful-Webservices können Java-Objekte nicht direkt, aber zum Beispiel als XML-Repräsentation übertragen werden, sowohl als Input-Argument, als auch als returniertes Ergebnis. Das Marshalling und Unmarshalling erfolgt vorzugsweise mit JAXB. Dies demonstriert das folgende Beispiel, welches der Einfachheit halber auf dem letzten aufbaut.

  1. Erstellen Sie im JaxRsMitMaven-Projekt im Verzeichnis src\main\java das neue Unterverzeichnis xmljaxb und darin folgende drei Java-Klassen.

    Input-Klasse: InputTO.java

    package xmljaxb;
    
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement
    public class InputTO
    {
       public int    i;
       public String s;
    }
    
  2. Ergebnis-Klasse: ResultTO.java

    package xmljaxb;
    
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement
    public class ResultTO
    {
       public int    i;
       public String s;
    }
    
  3. Service-Klasse: XmlJaxbService.java

    package xmljaxb;
    
    import javax.ws.rs.*;
    import javax.ws.rs.core.MediaType;
    
    @Path( "/xmljaxb" )
    public class XmlJaxbService
    {
       @POST
       @Consumes( MediaType.TEXT_XML )
       @Produces( MediaType.TEXT_XML )
       public ResultTO doXmlJaxbService( InputTO inp )
       {
          ResultTO res = new ResultTO();
          res.i = inp.i * 2;
          res.s = inp.s + " - ret";
          return res;
       }
    }
    
  4. Erstellen Sie im Testverzeichnis src\test\java das neue Unterverzeichnis xmljaxb und darin die JUnit-Modultestklasse: XmlJaxbServiceTest.java

    package xmljaxb;
    
    import com.sun.jersey.api.client.Client;
    import com.sun.jersey.api.client.WebResource;
    import com.sun.jersey.api.container.grizzly2.GrizzlyServerFactory;
    import org.glassfish.grizzly.http.server.HttpServer;
    import org.junit.*;
    
    public class XmlJaxbServiceTest
    {
       @Test
       public void testXmlJaxbService() throws Exception
       {
          String urlSrvr = "http://localhost:4434";
          String urlRest = urlSrvr + "/xmljaxb";
          String xmlUtf8 = "text/xml; charset=utf-8";
    
          // Testserver:
          HttpServer srv = GrizzlyServerFactory.createHttpServer( urlSrvr );
    
          try {
             // Testclient:
             WebResource wrs = Client.create().resource( urlRest );
    
             // Mit JAXB und mit bequemen Java-Objekten:
             InputTO inpTO = new InputTO();
             inpTO.i = 42;
             inpTO.s = "abc xyz";
             ResultTO resTO = wrs.type( xmlUtf8 ).accept( xmlUtf8 ).post( ResultTO.class, inpTO );
             Assert.assertEquals(  84,             resTO.i );
             Assert.assertEquals( "abc xyz - ret", resTO.s );
    
             // Ohne JAXB und mit XML-Strings:
             String resXml = wrs.type( xmlUtf8 ).accept( xmlUtf8 ).post(
                                  String.class, "<inputTO><i>42</i><s>abc xyz</s></inputTO>" );
             Assert.assertEquals( "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" +
                                  "<resultTO><i>84</i><s>abc xyz - ret</s></resultTO>", resXml );
    
          } finally {
             // Eventuell verzoegerte Beendigung:
             // Thread.sleep( 20000 );
             // Testserver beenden:
             srv.stop();
          }
       }
    }
    
  5. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\JaxRsMitMaven]
     |- [src]
     |   |- [main]
     |   |   |- [java]
     |   |   |   |- [minirestwebservice]
     |   |   |   |   |- HalloWeltService.java
     |   |   |   |   |- HalloWeltTestClient.java
     |   |   |   |   '- HalloWeltTestServer.java
     |   |   |   '- [xmljaxb]
     |   |   |       |- InputTO.java
     |   |   |       |- ResultTO.java
     |   |   |       '- XmlJaxbService.java
     |   |   '- [webapp]
     |   |       '- [WEB-INF]
     |   |           '- web.xml
     |   '- [test]
     |       '- [java]
     |           |- [minirestwebservice]
     |           |   '- HalloWeltServiceTest.java
     |           '- [xmljaxb]
     |               '- XmlJaxbServiceTest.java
     '- pom.xml
    
  6. Führen Sie die JUnit-Modultests aus:

    mvn test

  7. Falls Sie vor dem Stoppen des Testservers eine Warteschleife hinzufügen (z.B. Thread.sleep( 20000 );), können Sie den REST-Service auch mit cURL ansprechen:

    curl -i -H Content-type:text/xml --request POST -d "<inputTO><i>42</i><s>abc xyz</s></inputTO>" "http://localhost:4434/xmljaxb"

    Sie erhalten:

    HTTP/1.1 200 OK
    Content-Type: text/xml
    Date: Sat, 10 Apr 2010 11:14:15 GMT
    Content-Length: 105
    
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?><resultTO><i>84</i><s>abc xyz - ret</s></resultTO>
    

    Bitte beachten Sie, dass beim cURL-Kommando auf der Kommandozeile hinter "POST -d" XML übergeben wird und das Resultat ebenfals XML ist ("<?xml ... >").

    Falls Sie das Input-XML nicht wie gezeigt als Textstring übergeben wollen, sondern stattdessen aus einer XML-Datei beziehen wollen, müssen Sie beim "-d"-Parameter dem Pfad ein "@" voranstellen und das cURL-Kommando würde lauten:

    curl -i -H Content-type:text/xml --request POST -d "@meinpfad\MeineXmlDatei.xml" "http://localhost:4434/xmljaxb"

    Falls Sie das korrekte Character-Encoding mit cURL testen wollen, müssen Sie Umlaute und Sonderzeichen in Unicode übergeben, zum Beispiel so:

    curl -H Content-type:text/xml --request POST -d "<inputTO><i>0</i><s>&#xE4;&#xF6;&#xFC;&#xDF;&#xA7;&#xBD;&#xB2;&#x20AC;&#x221A;&#x2211;</s></inputTO>" "http://localhost:4434/xmljaxb" > x.xml

    Wenn Sie die resultierende x.xml in einen Webbrowser oder in einen auf UTF-8 umschaltbaren Editor laden, erhalten Sie:

    notepad x.xml

    ... <s>äöüߧ½²€√∑ - ret</s> ...
    
  8. Falls Sie nicht mit Java-Dateien beginnen wollen, sondern mit Schema-XSD-Dateien ("Contract-First"), dann können Sie die Java-Datentransferobjektklassen auch mit xjc generieren lassen: Sehen Sie sich hierzu das folgende Beispiel an.



"Contract-First"-REST-Service (ausgehend von Schema-XSD-Datei)

Bei den bisherigen Programmierbeispielen wurde ohne explizite Schnittstellendefinition direkt mit der Java-Programmierung begonnen ("Code-First").

"Contract-First" dagegen bedeutet, dass nicht mit der Programmierung begonnen wird, sondern stattdessen zuerst Schnittstellenbeschreibungen inklusive der Schema-XSD-Dateien erstellt werden. Dies ist zwar für den Java-Programmierer etwas umständlicher, aber bietet einige Vorteile:

Ähnlich wie bei SOAP-Webservices können auch für RESTFul-Webservices Java-Klassen aus Schema-XSD-Dateien generiert werden.

  1. Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:

    cd \MeinWorkspace

    md JaxRsContractFirstService

    cd JaxRsContractFirstService

    md src\main\webapp\WEB-INF

    md src\main\java\contractfirstservice

    md src\test\java\contractfirstservice

    tree /F

  2. Kopieren Sie die web.xml und pom.xml vom letzten Projekt:

    cd \MeinWorkspace\JaxRsContractFirstService

    copy ..\JaxRsMitMaven\src\main\webapp\WEB-INF\web.xml src\main\webapp\WEB-INF\web.xml

    copy ..\JaxRsMitMaven\pom.xml

    Alternativ können Sie die Dateien natürlich auch neu anlegen, siehe web.xml und pom.xml.

  3. Ersetzen Sie im neuen JaxRsContractFirstService-Projektverzeichnis in der kopierten pom.xml die Zeile
    "<artifactId>JaxRsMitMaven</artifactId>" durch "<artifactId>JaxRsContractFirstService</artifactId>".

  4. Erstellen Sie im src\main\webapp-Verzeichnis folgende Contract-First-Schema-XSD-Datei: MeinInpResSchema.xsd

    <?xml version="1.0" encoding="UTF-8"?>
    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
               xmlns="mein.ns"
               targetNamespace="mein.ns"
               elementFormDefault="qualified">
    
      <xs:element name="inputTO"  type="InputTO"  />
      <xs:element name="resultTO" type="ResultTO" />
    
      <xs:complexType name="InputTO">
        <xs:sequence>
          <xs:element name="i" type="xs:int" />
          <xs:element name="s" type="xs:string" minOccurs="0" />
        </xs:sequence>
      </xs:complexType>
    
      <xs:complexType name="ResultTO">
        <xs:sequence>
          <xs:element name="i" type="xs:int" />
          <xs:element name="s" type="xs:string" minOccurs="0" />
        </xs:sequence>
      </xs:complexType>
    
    </xs:schema>
    

    Bitte beachten Sie, dass die Schema-XSD-Datei den Namespace mein.ns enthält (der in realen Anwendungen mit Ihrer umgekehrten Domain-Adresse beginnen sollte).

  5. Generieren Sie mit dem beim Java-JDK mitgelieferten JAXB-xjc-Tool aus der Schema-XSD-Datei Java-Klassen:

    cd \MeinWorkspace\JaxRsContractFirstService

    xjc -d src/main/java -p contractfirstgenerated src/main/webapp/MeinInpResSchema.xsd

    tree /F

  6. Die Projektstruktur sieht jetzt so aus:

    [\MeinWorkspace\JaxRsContractFirstService]
     |- [src]
     |   |- [main]
     |   |   |- [java]
     |   |   |   |- [contractfirstgenerated]
     |   |   |   |   |- InputTO.java
     |   |   |   |   |- ObjectFactory.java
     |   |   |   |   |- package-info.java
     |   |   |   |   '- ResultTO.java
     |   |   |   '- [contractfirstservice]
     |   |   '- [webapp]
     |   |       |- [WEB-INF]
     |   |       |   '- web.xml
     |   |       '- MeinInpResSchema.xsd
     |   '- [test]
     |       '- [java]
     |           '- [contractfirstservice]
     '- pom.xml
    

    Sehen Sie sich die generierten Klassen im src\main\java\contractfirstgenerated-Verzeichnis an. Beachten Sie, dass package-info.java keine Java-Klasse enthält, sondern nur Package-bezogene Annotationen mit XML- und Namespace-Informationen, und beachten Sie die beiden JAXBElement-create...()-Methoden in ObjectFactory.java, mit denen Objekte erzeugt werden können, die zusätzlich zum Transferobjekt auch XML- und Namespace-Informationen enthalten.

  7. Fügen Sie im src\main\java\contractfirstgenerated-Verzeichnis in der generierten Klasse ResultTO.java vor dem Klassennamen "public class ResultTO" folgende Zeile hinzu:

    @javax.xml.bind.annotation.XmlRootElement
    
  8. Erstellen Sie im Verzeichnis src\main\java\contractfirstservice die Service-Klasse: ContractfirstService.java

    package contractfirstservice;
    
    import javax.ws.rs.*;
    import javax.ws.rs.core.MediaType;
    import contractfirstgenerated.*;
    
    @Path( "/contractfirst" )
    public class ContractfirstService
    {
       @POST
       @Consumes( MediaType.TEXT_XML )
       @Produces( MediaType.TEXT_XML )
       public ResultTO doContractfirstService( InputTO inp )
       {
          ResultTO res = new ResultTO();
          res.setI( inp.getI() * 2 );
          res.setS( inp.getS() + " - ret" );
          return res;
       }
    }
    
  9. Einen JUnit-Modultest könnten wir nahezu identisch wie oben gezeigt erstellen. Stattdessen wird diesmal der Test auf zwei Klassen aufgesplittet: Auf eine universelle wiederverwendbare REST-Server-Test-Util-Klasse (als AutoCloseable) und den eigentlichen JUnit-Modultest.
    Erstellen Sie im Testverzeichnis src\test\java\contractfirstservice die wiederverwendbare REST-Server-Test-Util-Klasse: RestServerTestUtil.java

    package contractfirstservice;
    
    import java.io.IOException;
    import com.sun.jersey.api.client.Client;
    import com.sun.jersey.api.client.WebResource;
    import com.sun.jersey.api.container.grizzly2.GrizzlyServerFactory;
    import org.glassfish.grizzly.http.server.HttpServer;
    
    public class RestServerTestUtil implements AutoCloseable
    {
       private String urlSrvr = "http://localhost:4434";
       private String urlRest = null;
       private HttpServer srv = null;
    
       // Fuer normale Modultests:
       public RestServerTestUtil( String restPath ) throws IOException
       {
          urlRest = urlSrvr + restPath;
          // Grizzly-Testserver starten:
          srv = GrizzlyServerFactory.createHttpServer( urlSrvr );
       }
    
       // Falls ein HTTP-Monitor oder TCP/IP-Monitor zwischengeschaltet werden soll:
       public RestServerTestUtil( String urlSrvr, String urlRest ) throws IOException
       {
          this.urlSrvr = urlSrvr;
          this.urlRest = urlRest;
          // Grizzly-Testserver starten:
          srv = GrizzlyServerFactory.createHttpServer( urlSrvr );
       }
    
       @Override
       public void close()
       {
          if( srv != null ) {
             // Eventuell verzoegerte Beendigung:
             // Thread.sleep( 20000 );
             // Grizzly-Testserver beenden:
             srv.stop();
             srv = null;
          }
       }
    
       public WebResource getWebResource()
       {
          return Client.create().resource( urlRest );
       }
    }
    
  10. Erstellen Sie im Testverzeichnis src\test\java\contractfirstservice die JUnit-Modultestklasse: ContractfirstServiceTest.java

    package contractfirstservice;
    
    import javax.xml.bind.JAXBElement;
    import com.sun.jersey.api.client.WebResource;
    import org.junit.*;
    import contractfirstgenerated.*;
    
    public class ContractfirstServiceTest
    {
       @Test
       public void testContractfirstService() throws Exception
       {
          final String xmlUtf8 = "text/xml; charset=utf-8";
    
          try( RestServerTestUtil restServerTestUtil = new RestServerTestUtil( "/contractfirst" ) )
          {
             WebResource wrs = restServerTestUtil.getWebResource();
    
             // Mit JAXB und mit bequemen Java-Objekten:
             InputTO inpTO = new InputTO();
             inpTO.setI( 42 );
             inpTO.setS( "abc xyz" );
             JAXBElement<InputTO> inpJaxb = (new ObjectFactory()).createInputTO( inpTO );
             ResultTO resTO = wrs.type( xmlUtf8 ).accept( xmlUtf8 ).post( ResultTO.class, inpJaxb );
             Assert.assertEquals(  84,             resTO.getI() );
             Assert.assertEquals( "abc xyz - ret", resTO.getS() );
    
             // Ohne JAXB und mit XML-Strings (Namespace beachten):
             String resXml = wrs.type( xmlUtf8 ).accept( xmlUtf8 ).post(
                                  String.class, "<inputTO xmlns='mein.ns'><i>42</i><s>abc xyz</s></inputTO>" );
             Assert.assertEquals( "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" +
                                  "<resultTO xmlns=\"mein.ns\"><i>84</i><s>abc xyz - ret</s></resultTO>", resXml );
          }
       }
    }
    
  11. Anders als bei der ResultTO-Klasse wurde der InputTO-Klasse keine @XmlRootElement-Annotation hinzugefügt. Deshalb muss das inpTO-Objekt vor der Übergabe an die WebResource.post()-Methode in ein JAXBElement umgewandelt werden. Dies erfolgt im Beispiel über "(new ObjectFactory()).createInputTO( inpTO )".
    Alternativ gibt es weitere Möglichkeiten:
    a) Die Umwandlung hätte auch erfolgen können über:
    "new JAXBElement( new QName( "mein.ns", "inputTO" ), InputTO.class, inpTO )".
    b) Die Umwandlung könnte entfallen, wenn auch der InputTO-Klasse die @XmlRootElement-Annotation hinzugefügt würde. Dann könnte der WebResource.post()-Methode statt inpJaxb direkt das inpTO-Objekt übergeben werden.

  12. Falls Sie die Exception

    javax.ws.rs.WebApplicationException: javax.xml.bind.UnmarshalException: unexpected element ... Expected elements are ...

    erhalten: Dann müssen Sie der XmlRootElement-Annotation per name-Attribut den Namen des XML-Elements übergeben, beispielsweise so: @XmlRootElement( name="mein-xml-element" ). Siehe hierzu auch: JAXB / XmlRootElement.

  13. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\JaxRsContractFirstService]
     |- [src]
     |   |- [main]
     |   |   |- [java]
     |   |   |   |- [contractfirstgenerated]
     |   |   |   |   |- InputTO.java
     |   |   |   |   |- ObjectFactory.java
     |   |   |   |   |- package-info.java
     |   |   |   |   '- ResultTO.java
     |   |   |   '- [contractfirstservice]
     |   |   |       |- ContractfirstService.java
     |   |   '- [webapp]
     |   |       |- [WEB-INF]
     |   |       |   '- web.xml
     |   |       '- MeinInpResSchema.xsd
     |   '- [test]
     |       '- [java]
     |           '- [contractfirstservice]
     |               |- ContractfirstServiceTest.java
     |               '- RestServerTestUtil.java
     '- pom.xml
    
  14. Führen Sie die JUnit-Modultests aus:

    mvn test

  15. Falls Sie in der REST-Server-Test-Util-Klasse RestServerTestUtil.java vor dem Stoppen des Testservers eine Warteschleife hinzufügen (z.B. Thread.sleep( 20000 );), können Sie den REST-Service auch mit cURL ansprechen:

    curl -i -H Content-type:text/xml --request POST -d "<inputTO xmlns='mein.ns'><i>42</i><s>abc xyz</s></inputTO>" "http://localhost:4434/contractfirst"

    Sie erhalten:

    HTTP/1.1 200 OK
    Content-Type: text/xml
    Content-Length: 121
    
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?><resultTO xmlns="mein.ns"><i>84</i><s>abc xyz - ret</s></resultTO>
    

    Bitte beachten Sie, dass Sie diesmal den korrekten Namespace xmlns='mein.ns' angeben müssen.

  16. Sie können mit mvn package eine WAR-Datei im target-Verzeichnis erzeugen und in einen Java EE Webserver oder Application Server deployen. Beispielsweise im Falle von WebLogic können Sie dann für einen Test in der ContractfirstServiceTest-Testklasse die Zeile WebResource wrs = ... ersetzen durch:
    WebResource wrs = Client.create().resource( "http://localhost:7001/JaxRsContractFirstService/rest/contractfirst" ); Oder Sie verwenden cURL:
    curl -i -H Content-type:text/xml --request POST -d "<inputTO xmlns='mein.ns'><i>42</i><s>abc xyz</s></inputTO>" "http://localhost:7001/JaxRsContractFirstService/rest/contractfirst"

    Außerdem können Sie dann die Schema-XSD-Datei per cURL oder per Webbrowser abfragen:

    curl http://localhost:7001/JaxRsContractFirstService/MeinInpResSchema.xsd

    start http://localhost:7001/JaxRsContractFirstService/MeinInpResSchema.xsd

    Dies ist möglich, weil die Schema-XSD-Datei im src\main\webapp-Verzeichnis liegt.

  17. Falls Sie nicht das gewünschte Ergebnis erhalten, prüfen Sie, ob Ihr Input-XML-Format wirklich dem XSD-Schema entspricht: Führen Sie eine Validierung durch, beispielsweise mit: XsdValidation.java.
    Alternativ können Sie auch das inputTO-XML-Rootelement erweitern um die Attribute "xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='mein.ns MeinInpResSchema.xsd'": Dann können Sie auch in Eclipse validieren (mit der rechten Maustaste).

  18. Falls Sie eine vollständige WADL-Datei haben, können Sie daraus Client-seitige Stubs mit wadl2java generieren.

    Falls Sie keine Schema-XSD-Datei haben, aber eine WSDL-Beschreibung, können Sie die Java-Klassen statt mit xjc auch mit wsimport generieren.

  19. Wenn Sie sowohl die vom Client versendeten XML-Anfragen als auch die XML-Server-Antworten analysieren wollen, sind so genannte HTTP-Monitore oder TCP/IP-Monitore sehr hilfreich, die als Proxy oder Tunnel zwischen Client und Server geschaltet werden.

    Um dies durchzuführen, müssen Sie allerdings in der Client-Anfrage-REST-URL (z.B. http://localhost:4435/contractfirst) eine andere Portnummer als im REST-Server (z.B. http://localhost:4434) konfigurieren.

    Dies ist sehr einfach möglich, indem Sie obigen ContractfirstServiceTest leicht modifizieren. Erstellen Sie im Testverzeichnis src\test\java\contractfirstservice die JUnit-Modultestklasse: ContractfirstServiceTestMitHttpMonitor.java

    package contractfirstservice;
    
    import javax.xml.bind.JAXBElement;
    import org.junit.*;
    import com.sun.jersey.api.client.WebResource;
    import contractfirstgenerated.*;
    
    public class ContractfirstServiceTestMitHttpMonitor
    {
       @Test
       public void testContractfirstServiceMitHttpMonitor() throws Exception
       {
          final String xmlUtf8 = "text/xml; charset=utf-8";
    
          // Aufruf mit verschiedenen Portnummern, damit mit einem HTTP-Monitor oder TCP/IP-Monitor analysiert werden kann:
          try( RestServerTestUtil restServerTestUtil = new RestServerTestUtil(
                "http://localhost:4434", "http://localhost:4435/contractfirst" ) )
          {
             WebResource wrs = restServerTestUtil.getWebResource();
             InputTO inpTO = new InputTO();
             inpTO.setI( 42 );
             inpTO.setS( "abc xyz" );
             JAXBElement<InputTO> inpJaxb = (new ObjectFactory()).createInputTO( inpTO );
             ResultTO resTO = wrs.type( xmlUtf8 ).accept( xmlUtf8 ).post( ResultTO.class, inpJaxb );
             Assert.assertEquals(  84,             resTO.getI() );
             Assert.assertEquals( "abc xyz - ret", resTO.getS() );
          }
       }
    }
    

    Konfigurieren Sie in einem beliebigen HTTP-Monitor oder TCP/IP-Monitor als Server-Host "http://localhost:4434" und als Local-Port "4435" und führen Sie ContractfirstServiceTestMitHttpMonitor aus. Weiter oben unter TCP/IP-Monitore werden die einzelnen Schritte gezeigt, um dies in verschiedenen TCP/IP-Monitoren durchzuführen. Sehen Sie sich dort die Screenshots an.



"Contract-First"-REST-Client (ausgehend von Schema-XSD-Datei)

Das letzte Beispiel JaxRsContractFirstService enthält einen REST-Service, aber keinen REST-Client. Anders als in den bisherigen Beispielen, soll diesmal der REST-Client in einem getrennten Modul implementiert werden, was natürlich wesentlich realitätsnäher ist.

Das Besondere an diesem Beispiel ist, dass auch das REST-Client-Modul über einen JUnit-Modultest verfügt, der während der Dauer des Tests einen Grizzly-Webserver als embedded Server temporär startet und darin den REST-Service des anderen REST-Service-Modules ausführt.

Dies ist nicht immer möglich. Aber es ist beispielsweise möglich, falls:

Der im Folgenden gezeigte REST-Client benötigt den REST-Service vom letzten Beispiel. Der Einfachheit halber werden auch einige Dateien aus dem letzten Beispiel kopiert (statt sie erneut per xjc zu generieren, was realitätsnäher wäre).

  1. Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:

    cd \MeinWorkspace

    md JaxRsContractFirstClient

    cd JaxRsContractFirstClient

    md src\main\java\contractfirstclient

    md src\test\java\contractfirstclient

    copy ..\JaxRsContractFirstService\pom.xml

    xcopy ..\JaxRsContractFirstService\src\main\java\contractfirstgenerated src\main\java\contractfirstgenerated\ /S

    tree /F

  2. Ersetzen Sie im neuen JaxRsContractFirstClient-Projektverzeichnis in der kopierten pom.xml die Zeile
    "<artifactId>JaxRsContractFirstService</artifactId>" durch "<artifactId>JaxRsContractFirstClient</artifactId>".

  3. Damit das Client-Modul den REST-Service des Service-Moduls ausführen kann, benötigt es Zugriff auf den Sourcecode vom REST-Service-Module JaxRsContractFirstService. Außerdem zeigt das Beispiel, wie zusätzlich auch auf den Test-Sourcecode dieses Moduls zugegriffen werden kann. So kann auch die REST-Server-Test-Util-Klasse RestServerTestUtil.java verwendet werden.

    Ergänzen Sie für diese beiden Abhängigkeiten im neuen JaxRsContractFirstClient-Projektverzeichnis in der kopierten pom.xml hinter der Zeile "<dependencies>" die Zeilen:

        <dependency>
          <groupId>minirestwebservice</groupId>
          <artifactId>JaxRsContractFirstService</artifactId>
          <classifier>fuer-test</classifier>
          <version>1.0-SNAPSHOT</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>minirestwebservice</groupId>
          <artifactId>JaxRsContractFirstService</artifactId>
          <version>1.0-SNAPSHOT</version>
          <type>test-jar</type>
          <scope>test</scope>
        </dependency>
    

    Beachten Sie die unterschiedliche spezielle Syntax: "<classifier>fuer-test</classifier>" und "<type>test-jar</type>".

  4. Im REST-Service-Module JaxRsContractFirstService wird bislang lediglich eine WAR-Datei erzeugt, die für den Betrieb im Webserver verwendet wird. Diese WAR-Datei kann nicht für die genannten Abhängigkeiten verwendet werden. Dafür werden zwei zusätzliche JAR-Dateien benötigt, die mit Hilfe des maven-jar-plugin generiert werden.

    Ergänzen Sie hierfür in dem anderen JaxRsContractFirstService-Projektverzeichnis in der pom.xml hinter der Zeile "<plugins>" die Zeilen:

          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>2.6</version>
            <executions>
              <execution>
                <goals>
                  <goal>test-jar</goal>
                  <goal>jar</goal>
                </goals>
              </execution>
            </executions>
            <configuration>
              <classifier>fuer-test</classifier>
            </configuration>
          </plugin>
    

    Erläuterungen zum maven-jar-plugin finden Sie unter maven.htm#Test-Jar und Maven JAR Plugin.

  5. Zurück zum JaxRsContractFirstClient-Projekt:
    Erstellen Sie im Verzeichnis src\main\java\contractfirstclient die Client-Klasse: ContractFirstClient.java

    package contractfirstclient;
    
    import javax.xml.bind.JAXBElement;
    import com.sun.jersey.api.client.*;
    import contractfirstgenerated.*;
    
    public class ContractFirstClient
    {
       static final String URL_SRVR = "http://localhost:4434";
       static final String RST_PATH = "/contractfirst";
       static final String XML_UTF8 = "text/xml; charset=utf-8";
    
       public static ResultTO callContractfirstService( InputTO inpTO )
       {
          WebResource wrs = Client.create().resource( URL_SRVR + RST_PATH );
          JAXBElement<InputTO> inpJaxb = (new ObjectFactory()).createInputTO( inpTO );
    
          // HTTP-POST zum REST-Service:
          return wrs.type( XML_UTF8 ).accept( XML_UTF8 ).post( ResultTO.class, inpJaxb );
       }
    }
    
  6. Erstellen Sie im Testverzeichnis src\test\java\contractfirstclient die JUnit-Modultestklasse: ContractFirstClientTest.java

    package contractfirstclient;
    
    import java.io.IOException;
    import org.junit.*;
    import contractfirstgenerated.*;
    import contractfirstservice.RestServerTestUtil;
    
    public class ContractFirstClientTest
    {
       @Test
       public void testContractFirstClient() throws IOException
       {
          InputTO inpTO = new InputTO();
          inpTO.setI( 42 );
          inpTO.setS( "abc xyz" );
    
          // Das REST-Server-Test-Util startet den Grizzly-Webserver:
          try( RestServerTestUtil restServerTestUtil = new RestServerTestUtil( ContractFirstClient.RST_PATH ) )
          {
             // Aufruf des REST-Services ueber den REST-Client:
             ResultTO resTO = ContractFirstClient.callContractfirstService( inpTO );
    
             Assert.assertEquals(  84,             resTO.getI() );
             Assert.assertEquals( "abc xyz - ret", resTO.getS() );
          }
       }
    }
    
  7. Die Projektstruktur des Client-Moduls sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\JaxRsContractFirstClient]
     |- [src]
     |   |- [main]
     |   |   '- [java]
     |   |       |- [contractfirstclient]
     |   |       |   |- ContractFirstClient.java
     |   |       '- [contractfirstgenerated]
     |   |           |- InputTO.java
     |   |           |- ObjectFactory.java
     |   |           |- package-info.java
     |   |           '- ResultTO.java
     |   '- [test]
     |       '- [java]
     |           '- [contractfirstclient]
     |               '- ContractFirstClientTest.java
     '- pom.xml
    
  8. Wechseln Sie in das REST-Service-Modulverzeichnis und führen Sie folgende Kommandos aus:

    cd \MeinWorkspace\JaxRsContractFirstService

    mvn clean install

    dir target\*.?ar

    Außer der WAR-Datei JaxRsContractFirstService.war werden zusätzlich die beiden JAR-Dateien JaxRsContractFirstService-fuer-test.jar und JaxRsContractFirstService-tests.jar erzeugt und in das lokale Maven-Repository kopiert.

  9. Wechseln Sie in das REST-Service-Modulverzeichnis und führen Sie folgende Kommandos aus:

    cd \MeinWorkspace\JaxRsContractFirstClient

    mvn clean test

    Der JUnit-Modultest ContractFirstClientTest.java im Client-Modul wird erfolgreich durchlaufen. Dafür wurde während des Tests ein Grizzly-Webserver gestartet und der REST-Service ausgeführt.



Authentifizierung, Integrationstest und Tomcat

Im folgenden Beispiel wird das JaxRsContractFirstService-Beispiel um Authentifizierung und um einen Integrationstest mit Tomcat erweitert.

  1. Installieren Sie Tomcat 8, zum Beispiel wie beschrieben unter jsp-install.htm#InstallationUnterWindows.

  2. Führen Sie in Ihrem Projekte-Workspace-Verzeichnis folgende Kommandos aus:

    cd \MeinWorkspace

    xcopy JaxRsContractFirstService JaxRsAuthentication\ /S

    cd JaxRsAuthentication

    md src\test\java\integrationstest

  3. Ersetzen Sie im neuen JaxRsAuthentication-Projektverzeichnis den Inhalt der pom.xml durch:

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>restwebservice</groupId>
      <artifactId>JaxRsAuthentication</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
      <name>JaxRsAuthentication: RESTful-Webservice mit Integrationstest</name>
      <properties>
        <appsrv.containerId>tomcat8x</appsrv.containerId>
        <!-- Tomcat-Pfad anpassen (unter Windows unbedingt mit Laufwerksbuchstabe): -->
        <appsrv.srvhome>D:\Tools\Tomcat</appsrv.srvhome>
        <appsrv.dmnhome>${appsrv.srvhome}</appsrv.dmnhome>
      </properties>
      <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
          <plugin>
            <groupId>org.codehaus.cargo</groupId>
            <artifactId>cargo-maven2-plugin</artifactId>
            <version>1.5.0</version>
            <configuration>
              <container>
                <containerId>${appsrv.containerId}</containerId>
                <home>${appsrv.srvhome}</home>
              </container>
              <configuration>
                <type>existing</type>
                <home>${appsrv.dmnhome}</home>
                <!--
                <properties>
                  <cargo.remote.username>${appsrv.usr}</cargo.remote.username>
                  <cargo.remote.password>${appsrv.pwd}</cargo.remote.password>
                </properties>
                -->
              </configuration>
              <wait>false</wait>
            </configuration>
            <executions>
              <execution>
                <id>start-container</id>
                <phase>pre-integration-test</phase>
                <goals>
                  <goal>start</goal>
                </goals>
              </execution>
              <execution>
                <id>stop-container</id>
                <phase>post-integration-test</phase>
                <goals>
                  <goal>stop</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
          <plugin>
             <groupId>org.apache.maven.plugins</groupId>
             <artifactId>maven-surefire-plugin</artifactId>
             <version>2.19.1</version>
             <configuration>
                <excludes>
                   <exclude>**/integrationstest/*Test.java</exclude>
                </excludes>
             </configuration>
             <executions>
                <execution>
                   <id>integration-tests</id>
                   <phase>integration-test</phase>
                   <goals>
                      <goal>test</goal>
                   </goals>
                   <configuration>
                      <skip>false</skip>
                      <excludes>
                         <exclude>none</exclude>
                      </excludes>
                      <includes>
                         <include>**/integrationstest/*Test.java</include>
                      </includes>
                   </configuration>
                </execution>
             </executions>
          </plugin>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
              <source>1.7</source>
              <target>1.7</target>
            </configuration>
          </plugin>
         </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-client</artifactId>
          <version>1.19.1</version>
        </dependency>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-server</artifactId>
          <version>1.19.1</version>
        </dependency>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-servlet</artifactId>
          <version>1.19.1</version>
        </dependency>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-grizzly2</artifactId>
          <version>1.19.1</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <!-- Version muss zu "com.sun.jersey:jersey-grizzly2" passen, siehe "mvn dependency:tree" -->
          <groupId>org.glassfish.grizzly</groupId>
          <artifactId>grizzly-http-all</artifactId>
          <version>2.2.16</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    </project>
    

    Passen Sie den Pfad in "<appsrv.srvhome> ... </appsrv.srvhome>" an Ihre Tomcat-Installation an.

  4. Ersetzen Sie im src\main\webapp\WEB-INF-Verzeichnis den Inhalt der web.xml durch:

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             id="WebApp_ID" version="2.5">
      <display-name>REST</display-name>
    
      <servlet>
        <display-name>JAX-RS REST Servlet</display-name>
        <servlet-name>REST-Servlet</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
      </servlet>
      <servlet-mapping>
        <servlet-name>REST-Servlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
      </servlet-mapping>
    
      <security-constraint>
        <web-resource-collection>
          <web-resource-name>JaxRsAuthentication</web-resource-name>
          <url-pattern>/rest/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
          <role-name>manager</role-name>
        </auth-constraint>
      </security-constraint>
      <login-config>
        <auth-method>BASIC</auth-method>
        <realm-name>Mein Applikations-Realm</realm-name>
      </login-config>
      <security-role>
        <role-name>manager</role-name>
      </security-role>
    
    </web-app>
    
  5. Erzeugen Sie im src\test\java\integrationstest-Verzeichnis folgende Testklasse: JaxRsAuthenticationIntegrTest.java

    package integrationstest;
    
    import javax.xml.bind.JAXBElement;
    import org.junit.*;
    import com.sun.jersey.api.client.Client;
    import com.sun.jersey.api.client.WebResource;
    import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter;
    import contractfirstgenerated.*;
    
    public class JaxRsAuthenticationIntegrTest
    {
       @Test
       public void testJaxRsAuthentication() throws Exception
       {
          // Testclient:
          final String xmlUtf8 = "text/xml; charset=utf-8";
          String url = "http://localhost:8080/JaxRsAuthentication/rest/contractfirst";
          String usr = "MeinName";
          String pwd = "MeinPasswort";
          Client clt = Client.create();
          clt.addFilter( new HTTPBasicAuthFilter( usr, pwd ) );
          WebResource wrs = clt.resource( url );
    
          // Mit JAXB und mit bequemen Java-Objekten:
          InputTO inpTO = new InputTO();
          inpTO.setI( 42 );
          inpTO.setS( "abc xyz" );
          JAXBElement<InputTO> inpJaxb = (new ObjectFactory()).createInputTO( inpTO );
          ResultTO resTO = wrs.type( xmlUtf8 ).accept( xmlUtf8 ).post( ResultTO.class, inpJaxb );
          Assert.assertEquals(  84,             resTO.getI() );
          Assert.assertEquals( "abc xyz - ret", resTO.getS() );
    
          // Ohne JAXB und mit XML-Strings (Namespace beachten):
          String resXml = wrs.type( xmlUtf8 ).accept( xmlUtf8 ).post(
                               String.class, "<inputTO xmlns='mein.ns'><i>42</i><s>abc xyz</s></inputTO>" );
          Assert.assertEquals( "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" +
                               "<resultTO xmlns=\"mein.ns\"><i>84</i><s>abc xyz - ret</s></resultTO>", resXml );
    
          // Eventuell verzoegerte Beendigung (siehe Text):
          // Thread.sleep( 20000 );
       }
    }
    

    Beachten Sie, dass diesmal innerhalb der Integrationstestklasse kein Server gestartet wird, weil durch die Maven-Integrationstest-Konfiguration Tomcat gestartet wird.

  6. Fügen Sie in Ihrer tomcat-users.xml in Ihrem Tomcat-conf-Verzeichnis (z.B. \Tools\Tomcat\conf) die benötigten Benutzer und Rollen hinzu, zum Beispiel so:

    <?xml version='1.0' encoding='utf-8'?>
    <tomcat-users>
      <role rolename="admin"/>
      <role rolename="manager"/>
      <user username="MeinName" password="MeinPasswort" roles="admin,manager"/>
    </tomcat-users>
    
  7. Starten Sie den Integrationstest:

    mvn integration-test

    Beachten Sie, dass Tomcat automatisch hoch- und wieder heruntergefahren wird.

  8. Falls Sie in der Integrationstestklasse JaxRsAuthenticationIntegrTest.java eine Warteschleife hinzufügen (z.B. Thread.sleep( 20000 );), oder alternativ die WAR-Datei normal deployen, können Sie den REST-Service auch mit cURL ansprechen:

    curl -u "MeinName:MeinPasswort" -i -H Content-type:text/xml --request POST -d "<inputTO xmlns='mein.ns'><i>42</i><s>abc xyz</s></inputTO>" "http://localhost:8080/JaxRsAuthentication/rest/contractfirst"

    Bitte beachten Sie, dass Sie diesmal für die HTTP Basic Authentication mit -u den Benutzernamen und das Kennwort angeben müssen (ansonsten würden Sie erhalten: "HTTP/1.1 401 Unauthorized").

    Außerdem können Sie die Schema-XSD-Datei per cURL oder per Webbrowser abfragen:

    curl http://localhost:8080/JaxRsAuthentication/MeinInpResSchema.xsd

    start http://localhost:8080/JaxRsAuthentication/MeinInpResSchema.xsd

    Dies ist möglich, weil die Schema-XSD-Datei im src\main\webapp-Verzeichnis liegt.

  9. Falls Sie die Exception "Tomcat starting... java.lang.ClassNotFoundException: org.apache.catalina.startup.Catalina" erhalten: Prüfen Sie den in der pom.xml bei "<properties> ... <appsrv.srvhome> ..." eingetragenen Pfad zum Tomcat-Verzeichnis. Falls Sie Windows verwenden, sollte dieser Pfad inklusive Laufwerksbuchstabe angegeben sein.

  10. Falls Sie eine andere Exception erhalten, suchen Sie in der \Tools\Tomcat\logs\localhost.*.log-Datei die ursprüngliche Exception.

  11. Falls Sie WebLogic statt Tomcat verwenden wollen, sehen Sie sich die Hinweise zur Authentifizierung unter WebLogic an.



Bücherverwaltung-Programmierbeispiel mit @GET, @PUT, @POST, @DELETE, @PathParam, @QueryParam, @FormParam und @XmlRootElement

Maven-Webprojekt für die Bücherverwaltung

  1. Installieren Sie Tomcat 8, zum Beispiel wie beschrieben unter jsp-install.htm#InstallationUnterWindows.

  2. Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:

    cd \MeinWorkspace

    md JaxRsBuecherverwaltung

    cd JaxRsBuecherverwaltung

    md src\main\webapp\WEB-INF

    md src\main\java\de\meinefirma\meinprojekt\buecher

    md src\main\java\de\meinefirma\meinprojekt\dao

    md src\main\java\de\meinefirma\meinprojekt\rest

    md src\main\java\de\meinefirma\meinprojekt\client

    tree /F

  3. Erstellen Sie im JaxRsBuecherverwaltung-Projektverzeichnis die Projektkonfigurationsdatei: pom.xml

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>buecher</groupId>
      <artifactId>JaxRsBuecherverwaltung</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
      <name>${project.artifactId}</name>
      <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
              <source>1.7</source>
              <target>1.7</target>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-client</artifactId>
          <version>1.19.1</version>
        </dependency>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-server</artifactId>
          <version>1.19.1</version>
        </dependency>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-servlet</artifactId>
          <version>1.19.1</version>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    </project>
    
  4. Erstellen Sie im src\main\webapp\WEB-INF-Verzeichnis: web.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             id="WebApp_ID" version="2.5">
      <display-name>JaxRsBuecherverwaltung</display-name>
      <servlet>
        <display-name>JAX-RS REST Servlet</display-name>
        <servlet-name>REST-Servlet</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
      </servlet>
      <servlet-mapping>
        <servlet-name>REST-Servlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
      </servlet-mapping>
      <welcome-file-list>
        <welcome-file>index.html</welcome-file>
      </welcome-file-list>
    </web-app>
    

DO, TO, CRUD-DAO und Util für die Bücherverwaltung

  1. Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\buecher-Verzeichnis die Buch-Domainobjekt-Klasse: BuchDO.java

    package de.meinefirma.meinprojekt.buecher;
    
    import javax.xml.bind.annotation.XmlRootElement;
    
    /** Buch-Domainobjekt */
    @XmlRootElement
    public class BuchDO
    {
       private Long   id;
       private String isbn;
       private String titel;
       private Double preis;
    
       public Long   getId()    { return id; }
       public String getIsbn()  { return isbn; }
       public String getTitel() { return titel; }
       public Double getPreis() { return preis; }
       public void setId(    Long   id    ) { this.id    = id; }
       public void setIsbn(  String isbn  ) { this.isbn  = isbn; }
       public void setTitel( String titel ) { this.titel = titel; }
       public void setPreis( Double preis ) { this.preis = preis; }
    }
    
  2. Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\buecher-Verzeichnis die Rueckgabe-Transferobjekt-Klasse: BuecherTO.java

    package de.meinefirma.meinprojekt.buecher;
    
    import java.util.*;
    import javax.xml.bind.annotation.*;
    
    /** Rueckgabe-Transferobjekt */
    @XmlRootElement
    public class BuecherTO
    {
       private Integer      returncode;
       private String       message;
       @XmlElement(nillable = true)
       private List<BuchDO> results = new ArrayList<BuchDO>();
    
       public Integer      getReturncode() { return returncode; }
       public String       getMessage()    { return message;    }
       public List<BuchDO> getResults()    { return results;    }
       public void setReturncode( Integer returncode ) { this.returncode = returncode; }
       public void setMessage(    String  message    ) { this.message    = message;    }
    }
    
  3. Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\dao-Verzeichnis die CRUD-DAO-Klasse: BuchDoDAO.java

    package de.meinefirma.meinprojekt.dao;
    
    import java.util.*;
    import java.util.concurrent.ConcurrentHashMap;
    import de.meinefirma.meinprojekt.buecher.BuchDO;
    
    /** DAO (Data Access Object) fuer CRUD-Operationen (Create, Read, Update, Delete) */
    public class BuchDoDAO
    {
       // Map als Datenbank-Simulation:
       private              Map<Long,BuchDO> buecherPool = new ConcurrentHashMap<Long,BuchDO>();
       private static final BuchDoDAO        INSTANCE    = new BuchDoDAO();
       private static final long             DFLT_ID     = 4710;
    
       private BuchDoDAO()
       {
       }
    
       public static BuchDoDAO getInstance()
       {
          return INSTANCE;
       }
    
       // Neues Buch hinzufuegen:
       public BuchDO createBuch( BuchDO bu )
       {
          synchronized( buecherPool ) {
             if( bu.getId() != null ) {
                if( getBuchById( bu.getId() ) != null )
                   throw new RuntimeException(
                      "Fehler: Es gibt bereits ein Buch mit der ID " + bu.getId() + "." );
             } else {
                long maxId = ( buecherPool.size() > 0 )
                             ? Collections.max( buecherPool.keySet() ).longValue() : DFLT_ID;
                bu.setId( Long.valueOf( ++maxId ) );
             }
             buecherPool.put( bu.getId(), bu );
             return bu;
          }
       }
    
       // Finde Buch mit ID:
       public BuchDO getBuchById( Long id )
       {
          return ( id == null ) ? null : buecherPool.get( id );
       }
    
       // Finde Buecher nach Suchkriterien:
       public List<BuchDO> findeBuecher( Long id, String isbn, String titel )
       {
          List<BuchDO> resultList = new ArrayList<BuchDO>();
          List<BuchDO> snapshotList;
          if( id == null && isEmpty( isbn ) && isEmpty( titel ) )
             return Collections.unmodifiableList( new ArrayList<BuchDO>( buecherPool.values() ) );
          if( id != null && isEmpty( isbn ) && isEmpty( titel ) ) {
             BuchDO bu = getBuchById( id );
             if( bu != null ) resultList.add( bu );
             return resultList;
          }
          synchronized( buecherPool ) {
             snapshotList = new ArrayList<BuchDO>( buecherPool.values() );
          }
          String isbnLC  = ( isbn  == null ) ? null :  isbn.trim().toLowerCase();
          String titelLC = ( titel == null ) ? null : titel.trim().toLowerCase();
          for( BuchDO bu : snapshotList )
             if( (id != null && bu.getId() != null && id.equals( bu.getId() )) ||
                 (!isEmpty( bu.getIsbn() ) && !isEmpty( isbnLC ) &&
                       bu.getIsbn().trim().toLowerCase().contains( isbnLC )) ||
                 (!isEmpty( bu.getTitel() ) && !isEmpty( titelLC ) &&
                       bu.getTitel().trim().toLowerCase().contains( titelLC )) )
                resultList.add( bu );
          return resultList;
       }
    
       // Daten eines per ID definierten Buches aendern:
       public BuchDO updateBuchById( BuchDO bu )
       {
          synchronized( buecherPool ) {
             BuchDO buAlt = buecherPool.get( bu.getId() );
             if( buAlt == null )
                throw new RuntimeException( "Fehler: Es gibt kein Buch mit der ID " + bu.getId() + "." );
             buecherPool.put( bu.getId(), bu );
             return bu;
          }
       }
    
       // Per ID definiertes Buch loeschen:
       public BuchDO deleteBuchById( Long id )
       {
          synchronized( buecherPool ) {
             return buecherPool.remove( id );
          }
       }
    
       private static boolean isEmpty( String s )
       {
          return s == null || s.trim().length() <= 0;
       }
    }
    
  4. Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\dao-Verzeichnis die Util-Klasse: BuecherUtil.java

    package de.meinefirma.meinprojekt.dao;
    
    import java.util.List;
    import de.meinefirma.meinprojekt.buecher.BuchDO;
    import de.meinefirma.meinprojekt.buecher.BuecherTO;
    
    public class BuecherUtil
    {
       public static final Integer RET_CODE_OK    = new Integer(  0 );
       public static final Integer RET_CODE_ERROR = new Integer( -1 );
    
       // Finde Buecher nach Suchkriterien:
       public static BuecherTO findeBuecher( Long id, String isbn, String titel )
       {
          BuecherTO    buecherTO    = new BuecherTO();
          List<BuchDO> buecherListe = BuchDoDAO.getInstance().findeBuecher( id, isbn, titel );
          if( buecherListe == null )
             return fehlerBuecherTO();
          if( id == null && isEmpty( isbn ) && isEmpty( titel ) ) {
             buecherTO.setMessage( buecherListe.size() + " Buecher" );
          } else {
             StringBuffer sb = new StringBuffer();
             sb.append( buecherListe.size() + " Ergebnis(se) fuer" );
             if( id != null        ) sb.append( " ID = " + id );
             if( !isEmpty( isbn  ) ) sb.append( " ISBN = " + isbn );
             if( !isEmpty( titel ) ) sb.append( " Titel = " + titel );
             buecherTO.setMessage( sb.toString() );
          }
          buecherTO.getResults().addAll( buecherListe );
          buecherTO.setReturncode( RET_CODE_OK );
          return buecherTO;
       }
    
       public static BuchDO erzeugeBuchDO( Long id, String isbn, String titel, Double preis )
       {
          BuchDO buchDO = new BuchDO();
          buchDO.setId(    id    );
          buchDO.setIsbn(  isbn  );
          buchDO.setTitel( titel );
          buchDO.setPreis( preis );
          return buchDO;
       }
    
       public static BuecherTO erzeugeBuecherTO( String msg, BuchDO buchDO )
       {
          if( buchDO == null ) return fehlerBuecherTO();
          BuecherTO buecherTO = new BuecherTO();
          buecherTO.getResults().add( buchDO );
          buecherTO.setMessage( msg );
          buecherTO.setReturncode( RET_CODE_OK );
          return buecherTO;
       }
    
       public static BuecherTO fehlerBuecherTO()
       {
          BuecherTO buecherTO = new BuecherTO();
          buecherTO.setMessage( "Parameterfehler" );
          buecherTO.setReturncode( RET_CODE_ERROR );
          return buecherTO;
       }
    
       public static boolean isEmpty( String s )
       {
          return s == null || s.trim().length() <= 0;
       }
    }
    

RESTful-Webservice

  1. Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\rest-Verzeichnis die Java-Klasse BuecherRestService.java, in welcher die vier HTTP-Verben GET, PUT, POST und DELETE als RESTful-Webservice REST-konform implementiert werden:

    package de.meinefirma.meinprojekt.rest;
    
    import javax.ws.rs.*;
    import javax.ws.rs.core.MediaType;
    import de.meinefirma.meinprojekt.buecher.*;
    import de.meinefirma.meinprojekt.dao.*;
    
    /** RESTful-Webservice */
    @Produces( MediaType.TEXT_XML )
    @Path( "/Artikel/Buecher" )
    public class BuecherRestService
    {
       private BuchDoDAO dao = BuchDoDAO.getInstance();
    
       // Per ID definiertes Buch ausgeben:
       @GET @Path("{id}")
       public BuecherTO getBuchById(
             @PathParam("id") String id )
       {
          BuchDO bu = dao.getBuchById( longFromString( id ) );
          return BuecherUtil.erzeugeBuecherTO( "Buch mit ID " + id, bu );
       }
    
       // Liste von ueber Suchkriterien gefundener Buecher ausgeben:
       @GET
       public BuecherTO getBuecherListe(
             @QueryParam("id")    String id,
             @QueryParam("isbn")  String isbn,
             @QueryParam("titel") String titel )
       {
          return BuecherUtil.findeBuecher( longFromString( id ), isbn, titel );
       }
    
       // Daten eines per ID definierten Buches aendern:
       @PUT @Path("{id}")
       public BuecherTO updateBuchById(
             @PathParam("id")    String id,
             @FormParam("isbn")  String isbn,
             @FormParam("titel") String titel,
             @FormParam("preis") String preis )
       {
          BuchDO bu = BuecherUtil.erzeugeBuchDO( longFromString( id ), isbn, titel, doubleFromString( preis ) );
          return BuecherUtil.erzeugeBuecherTO( "Buchdaten geaendert", dao.updateBuchById( bu ) );
       }
    
       // Neues Buch hinzufuegen (ueber Formular):
       @POST
       public BuecherTO createBuch(
             @FormParam("isbn")  String isbn,
             @FormParam("titel") String titel,
             @FormParam("preis") String preis )
       {
          BuchDO bu = BuecherUtil.erzeugeBuchDO( null, isbn, titel, doubleFromString( preis ) );
          return BuecherUtil.erzeugeBuecherTO( "Buch hinzugefuegt", dao.createBuch( bu ) );
       }
    
       // Neues Buch hinzufuegen (ueber XML):
       @POST @Consumes( MediaType.TEXT_XML )
       public BuecherTO createBuch( BuchDO bu )
       {
          return BuecherUtil.erzeugeBuecherTO( "Buch hinzugefuegt", dao.createBuch( bu ) );
       }
    
       // Per ID definiertes Buch loeschen:
       @DELETE @Path("{id}")
       public BuecherTO deleteBuchById(
             @PathParam("id") String id )
       {
          BuchDO bu = dao.deleteBuchById( longFromString( id ) );
          return BuecherUtil.erzeugeBuecherTO( "Buch geloescht", bu );
       }
    
       private static Long longFromString( String s )
       {
          if( !BuecherUtil.isEmpty( s ) ) try { return new Long( s.trim() ); } catch( NumberFormatException ex ) {/*ok*/}
          return null;
       }
    
       private static Double doubleFromString( String s )
       {
          if( !BuecherUtil.isEmpty( s ) ) try { return new Double( s.trim() ); } catch( NumberFormatException ex ) {/*ok*/}
          return null;
       }
    }
    

Webseite

  1. Erzeugen Sie im src\main\webapp-Verzeichnis die HTML-Seite: index.html

    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
      <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
      <title>Meine RESTful-Webservice-Startseite</title>
    </head>
    <body>
      <h3> Meine RESTful-Webservice-Startseite </h3>
    
      <h4><u> WADL </u></h4>
      <p> <a href="/JaxRsBuecherverwaltung/rest/application.wadl">/JaxRsBuecherverwaltung/rest/application.wadl</a> </p>
    
      <h4><u> Buecher-Verwaltung </u></h4>
    
      <p> Zwei erste Bücher anlegen (alle Felder müssen ausgefüllt sein): </p>
      <form method="POST" action="/JaxRsBuecherverwaltung/rest/Artikel/Buecher" enctype="application/x-www-form-urlencoded">
         ISBN:&nbsp;<input type="text" name="isbn"  value="1234567891" maxlength=20> &nbsp;
        Titel:&nbsp;<input type="text" name="titel" value="MeinTitel1" maxlength=80> &nbsp;
        Preis:&nbsp;<input type="text" name="preis" value="12.34"      maxlength=20> &nbsp;
        <input type="submit" value="Erstes Buch anlegen">
      </form>
      <form method="POST" action="/JaxRsBuecherverwaltung/rest/Artikel/Buecher" enctype="application/x-www-form-urlencoded">
         ISBN:&nbsp;<input type="text" name="isbn"  value="1234567892" maxlength=20> &nbsp;
        Titel:&nbsp;<input type="text" name="titel" value="MeinTitel2" maxlength=80> &nbsp;
        Preis:&nbsp;<input type="text" name="preis" value="22.34"      maxlength=20> &nbsp;
        <input type="submit" value="Zweites Buch anlegen">
      </form>
    
      <p> Abfragen (bitte vorher obige zwei Test-Bücher anlegen): </p>
      <p> <a href="/JaxRsBuecherverwaltung/rest/Artikel/Buecher">/JaxRsBuecherverwaltung/rest/Artikel/Buecher</a> (alle Bücher) </p>
      <p> <a href="/JaxRsBuecherverwaltung/rest/Artikel/Buecher/4711">/JaxRsBuecherverwaltung/rest/Artikel/Buecher/4711</a> (Buch mit ID 4711) </p>
      <p> <a href="/JaxRsBuecherverwaltung/rest/Artikel/Buecher?isbn=1234567892">/JaxRsBuecherverwaltung/rest/Artikel/Buecher?isbn=1234567892</a> (Buch mit ISBN 1234567892) </p>
      <p> <a href="/JaxRsBuecherverwaltung/rest/Artikel/Buecher/?titel=MeinTitel2">/JaxRsBuecherverwaltung/rest/Artikel/Buecher/?titel=MeinTitel2</a> (Buch mit Titel 'MeinTitel2') </p>
      <p> <a href="/JaxRsBuecherverwaltung/rest/Artikel/Buecher/?isbn=1234567891&titel=MeinTitel2">/JaxRsBuecherverwaltung/rest/Artikel/Buecher/?isbn=1234567891&titel=MeinTitel2</a> (Bücher mit ISBN 1234567891 oder Titel 'MeinTitel2') </p>
    
      <p> Weiteres neues Buch anlegen (alle Felder müssen ausgefüllt werden): </p>
      <form method="POST" action="/JaxRsBuecherverwaltung/rest/Artikel/Buecher" enctype="application/x-www-form-urlencoded">
         ISBN:&nbsp;<input type="text" name="isbn"  value="1472583693"    maxlength=20> &nbsp;
        Titel:&nbsp;<input type="text" name="titel" value="Neuer Titel X" maxlength=80> &nbsp;
        Preis:&nbsp;<input type="text" name="preis" value="123.45"        maxlength=20> &nbsp;
        <input type="submit" value="Neues Buch anlegen">
      </form>
    
      <p> Bücher finden (es genügt einzelne Felder zu füllen; als ISBN und Titel genügen Teilstrings zum Finden mehrerer passender Bücher): </p>
      <form method="GET" action="/JaxRsBuecherverwaltung/rest/Artikel/Buecher/" enctype="application/x-www-form-urlencoded">
         ISBN:&nbsp;<input type="text" name="isbn"  maxlength=20> &nbsp;
        Titel:&nbsp;<input type="text" name="titel" maxlength=80> &nbsp;
           ID:&nbsp;<input type="text" name="id"    maxlength=40> &nbsp;
        <input type="submit" value="Bücher finden">
      </form>
    </body>
    </html>
    

Test per Webseite

  1. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\JaxRsBuecherverwaltung]
     |- [src]
     |   '- [main]
     |       |- [java]
     |       |   '- [de]
     |       |       '- [meinefirma]
     |       |           '- [meinprojekt]
     |       |               |- [buecher]
     |       |               |   |- BuchDO.java
     |       |               |   '- BuecherTO.java
     |       |               |- [client]
     |       |               |- [dao]
     |       |               |   |- BuchDoDAO.java
     |       |               |   '- BuecherUtil.java
     |       |               '- [rest]
     |       |                   '- BuecherRestService.java
     |       '- [webapp]
     |           |- [WEB-INF]
     |           |   '- web.xml
     |           '- index.html
     '- pom.xml
    

    Die Package-Struktur ist etwas ungewöhnlich, aber so können zum einen leichter die DO- und TO-Klassen aus Schema-XSD-Dateien generiert werden ("Contract-First") und zum anderen kann leichter zusätzlich ein SOAP Webservice hinzugefügt werden (siehe unten).

  2. Testen Sie den RESTful-Webservice (passen Sie die Pfade an):

    set TOMCAT_HOME=D:\Tools\Tomcat

    del %TOMCAT_HOME%\webapps\JaxRsBuecherverwaltung.war

    pushd .

    cd /D %TOMCAT_HOME%\bin

    startup.bat

    popd

    mvn package

    copy target\JaxRsBuecherverwaltung.war %TOMCAT_HOME%\webapps

    start http://localhost:8080/JaxRsBuecherverwaltung/

  3. Klicken Sie auf der JaxRsBuecherverwaltung-Webseite auf die verschiedenen Links und sehen Sie sich die jeweiligen XML-Ergebnisse an. Leider zeigen neuere Versionen vom Firefox-Webbrowser die XML-Dateien nur noch verstümmelt an. Verwenden Sie in diesem Fall entweder einen anderen Webbrowser, z.B. Edge, oder verwenden Sie ein Kommandozeilen-Tool, wie curl, oder verwenden Sie einen eigenen Client, wie im folgenden Beispiel.
    Studieren Sie die WADL-Beschreibung.
    Fügen Sie über "Neues Buch anlegen" verschiedene neue Bücher hinzu.
    Finden Sie Bücher über "Bücher finden". Dabei genügen in den ISBN- und Titel-Feldern auch Teil-Strings (z.B. ISBN '123' oder Titel 'meintit'), so dass mit einer Anfrage mehrere passende Bücher gefunden werden können.

  4. Beenden Sie Tomcat:

    set TOMCAT_HOME=D:\Tools\Tomcat

    pushd .

    cd /D %TOMCAT_HOME%\bin

    shutdown.bat

    popd

GET, PUT, POST und DELETE mit einem Java-Client

  1. Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\client-Verzeichnis die RESTful-Webservice-Client-Klasse: BuecherRestClient.java

    package de.meinefirma.meinprojekt.client;
    
    import javax.ws.rs.core.MultivaluedMap;
    import com.sun.jersey.api.client.*;
    import com.sun.jersey.core.util.MultivaluedMapImpl;
    
    /** RESTful-Webservice-Client */
    public class BuecherRestClient
    {
       public static void main( String[] args )
       {
          Client      client = Client.create();
          WebResource webResource = client.resource( "http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher" );
    
          if( args.length > 1 && args[0].toLowerCase().equals( "getbuchbyid" ) ) {
             System.out.println( "\nBuch mit ID " + args[1] + ":" );
             System.out.println( getBuchById( webResource, args[1] ) );
          }
          else if( args.length > 2 && args[0].toLowerCase().equals( "findebuecher" ) ) {
             System.out.println( "\nFinde Buecher mit ISBN " + args[1] + " oder Titel '" + args[2] + "':" );
             System.out.println( findeBuecher( webResource, null, args[1], args[2] ) );
          }
          else if( args.length > 4 && args[0].toLowerCase().equals( "putbuch" ) ) {
             System.out.println( "\nAendere Daten zum Buch mit der ID " + args[1] + ":" );
             System.out.println( putBuch( webResource, args[1], "isbn=" + args[2] + "&titel=" + args[3] + "&preis=" + args[4] ) );
          }
          else if( args.length > 3 && args[0].toLowerCase().equals( "postbuch" ) ) {
             System.out.println( "\nNeues Buch anlegen:" );
             ClientResponse response = postBuch( webResource, args[1], args[2], new Double( args[3] ) );
             System.out.println( response.getStatus() + " " + response.getStatusInfo() );
             System.out.println( response.getEntity( String.class ) );
          }
          else if( args.length > 1 && args[0].toLowerCase().equals( "deletebuch" ) ) {
             System.out.println( "\nLoesche Buch mit ID " + args[1] + ":" );
             System.out.println( deleteBuch( webResource, args[1] ) );
          }
       }
    
       static String getBuchById( WebResource webResource, String id )
       {
          return webResource.path( id ).get( String.class );
       }
    
       static String findeBuecher( WebResource webResource, String id, String isbn, String titel )
       {
          MultivaluedMap<String,String> queryParams = new MultivaluedMapImpl();
          if( id    != null ) queryParams.add( "id",    id );
          if( isbn  != null ) queryParams.add( "isbn",  isbn );
          if( titel != null ) queryParams.add( "titel", titel );
          return webResource.queryParams( queryParams ).get( String.class );
       }
    
       static ClientResponse putBuch( WebResource webResource, String id, String data )
       {
          return webResource.path( id ).put( ClientResponse.class, data );
       }
    
       static ClientResponse postBuch( WebResource webResource, String isbn, String titel, Double preis )
       {
          MultivaluedMap<String,String> formData = new MultivaluedMapImpl();
          formData.add( "isbn",  isbn );
          formData.add( "titel", titel );
          formData.add( "preis", "" + preis );
          return webResource.type( "application/x-www-form-urlencoded" ).post( ClientResponse.class, formData );
       }
    
       static ClientResponse deleteBuch( WebResource webResource, String id )
       {
          return webResource.path( id ).delete( ClientResponse.class );
       }
    }
    
  2. Speichern Sie folgende Kommandos in einer Batchdatei (z.B. run-Client.bat), passen Sie darin die Pfade an und führen Sie die Batchdatei aus:

    cls
    set TOMCAT_HOME=D:\Tools\Tomcat
    set _MEIN_PROJEKT_NAME=JaxRsBuecherverwaltung
    
    del %TOMCAT_HOME%\webapps\%_MEIN_PROJEKT_NAME%.war
    rd  %TOMCAT_HOME%\webapps\%_MEIN_PROJEKT_NAME% /S /Q
    
    pushd .
    cd /D %TOMCAT_HOME%\bin
    call startup.bat
    @echo on
    popd
    
    call mvn clean package
    @echo on
    copy target\%_MEIN_PROJEKT_NAME%.war %TOMCAT_HOME%\webapps
    
    @echo Ca. 30 Sekunden warten (eventuell Wartezeit anpassen)
    @ping -n 30 127.0.0.1 >nul
    @echo.
    @echo .................................................................
    set CLASSPATH=target/%_MEIN_PROJEKT_NAME%/WEB-INF/classes;target/%_MEIN_PROJEKT_NAME%/WEB-INF/lib/*
    java de.meinefirma.meinprojekt.client.BuecherRestClient postBuch 1234567891 "MeinTitel1" 12.34
    java de.meinefirma.meinprojekt.client.BuecherRestClient postBuch 1234567892 "MeinTitel2" 22.34
    @echo.
    start http://localhost:8080/%_MEIN_PROJEKT_NAME%/rest/Artikel/Buecher
    @echo.
    @ping -n 2 127.0.0.1 >nul
    java de.meinefirma.meinprojekt.client.BuecherRestClient getBuchById 4711
    java de.meinefirma.meinprojekt.client.BuecherRestClient findeBuecher 1234567891 "MeinTitel2"
    java de.meinefirma.meinprojekt.client.BuecherRestClient putBuch 4712 1234567898 "Client-PUT-Titel" 111
    java de.meinefirma.meinprojekt.client.BuecherRestClient postBuch 9876543210 "Client-POST-Titel" 222
    java de.meinefirma.meinprojekt.client.BuecherRestClient deleteBuch 4711
    @echo.
    start http://localhost:8080/%_MEIN_PROJEKT_NAME%/rest/Artikel/Buecher
    @echo.
    @echo .................................................................
    @ping -n 4 127.0.0.1 >nul
    
    pushd .
    cd /D %TOMCAT_HOME%\bin
    call shutdown.bat
    popd
    

    Sehen Sie sich die Ausgaben von BuecherRestClient auf der Konsole an, und vergleichen Sie die beiden Webseiten (vorher bzw. nachher). Die ping-Kommandos dienen nur dazu, um kurze Pausen einzulegen.

  3. Das Programmierbeispiel ist etwas unsystematisch und inkonsistent: Die Übergabeparameter sind mal einzeln und mal als zusammengefügter String, der Rückgabewert ist mal der XML-String und mal das ClientResponse-Objekt, mal wird der Status ausgegeben und mal nicht etc. Der einzige Grund hierfür ist: Es sollen verschiedene Möglichkeiten demonstriert werden.

GET, PUT, POST und DELETE mit cURL

  1. Die REST-konforme Verwendungen von GET, PUT, POST und DELETE ist oben unter GET, PUT, POST und DELETE erläutert.
    Installieren Sie cURL wie oben beschrieben.
    Lassen Sie sich nach jeder Änderung die Liste aller Bücher anzeigen über: http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher.
    Starten Sie Tomcat:

    cd /D D:\Tools\Tomcat\bin

    startup.bat

  2. POST: Fügen Sie zwei neue Bücher hinzu:

    curl --request POST -d "isbn=1234567891&titel=MeinTitel1&preis=12.34" "http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher"

    curl --request POST -d "isbn=1234567892&titel=MeinTitel2&preis=22.34" "http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher"

    Die neu angelegten Bücher werden mit den neu erzeugten Buch-IDs returniert:

    <?xml version="1.0" encoding="UTF-8" standalone="yes"?><buecherTO>
       <message>Buch hinzugefuegt</message>
       <results><id>4711</id><isbn>1234567891</isbn><preis>12.34</preis><titel>MeinTitel1</titel></results>
       <returncode>0</returncode></buecherTO>
    
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?><buecherTO>
       <message>Buch hinzugefuegt</message>
       <results><id>4712</id><isbn>1234567892</isbn><preis>22.34</preis><titel>MeinTitel2</titel></results>
       <returncode>0</returncode></buecherTO>
    
  3. GET per ID: Laden Sie das Buch mit der ID 4711:

    curl -i "http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher/4711"

    Sie erhalten:

    HTTP/1.1 200 OK
    Server: Apache-Coyote/1.1
    Content-Type: text/xml
    Content-Length: 239
    
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?><buecherTO>
       <message>Buch mit ID 4711</message>
       <results><id>4711</id><isbn>1234567891</isbn><preis>12.34</preis><titel>MeinTitel1</titel></results>
       <returncode>0</returncode></buecherTO>
    
  4. GET mit Suchparametern: Suchen Sie nach Titeln:

    curl -i "http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher?titel=MeinTitel"

    Sie erhalten:

    HTTP/1.1 200 OK
    Server: Apache-Coyote/1.1
    Content-Type: text/xml
    Content-Length: 360
    
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?><buecherTO>
       <message>2 Ergebnis(se) fuer Titel = MeinTitel</message>
       <results><id>4711</id><isbn>1234567891</isbn><preis>12.34</preis><titel>MeinTitel1</titel></results>
       <results><id>4712</id><isbn>1234567892</isbn><preis>22.34</preis><titel>MeinTitel2</titel></results>
    <returncode>0</returncode></buecherTO>
    
  5. PUT: Ändern Sie die Informationen eines vorhandenen Buches:

    curl --request PUT -d "isbn=1234567899&titel=PUT-Titel&preis=111" "http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher/4712"

    Das geänderte Buch wird returniert:

    <?xml version="1.0" encoding="UTF-8" standalone="yes"?><buecherTO>
       <message>Buchdaten geaendert</message>
       <results><id>4712</id><isbn>1234567899</isbn><preis>111.0</preis><titel>PUT-Titel</titel></results>
       <returncode>0</returncode></buecherTO>
    
  6. DELETE: Löschen Sie ein vorhandenes Buch:

    curl --request DELETE "http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher/4711"

  7. Media Type: Falls Sie bei anderen REST-Diensten den Fehler "415 Unsupported Media Type" erhalten, probieren Sie verschiedene Mediatypen. Falls Sie Daten (z.B. per XML über POST) übergeben, sollte auch "-H Content-type:..." korrekt gesetzt werden. "-H Accept:..." definiert nur den Mediatyp des erwarteten Ergebnisses. Bitte beachten Sie auch, dass bei XML manchmal text/xml und manchmal application/xml erwartet wird (siehe hierzu auch: IETF RFC 3023: XML Media Types, difference text/xml vs application/xml, application/xml or text/xml?).

POST mit PostToUrl

Falls Sie einen POST-Request per Java, aber ohne Jersey, verschicken wollen, sehen Sie sich das Programmierbeispiel PostToUrl an. Zum Beispiel folgendermaßen können Sie damit ein neues Buch anlegen:

java PostToUrl http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher "isbn=9876543213&titel=PostToUrl-Titel&preis=333"

Das Programmierbeispiel ist auch auf GET, PUT und DELETE erweiterbar.



Versionierung von REST-Schnittstellen

Damit per REST kommunizierende Systeme bei Änderungen an der REST-Schnittstelle weiter funktionieren, ist es üblich, bei inkompatiblen Änderungen für eine gewisse Zeit sowohl die alte als auch die neue REST-Schnittstelle zu unterstützen, so lange, bis alle Systeme auf die neue Schnittstellenversion umgestellt sind.

Hierzu ist eine Versionierung der REST-Schnittstelle erforderlich, damit geziehlt die passende Version genutzt werden kann. Dazu gibt es verschiedene Verfahren, beispielsweise:



Performance-Vergleich zwischen REST (mit JAX-RS) und SOAP (mit JAX-WS)

Im Folgenden wird das letzte Beispiel (mit REST-Webservice) um einen SOAP-Webservice (mit JAX-WS) erweitert, um vergleichende Performance-Messungen durchführen zu können.

  1. Legen Sie eine Kopie in einem neuen Verzeichnis an:

    cd \MeinWorkspace

    xcopy JaxRsBuecherverwaltung JaxRsJaxWsPerformance\ /S

    cd JaxRsJaxWsPerformance

  2. Ergänzen Sie in der pom.xml folgende Dependency:

        <dependency>
          <!-- Es gibt zwischen bestimmten Versionen von JAX-WS 2, Java SE und Tomcat Inkompatibilitaeten.
               Diese Dependency sollte eingefuegt werden, um die folgenden Exceptions zu vermeiden:
               javax.xml.ws.WebServiceException: Failed to access ...
               java.lang.ClassNotFoundException: com.sun.xml.ws.transport.http.servlet.WSServletContextListener
          -->
          <groupId>com.sun.xml.ws</groupId>
          <artifactId>jaxws-rt</artifactId>
          <version>2.2.10</version>
        </dependency>
    
  3. Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\buecher-Verzeichnis das SOAP-Webservice-Interface: BuecherSoapServiceIntf.java

    package de.meinefirma.meinprojekt.buecher;
    
    import javax.jws.*;
    
    /** Dienst-Interface */
    @WebService
    public interface BuecherSoapServiceIntf
    {
       BuecherTO createBuch(   @WebParam( name = "buch" ) BuchDO buch ) throws Exception;
       BuecherTO getBuchById(  @WebParam( name = "id"   ) Long   id   );
       BuecherTO findeBuecher( @WebParam( name = "buch" ) BuchDO buch );
    }
    
  4. Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\buecher-Verzeichnis die SOAP-Webservice-Implementierung: BuecherSoapServiceImpl.java

    package de.meinefirma.meinprojekt.buecher;
    
    import javax.jws.WebService;
    import de.meinefirma.meinprojekt.dao.*;
    
    /** Dienstimplementierung */
    @WebService( endpointInterface="de.meinefirma.meinprojekt.buecher.BuecherSoapServiceIntf" )
    public class BuecherSoapServiceImpl implements BuecherSoapServiceIntf
    {
       private BuchDoDAO dao = BuchDoDAO.getInstance();
    
       @Override public BuecherTO createBuch( BuchDO bu ) throws Exception
       {
          bu = dao.createBuch( bu );
          return BuecherUtil.erzeugeBuecherTO( "Buch hinzugefuegt", bu );
       }
    
       @Override public BuecherTO getBuchById( Long id )
       {
          BuchDO bu = dao.getBuchById( id );
          return BuecherUtil.erzeugeBuecherTO( "Buch mit ID " + id, bu );
       }
    
       @Override public BuecherTO findeBuecher( BuchDO bu )
       {
          return BuecherUtil.findeBuecher( bu.getId(), bu.getIsbn(), bu.getTitel() );
       }
    }
    
  5. Ersetzen Sie im src\main\webapp\WEB-INF-Verzeichnis den Inhalt der web.xml durch:

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             id="WebApp_ID" version="2.5">
      <display-name>JaxRsJaxWsPerformance</display-name>
      <listener>
        <listener-class>com.sun.xml.ws.transport.http.servlet.WSServletContextListener</listener-class>
      </listener>
      <servlet>
        <servlet-name>JaxWsServlet</servlet-name>
        <servlet-class>com.sun.xml.ws.transport.http.servlet.WSServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
      </servlet>
      <servlet>
        <display-name>JAX-RS REST Servlet</display-name>
        <servlet-name>REST-Servlet</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
      </servlet>
      <servlet-mapping>
        <servlet-name>JaxWsServlet</servlet-name>
        <url-pattern>/ws/*</url-pattern>
      </servlet-mapping>
      <servlet-mapping>
        <servlet-name>REST-Servlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
      </servlet-mapping>
      <welcome-file-list>
        <welcome-file>index.html</welcome-file>
      </welcome-file-list>
    </web-app>
    
  6. Erzeugen Sie im src\main\webapp\WEB-INF-Verzeichnis den JAX-WS-Deskriptor: sun-jaxws.xml

    <endpoints version="2.0" xmlns="http://java.sun.com/xml/ns/jax-ws/ri/runtime">
      <endpoint name="BuecherSoapService"
        implementation="de.meinefirma.meinprojekt.buecher.BuecherSoapServiceImpl"
        url-pattern="/ws/BuecherSoapService" />
    </endpoints>
    
  7. Erzeugen Sie im src\main\java\de\meinefirma\meinprojekt\client-Verzeichnis die für REST und SOAP gemeinsame Performance-Test-Klasse: BuecherRestAndSoapPerfTestClient.java

    package de.meinefirma.meinprojekt.client;
    
    import java.net.URL;
    import java.text.DecimalFormat;
    import java.util.Random;
    import javax.xml.namespace.QName;
    import javax.xml.ws.*;
    import com.sun.jersey.api.client.*;
    import de.meinefirma.meinprojekt.buecher.*;
    import de.meinefirma.meinprojekt.dao.BuecherUtil;
    
    /** Performance-Testclient sowohl fuer den REST- als auch fuer den SOAP-Webservice */
    public class BuecherRestAndSoapPerfTestClient
    {
       public static void main( final String[] args ) throws Exception
       {
          int    anzahlBuecher = 100;
          String urlBasis      = "http://localhost:8080/JaxRsBuecherverwaltung";
          switch( Math.min( 2, args.length ) ) {
             case 2: urlBasis      = args[1]; // $FALL-THROUGH$
             case 1: anzahlBuecher = Integer.parseInt( args[0] );
          }
          // SOAP- und REST-SUT (System under Test):
          test( new SoapSut(), "SOAP", urlBasis + "/ws/BuecherSoapService", anzahlBuecher, true );
          test( new RestSut(), "REST", urlBasis + "/rest/Artikel/Buecher",  anzahlBuecher, true );
       }
    
       public static BuecherTO test( SutIntf sut, String testName, String url, int anzahlBuecher, boolean trace ) throws Exception
       {
          System.out.println( "\n" + testName + ": " + url + "\n" );
          Long[] ids = new Long[anzahlBuecher];
          sut.initialize( url );
          System.gc();
    
          // Anlage von Buechern:
          if( trace ) System.out.println( "\n" + testName + ": Starte Anlage von " + anzahlBuecher + " Buechern" );
          long startZeit = System.nanoTime();
          for( int i = 0; i < anzahlBuecher; i++ ) {
             BuchDO bu = BuecherUtil.erzeugeBuchDO( null, "" + (1000000000L + i), "Buch " + i, new Double( i ) );
             BuecherTO bueTO = sut.createBuch( bu );
             ids[i] = bueTO.getResults().get( 0 ).getId();
          }
          String s1 = "\nAnlage von " + anzahlBuecher + " Buechern dauert: " + ermittleDauer( startZeit );
    
          // Auslesen von Buechern in einzelnen Lesevorgaengen:
          if( trace ) System.out.println( testName + ": Starte einzelnes Auslesen" );
          startZeit = System.nanoTime();
          for( int i = 0; i < anzahlBuecher; i++ ) {
             Long id = ids[(new Random()).nextInt( anzahlBuecher )];
             BuecherTO bueTO = sut.getBuchById( id );
             if( bueTO == null || bueTO.getResults() == null || bueTO.getResults().size() != 1 ) {
                throw new RuntimeException( "Fehler beim Auslesen des Buches mit der ID " + id );
             }
          }
          String s2 = "\nEinzelnes Auslesen von " + anzahlBuecher + " Buechern dauert: " + ermittleDauer( startZeit );
    
          // Auslesen aller Buecher in einem Lesevorgang:
          if( trace ) System.out.println( testName + ": Starte gemeinsames Auslesen" );
          startZeit = System.nanoTime();
          BuecherTO bueTO = sut.findeBuecher( new BuchDO() );
          String s3 = "\nGemeinsames Auslesen von " + bueTO.getResults().size() + " Buechern dauert: " + ermittleDauer( startZeit );
    
          // Ausgabe:
          System.out.println( "\n" + testName + ": " + s1 + s2 + s3 + "\n" );
          return bueTO;
       }
    
       static String zeigeErgebnis( BuecherTO bueTO )
       {
          StringBuffer sb = new StringBuffer();
          sb.append( "\n" + bueTO.getMessage() + "\n" );
          for( BuchDO bu : bueTO.getResults() )
             sb.append( "    Buch (ID=" + bu.getId() + ", ISBN=" + bu.getIsbn() + ", Titel=" + bu.getTitel() + ", Preis=" + bu.getPreis() + ")\n" );
          sb.append( "    Returncode " + bueTO.getReturncode() + "\n" );
          return sb.toString();
       }
    
       static String ermittleDauer( long startZeitNanoSek )
       {
          long dauerMs = (System.nanoTime() - startZeitNanoSek) / 1000 / 1000;
          if( dauerMs < 1000 ) return "" + dauerMs + " ms";
          return (new DecimalFormat( "#,##0.00" )).format( dauerMs / 1000. ) + " s";
       }
    }
    
    // REST-Implementierung des "System under Test":
    class RestSut implements SutIntf
    {
       WebResource webResource;
    
       @Override public void initialize( String url ) throws Exception
       {
          webResource = Client.create().resource( url );
          webResource.get( BuecherTO.class );
          Thread.sleep( 1000 );
       }
    
       @Override public BuecherTO createBuch( BuchDO bu ) throws Exception
       {
          return webResource.type( "text/xml; charset=utf-8" ).accept( "text/xml; charset=utf-8" ).post( BuecherTO.class, bu );
       }
    
       @Override public BuecherTO getBuchById( Long id )
       {
          return webResource.path( "" + id ).get( BuecherTO.class );
       }
    
       @Override public BuecherTO findeBuecher( BuchDO bu )
       {
          return webResource.get( BuecherTO.class );
       }
    }
    
    // SOAP-Implementierung des "System under Test":
    class SoapSut implements SutIntf
    {
       BuecherSoapServiceIntf buecherService;
    
       @Override public void initialize( String url ) throws Exception
       {
          // Zugriff auf den Webservice vorbereiten:
          if( url.startsWith( "direkt" ) ) {
             buecherService = new BuecherSoapServiceImpl();
          } else {
             Service service = null;
             int timeoutSekunden = 20;
             while( service == null ) {
                try {
                   service = Service.create(
                         new URL( url + "?wsdl" ),
                         new QName( "http://buecher.meinprojekt.meinefirma.de/", "BuecherSoapServiceImplService" ) );
                } catch( WebServiceException ex ) {
                   if( timeoutSekunden-- <= 0 ) throw ex;
                   try { Thread.sleep( 1000 ); } catch( InterruptedException e ) {/*ok*/}
                }
             }
             buecherService = service.getPort( BuecherSoapServiceIntf.class );
          }
       }
    
       @Override public BuecherTO createBuch( BuchDO bu ) throws Exception
       {
          return buecherService.createBuch( bu );
       }
    
       @Override public BuecherTO getBuchById( Long id )
       {
          return buecherService.getBuchById( id );
       }
    
       @Override public BuecherTO findeBuecher( BuchDO bu )
       {
          return buecherService.findeBuecher( bu );
       }
    }
    
    // Interface fuer "System under Test":
    interface SutIntf
    {
       void initialize( String url ) throws Exception;
       BuecherTO createBuch( BuchDO bu ) throws Exception;
       BuecherTO getBuchById( Long id );
       BuecherTO findeBuecher( BuchDO bu );
    }
    
  8. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\JaxRsJaxWsPerformance]
     |- [src]
     |   '- [main]
     |       |- [java]
     |       |   '- [de]
     |       |       '- [meinefirma]
     |       |           '- [meinprojekt]
     |       |               |- [buecher]
     |       |               |   |- BuchDO.java
     |       |               |   |- BuecherSoapServiceImpl.java
     |       |               |   |- BuecherSoapServiceIntf.java
     |       |               |   '- BuecherTO.java
     |       |               |- [client]
     |       |               |   |- BuecherRestAndSoapPerfTestClient.java
     |       |               |   '- BuecherRestClient.java
     |       |               |- [dao]
     |       |               |   |- BuchDoDAO.java
     |       |               |   '- BuecherUtil.java
     |       |               '- [rest]
     |       |                   '- BuecherRestService.java
     |       '- [webapp]
     |           |- [WEB-INF]
     |           |   |- sun-jaxws.xml
     |           |   '- web.xml
     |           '- index.html
     |- pom.xml
     |- run-Client.bat
     |- run-Curl.bat
     |- run-RestSoapPerf.bat
     '- run-Web.bat
    

    Wenn Sie wie gezeigt die SOAP-Service-Klassen im buecher-Package speichern, können Sie das Beispiel leicht für andere neue generierte Klassen erweitern ("Contract-First"):
    Falls Sie eine Schema-XSD-Datei haben, können Sie BuchDO.java und BuecherTO.java mit xjc generieren.
    Falls Sie eine WSDL-Datei haben, können Sie BuchDO.java, BuecherTO.java und BuecherSoapServiceIntf.java mit wsimport generieren.

  9. Die Batchdateien run-*.bat sind im Source-Download enthalten.

  10. Starten Sie Tomcat, kopieren Sie die WAR-Datei in das Tomcat-webapps-Verzeichnis und starten Sie den Test (siehe auch run-RestSoapPerf.bat):

    cd \MeinWorkspace\JaxRsJaxWsPerformance

    mvn package

    copy target\JaxRsBuecherverwaltung.war D:\Tools\Tomcat\webapps

    Ca. 20 Sekunden warten, bis Deployment fertig.

    set CLASSPATH=target/JaxRsBuecherverwaltung/WEB-INF/classes;target/JaxRsBuecherverwaltung/WEB-INF/lib/*

    java de.meinefirma.meinprojekt.client.BuecherRestAndSoapPerfTestClient 1000 http://localhost:8080/JaxRsBuecherverwaltung

  11. Je nach Geschwindigkeit Ihres PCs bzw. Ihres Netzwerks erhalten Sie sehr unterschiedliche Ergebnisse. Aber die Unterschiede zwischen REST und SOAP sind gering und eher zufällig, weshalb die folgende Tabelle nur verschiedene Anbindungen zeigt:

    AnbindungAnlage von
    1000 Büchern
    Einzelnes Auslesen
    von 1000 Büchern
    Gemeinsames Auslesen
    von 1000 Büchern
    direkt (ohne REST/SOAP)30 ms5 ms0,1 ms
    REST/SOAP auf lokalem PC ("localhost")0,9 s0,5 s10 ms
    REST/SOAP remote über Netzwerk2 s1,2 s30 ms
  12. Falls Sie eine Exception erhalten, suchen Sie in der \Tools\Tomcat\logs\localhost.*.log-Datei die ursprüngliche Exception.



HelloWorld-Programmierbeispiel eines RESTful-Webservices mit Tomcat und Eclipse, ohne Maven

Alle obigen Beispiele, die Maven verwenden, können Sie auch mit Eclipse verwenden: Bereiten Sie Eclipse vor wie unter maven.htm#Eclipse beschrieben: Führen Sie entweder das Kommando "mvn eclipse:eclipse" aus oder verwenden Sie M2Eclipse, und laden Sie die Projekte in Eclipse.

Falls Sie nicht Maven verwenden wollen, können Sie auch wie im Folgenden beschrieben die Projekte ohne Maven und mit Eclipse (und Tomcat) aufsetzen.

Dynamic Web Project und Web Module Settings

  1. Installieren Sie Java, Eclipse und Tomcat wie beschrieben unter: jee-tomcat-eclipse.htm#Installation.
  2. Starten Sie in Eclipse ein neues Web-Projekt über:
    'File' | 'New' | 'Project...' | '[+] Web' | 'Dynamic Web Project' | 'Next >'.

  3. Tragen Sie ein:

    Project name: JaxRsMitEclipse
    Use default: Ja
    Target Runtime: Apache Tomcat v8.0
    Configurations: Default Configuration for Apache Tomcat v8.0
    Add project to an EAR: Nein

    'Next >'

  4. Falls der Project Facets Dialog erscheint: Bei 'Java' sollte mindestens '5.0' eingetragen sein.
    'Next >'

  5. Tragen Sie in den folgenden Dialogen ein:

    Java Source folders: src
    Java Default output folder: build\classes
    Context Root: JaxRsMitEclipse
    Content Directory: WebContent
    Generate web.xml Deployment Descriptor: Ja

    'Finish'

Jersey

  1. Downloaden Sie jersey-archive-1.19.1.zip von http://jersey.java.net (Sie können auch eine aktuellere Version verwenden, aber wenn Sie später Ihre REST-Lösung auf Java EE Application Servern wie z.B. WebLogic einsetzen wollen, beinhaltet dieser eventuell nur diese Version).

    Entzippen Sie das Jersey-Archiv in ein temporäres Verzeichnis und kopieren Sie die fünf .jar-Libraries jersey-client-1.19.1.jar, jersey-core-1.19.1.jar, jersey-server-1.19.1.jar, jersey-servlet-1.19.1.jar und jsr311-api-1.1.1.jar (oder alle Libs) aus dem jersey-archive-1.19.1/lib-Verzeichnis in das JaxRsMitEclipse/WebContent/WEB-INF/lib-Verzeichnis.

  2. Klicken Sie im Eclipse Project Explorer mit der rechten Maustaste auf den 'JaxRsMitEclipse'-Projektnamen und wählen Sie 'Refresh' (oder betätigen Sie 'F5'), damit Eclipse die .jar-Libraries registriert.

web.xml

  1. Öffnen Sie im Eclipse Project Explorer das 'JaxRsMitEclipse'-Projekt und darunter den 'WebContent'-Zweig und das 'WEB-INF'-Verzeichnis. Öffnen Sie die web.xml, schalten Sie eventuell von der 'Design'-Ansicht auf die 'Source'-Ansicht um, und ersetzen Sie den Inhalt durch:

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             id="WebApp_ID" version="2.5">
      <display-name>JaxRsMitEclipse</display-name>
      <servlet>
        <display-name>JAX-RS REST Servlet</display-name>
        <servlet-name>REST-Servlet</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
      </servlet>
      <servlet-mapping>
        <servlet-name>REST-Servlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
      </servlet-mapping>
      <welcome-file-list>
        <welcome-file>index.html</welcome-file>
      </welcome-file-list>
    </web-app>
    

HTML

  1. Klicken Sie im Eclipse Project Explorer mit der rechten Maustaste auf den 'JaxRsMitEclipse'-Projektnamen und wählen Sie 'New' | 'Other...' | 'Web' | 'HTML File' | 'File name' = 'index.html' | 'Finish'.

  2. Ändern Sie den Inhalt von index.html zu:

    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
      <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
      <title>Meine RESTful-Webservice-Startseite</title>
    </head>
    <body>
      <h3> Meine RESTful-Webservice-Startseite </h3>
      <p> <a href="/JaxRsMitEclipse/rest/application.wadl">/JaxRsMitEclipse/rest/application.wadl</a> </p>
      <p> <a href="/JaxRsMitEclipse/rest/helloworld?name=ich">/JaxRsMitEclipse/rest/helloworld?name=ich</a> </p>
    </body>
    </html>
    

RESTful-Webservice

  1. Klicken Sie im Eclipse Project Explorer mit der rechten Maustaste auf den 'JaxRsMitEclipse'-Projektnamen, wählen Sie 'New' | 'Package' und tragen Sie ein:

    Source folder: JaxRsMitEclipse/src
    Name: minirestwebservice

    'Finish'

  2. Öffnen Sie im Eclipse Project Explorer das 'JaxRsMitEclipse'-Projekt und darunter den 'Java Resources: src'-Zweig. Klicken Sie mit der rechten Maustaste auf den Package-Namen 'minirestwebservice', wählen Sie 'New' | 'Class' und tragen Sie ein:

    Source folder: JaxRsMitEclipse/src
    Package: minirestwebservice
    Name: HalloWeltService

    'Finish'

  3. Ändern Sie den Inhalt von HalloWeltService.java zu:

    package minirestwebservice;
    
    import javax.ws.rs.*;
    import javax.ws.rs.core.MediaType;
    
    @Path( "/helloworld" )
    public class HalloWeltService
    {
       @GET @Produces( MediaType.TEXT_PLAIN )
       public String halloText( @QueryParam("name") String name )
       {
          return "Hallo " + name;
       }
    
       @GET @Produces( MediaType.TEXT_HTML )
       public String halloHtml( @QueryParam("name") String name )
       {
          return "<html><title>HelloWorld</title><body><h2>Html: Hallo " + name + "</h2></body></html>";
       }
    }
    
  4. Klicken Sie im Eclipse Project Explorer mit der rechten Maustaste auf den 'JaxRsMitEclipse'-Projektnamen und wählen Sie 'Run As' | 'Run on Server'.

  5. Falls der Dialog 'Define a New Server' oder 'Run On Server' erscheint:
    Wählen Sie 'Tomcat v8.0 Server at localhost' und aktivieren Sie die Checkbox 'Always use this server when running this project'. 'Finish'.

  6. Falls Sie danach gefragt werden: Aktivieren Sie 'Update context root ...'.

  7. Falls Tomcat nicht automatisch gestartet wurde:
    Klicken Sie unten auf den 'Servers'-Tabulatorreiter und starten Sie Tomcat (rechte Maustaste auf 'Tomcat v8.0 Server at localhost'-Eintrag oder über den grünen Kreis-Button).

  8. Lassen Sie sich über http://localhost:8080/JaxRsMitEclipse/rest/application.wadl die WADL-Schnittstellenbeschreibung anzeigen (Web Application Description Language). Da der HalloWeltService-Dienst identisch zu obigem HalloWeltService vom HelloWorld-Beispiel ist, ist auch die WADL-Datei sehr ähnlich zu obiger WADL-Datei.

  9. Lesen Sie die HelloWorld-Ausgabe:
    http://localhost:8080/JaxRsMitEclipse/rest/helloworld?name=ich

    Je nach Lese-Tool bzw. Browser erhalten Sie entweder die Plain-Text- oder die HTML-Ausgabe.

  10. Falls Sie die URL-Bestandteile ändern wollen:
    "JaxRsMitEclipse" können Sie ändern über: Rechtsklick im Eclipse Project Explorer auf den 'JaxRsMitEclipse'-Projektnamen, 'Properties', 'Web Project Settings', 'Context Root'.
    "rest" können Sie ändern über 'url-pattern' in: <MeinWorkspace>\JaxRsMitEclipse\WebContent\WEB-INF\web.xml.
    "helloworld" können Sie ändern über '@Path( "/helloworld" )' in: <MeinWorkspace>\JaxRsMitEclipse\src\minirestwebservice\HalloWeltService.java.

Java-Client für den RESTful-Webservice

Mit Jersey lassen sich sehr einfach Java-Clients für RESTful-Webservices programmieren. Das folgende Beispiel liest sowohl die Text- als auch die HTML-Ausgabe des obigen HelloWorld-Beispiels.

  1. Erzeugen Sie im Package minirestwebservice die Java-Klasse HalloWeltClient.java:

    package minirestwebservice;
    
    import javax.ws.rs.core.MediaType;
    import com.sun.jersey.api.client.*;
    
    public class HalloWeltClient
    {
       public static void main( String[] args )
       {
          String url = ( args.length > 0 ) ? args[0] : "http://localhost:8080/JaxRsMitEclipse/rest";
          String nam = ( args.length > 1 ) ? args[1] : "ich";
          url = url + "/helloworld?name=" + nam;
          System.out.println( "URL: " + url );
    
          WebResource wrs = Client.create().resource( url );
    
          System.out.println( "\nTextausgabe:" );
          System.out.println( wrs.accept( MediaType.TEXT_PLAIN ).get( String.class ) );
          System.out.println( "\nHTML-Ausgabe:" );
          System.out.println( wrs.accept( MediaType.TEXT_HTML  ).get( String.class ) );
       }
    }
    
  2. Klicken Sie im Eclipse Project Explorer mit der rechten Maustaste auf die neu erstellte Java-Klasse HalloWeltClient.java und wählen Sie: 'Run As' | 'Java Application'.

  3. Die Ausgabe sieht folgendermaßen aus:

    Textausgabe:
    Hallo ich
    
    HTML-Ausgabe:
    <html><title>HelloWorld</title><body><h2>Html: Hallo ich</h2></body></html>
    
  4. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\JaxRsMitEclipse]
     |- ...
     |- [build]
     |   '- [classes]
     |       '- ...
     |- [src]
     |   '- [minirestwebservice]
     |       |- HalloWeltClient.java
     |       '- HalloWeltService.java
     '- [WebContent]
         |- [META-INF]
         |   '- MANIFEST.MF
         |- [WEB-INF]
         |   '- [lib]
         |   |   |- jersey-client-1.19.1.jar
         |   |   |- jersey-core-1.19.1.jar
         |   |   |- jersey-server-1.19.1.jar
         |   |   |- jersey-servlet-1.19.1.jar
         |   |   '- jsr311-api-1.1.1.jar
         |   '- web.xml
         '- index.html
    

Client-Tools

Natürlich können Sie auch für dieses Beispiel wieder cURL, Wget und die Webbrowser-Client-Tools Firebug, RESTClient und Poster einsetzen.



JUnit-Test mit "embedded GlassFish"

Im folgenden Beispiel wird für den Server statt Grizzly der embedded GlassFish verwendet. Sie brauchen hierfür GlassFish nicht zu installieren. Innerhalb des JUnit-Tests wird embedded GlassFish gestartet, das Deployment durchgeführt, Tests ausgeführt und embedded GlassFish wieder heruntergefahren.

Das Beispiel erweitert obiges JaxRsContractFirstService-Beispiel.

  1. Führen Sie in Ihrem Projekte-Workspace-Verzeichnis folgende Kommandos aus:

    cd \MeinWorkspace

    xcopy JaxRsContractFirstService JaxRsEmbeddedGlassFish\ /S

    cd JaxRsEmbeddedGlassFish

  2. Ersetzen Sie im neuen JaxRsEmbeddedGlassFish-Projektverzeichnis den Inhalt der pom.xml durch:

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>minirestwebservice</groupId>
      <artifactId>JaxRsEmbeddedGlassFish</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
      <name>${project.artifactId}</name>
      <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
              <source>1.7</source>
              <target>1.7</target>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-client</artifactId>
          <version>1.1.5.2</version>
        </dependency>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-server</artifactId>
          <version>1.1.5.2</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.extras</groupId>
          <!-- Falls Sie zusaetzlich zum Webcontainer weitere GlassFish-Module benoetigen:
               "glassfish-embedded-all" statt "glassfish-embedded-web" verwenden: -->
          <artifactId>glassfish-embedded-web</artifactId>
          <!-- 3.1 funktioniert nicht: -->
          <version>3.0.1</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    </project>
    
  3. Ersetzen Sie im src\main\webapp\WEB-INF-Verzeichnis den Inhalt der web.xml durch:

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             id="WebApp_ID" version="2.5">
      <display-name>REST</display-name>
      <servlet>
        <display-name>JAX-RS REST Servlet</display-name>
        <servlet-name>REST-Servlet</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
        <init-param>
          <param-name>com.sun.jersey.config.property.packages</param-name>
          <param-value>contractfirstservice</param-value>
        </init-param>
      </servlet>
      <servlet-mapping>
        <servlet-name>REST-Servlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
      </servlet-mapping>
    </web-app>
    
  4. Ersetzen Sie im src\test\java\contractfirstservice-Verzeichnis den Inhalt der ContractfirstServiceTest.java durch:

    package contractfirstservice;
    
    import java.io.File;
    import java.net.URI;
    import javax.xml.bind.JAXBElement;
    import org.glassfish.api.deployment.DeployCommandParameters;
    import org.glassfish.api.embedded.*;
    import org.junit.*;
    import com.sun.jersey.api.client.*;
    import contractfirstgenerated.*;
    
    public class ContractfirstServiceTest
    {
       private Server           glassfish;
       private EmbeddedDeployer deployer;
       private WebResource      wrs; // (WebResource ist thread safe)
    
       @Before
       public void setUp() throws Exception
       {
          // Embedded GlassFish Server:
          final URI    BASE_URI = new URI( "http://localhost:4434/JaxRsEmbeddedGlassFish/rest" );
          final int    PORT     = 4434;
          final String NAME     = "JaxRsEmbeddedGlassFish";
          glassfish = (new Server.Builder( NAME )).build();
          glassfish.addContainer( glassfish.createConfig( ContainerBuilder.Type.web ) );
          glassfish.createPort( PORT );
          glassfish.start();
    
          // Deploy WAR:
          ScatteredArchive.Builder war = new ScatteredArchive.Builder( NAME, new File( "target/" ) );
          war.resources(    new File( "src/main/webapp" ) );
          war.addMetadata(  new File( "src/main/webapp/WEB-INF/web.xml" ) );
          war.addClassPath( new File( "target/classes" ).toURI().toURL() );
          DeployCommandParameters params = new DeployCommandParameters();
          params.name = NAME;
          deployer = glassfish.getDeployer();
          deployer.deploy( war.buildWar(), params );
    
          // Client:
          wrs = Client.create().resource( BASE_URI );
       }
    
       @After
       public void tearDown() throws Exception
       {
          deployer.undeployAll();
          glassfish.stop();
       }
    
       @Test
       public void testContractfirstServiceMitEmbeddedGlassFish()
       {
          // Mit JAXB und mit bequemen Java-Objekten:
          InputTO inpTO = new InputTO();
          inpTO.setI( 42 );
          inpTO.setS( "abc xyz" );
          JAXBElement<InputTO> inpJaxb = (new ObjectFactory()).createInputTO( inpTO );
          ResultTO resTO = wrs.path( "contractfirst" ).type( "text/xml; charset=utf-8" ).accept( "text/xml; charset=utf-8" ).post(
                               ResultTO.class, inpJaxb );
          Assert.assertEquals(  84,             resTO.getI() );
          Assert.assertEquals( "abc xyz - ret", resTO.getS() );
    
          // Ohne JAXB und mit XML-Strings (Namespace beachten):
          String resXml = wrs.path( "contractfirst" ).type( "text/xml; charset=utf-8" ).accept( "text/xml; charset=utf-8" ).post(
                              String.class, "<inputTO xmlns='mein.ns'><i>42</i><s>abc xyz</s></inputTO>" );
          Assert.assertEquals( "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" +
                               "<resultTO xmlns=\"mein.ns\"><i>84</i><s>abc xyz - ret</s></resultTO>", resXml );
       }
    }
    
  5. Führen Sie den JUnit-Test aus:

    mvn test

    Es erscheint:

    ...
    INFO: GlassFish v3 ... startup ...
    ...
    INFO: Loading application JaxRsEmbeddedGlassFish at /JaxRsEmbeddedGlassFish
    ...
    com.sun.jersey.api.core.PackagesResourceConfig init
    INFO: Scanning for root resource and provider classes in the packages:
      contractfirstservice
    ...
    INFO: Root resource classes found:
      class contractfirstservice.ContractfirstService
    ...
    Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
    ...
    [INFO] BUILD SUCCESS
    
  6. Falls Sie in der tearDown()-Methode vor dem Undeploy eine Warteschleife hinzufügen (z.B. Thread.sleep( 20000 );), können Sie den REST-Service auch mit cURL ansprechen:

    curl -i -H Content-type:text/xml --request POST -d "<inputTO xmlns='mein.ns'><i>42</i><s>abc xyz</s></inputTO>" "http://localhost:4434/JaxRsEmbeddedGlassFish/rest/contractfirst"

    curl -i "http://localhost:4434/JaxRsEmbeddedGlassFish/rest/application.wadl"

  7. Falls Sie nicht GlassFish, sondern stattdessen JBoss verwenden wollen, sollten Sie sich die Doku unter http://www.jboss.org/shrinkwrap und http://community.jboss.org/wiki/ShrinkWrap ansehen.



Atom Syndication Format (Atomfeed) mit JAX-RS und Abdera

Vorbemerkung

Im Internetumfeld gibt es zwei unterschiedliche "Atom-Standards":

Im Folgenden wird nur auf das "Atom Syndication Format" eingegangen. Es ist vom IETF im RFC 4287 definiert.

Ein Webfeed erleichtert es Clients zu überprüfen, ob es Aktualisierungen oder neue Informationen gibt. Dazu stellt der Server in seinem Webfeed in einem standardisierten und maschinenlesbaren XML-Format eine Liste der letzten Informationen zur Verfügung und erweitert bei neuen Information diese Liste. Beim Atom Syndication Format gibt es das XML-Root-Element "feed" mit einigen Meta-Informationen zum gesamten Webfeed sowie mit beliebig vielen "entry"-Elementen, welche jeweils im "content"-Element den eigentlichen Nutzinhalt enthalten. Der Nutzinhalt kann entweder ein Link auf die eigentliche Ressource sein (z.B. für Bilder oder Streams) oder den Inhalt direkt "inline" enthalten (z.B. als Text, HTML, XHTML, XML, JSON). Für den Nutzinhalt kann ein eigener Mediatyp und eigener Namespace verwendet werden. Die "entry"-Elemente sind meistens zeitlich sortiert (zuerst die aktuellsten Elemente). Wenn die Anzahl der "entry"-Elemente zu groß wird, können ältere "entry"-Elemente in "Archive-Feeds" ausgelagert werden, auf die im aktuellen "Working-Feed" (bzw. im Vorgänger-Archive-Feed) per Link mit dem rel-Attribut "prev-archive" verwiesen wird.

Webfeeds im Atom Syndication Format werden von den meisten Webbrowsern verstanden und können "abonniert" werden, damit der Benutzer leichter aktuelle Informationen findet. Beispiele:

Google News:  http://news.google.com/news?ned=us&topic=h&output=atom
Heise-News:  http://www.heise.de/newsticker/heise-atom.xml

Darüberhinaus kann das Atom Syndication Format auch als Kommunikationsschnittstelle zwischen technischen Systemen verwendet werden. Während reine HTTP- und REST-Aufrufe synchron arbeiten, können mit der Kombination aus REST und Atomfeeds leicht asynchrone Kommunikationsmuster abgebildet werden. So kann unter bestimmten Umständen auf den Einsatz von Message-orientierter Middleware verzichtet werden und trotzdem hohe Entkopplung durch asynchrone Kommunikation erreicht werden.

Die folgenden Beispiele nutzen Abdera, siehe hierzu:
http://abdera.apache.org
http://abdera.apache.org/docs/api/index.html?org/apache/abdera/Abdera.html
http://abdera.apache.org/docs/api/index.html?org/apache/abdera/model/package-summary.html
https://cwiki.apache.org/confluence/display/ABDERA/Creating+and+Consuming+Atom+Documents.

Beispiel eines einfachen Atomfeeds

<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>http://meinefirma.de:4434/atomfeed</id>
  <updated>2011-01-02T08:47:11.000Z</updated>
  <title type="text">Mein Feed-Titel</title>
  <subtitle type="text">Mein Feed-Untertitel</subtitle>
  <author><name>Mein Name</name></author>
  <link href="http://meinefirma.de:4434/atomfeed" rel="self" />
  <entry>
    <id>http://meinefirma.de:4434/atomfeed/entry/4711</id>
    <updated>2011-01-02T08:47:11.000Z</updated>
    <title type="text">Titel zum Entry mit der ID 4711</title>
    <content type="text">Hier ist der Inhalt des Entrys mit der ID 4711</content>
    <author><name>Mein Name</name></author>
  </entry>
  <entry>
    <id>http://meinefirma.de:4434/atomfeed/entry/42</id>
    <updated>2011-01-02T08:00:42.000Z</updated>
    <title type="text">Titel zum Entry mit der ID 42</title>
    <content type="text">Hier ist der Inhalt des Entrys mit der ID 42</content>
    <author><name>Mein Name</name></author>
  </entry>
</feed>

Projektvorbereitung für die folgenden Beispiele

  1. Führen Sie im Kommandozeilenfenster aus:

    md \MeinWorkspace\AtomAbdera\src\main\webapp\WEB-INF

    md \MeinWorkspace\AtomAbdera\src\main\java\atomfeed

    md \MeinWorkspace\AtomAbdera\src\test\java\atomfeed

    cd \MeinWorkspace\AtomAbdera

  2. Erzeugen Sie im AtomAbdera-Projektverzeichnis die Maven-Projektkonfiguration: pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>de.meinefirma.meinprojekt</groupId>
      <artifactId>AtomAbdera</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
      <name>${project.artifactId}</name>
      <build>
        <finalName>${project.artifactId}</finalName>
      </build>
      <dependencies>
        <dependency>
          <groupId>org.apache.abdera</groupId>
          <artifactId>abdera-parser</artifactId>
          <version>1.1.3</version>
        </dependency>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-client</artifactId>
          <version>1.19.1</version>
        </dependency>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-server</artifactId>
          <version>1.19.1</version>
        </dependency>
        <dependency>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-grizzly2</artifactId>
          <version>1.19.1</version>
        </dependency>
        <dependency>
          <!-- Version muss zu "com.sun.jersey:jersey-grizzly2" passen, siehe "mvn dependency:tree" -->
          <groupId>org.glassfish.grizzly</groupId>
          <artifactId>grizzly-http-all</artifactId>
          <version>2.2.16</version>
        </dependency>
        <dependency>
          <!-- Wird benoetigt, um folgende Exception zu vermeiden:
               org.xml.sax.SAXNotRecognizedException: Feature 'http://javax.xml.XMLConstants/feature/secure-processing' is not recognized -->
          <groupId>xerces</groupId>
          <artifactId>xercesImpl</artifactId>
          <version>2.11.0</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    </project>
    
  3. Erzeugen Sie im src\main\webapp\WEB-INF-Verzeichnis die Webapp-Konfigurationsdatei: web.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
             id="WebApp_ID" version="2.5">
      <display-name>REST</display-name>
      <servlet>
        <display-name>JAX-RS REST Servlet</display-name>
        <servlet-name>REST-Servlet</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
      </servlet>
      <servlet-mapping>
        <servlet-name>REST-Servlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
      </servlet-mapping>
    </web-app>
    

Atomfeed lesen

  1. Erzeugen Sie im src\test\java\atomfeed-Verzeichnis die Atomfeed-Anzeigeklasse: AtomFeedPrint.java

    package atomfeed;
    
    import java.io.*;
    import java.net.*;
    import java.net.URL;
    import java.util.Iterator;
    import org.apache.abdera.Abdera;
    import org.apache.abdera.model.*;
    import org.apache.abdera.parser.Parser;
    import org.apache.abdera.writer.Writer;
    
    public class AtomFeedPrint
    {
       /**
        * Anzeige eines Atomfeeds: wahlweise nur alle Titel oder vollstaendig (z.B. als XML).<br>
        * @param args Als ersten Kommandozeilenparameter die URL zum Atomfeed uebergeben, z.B.:<br>
        *    {@link "http://localhost:4434/atomfeed"}<br>
        *    {@link "http://news.google.com/news?ned=us&topic=h&output=atom"}<br>
        *    {@link "http://www.heise.de/newsticker/heise-atom.xml"}<br>
        *    Der zweite Parameter kann leer sein ("") oder "PrettyXML"<br>
        *    Der dritte Parameter kann leer sein ("") oder "alles" (fuer Titel + Deskription)<br>
        *    Falls Proxy: Als vierten und fuenften Parameter Proxy-Host und -Port uebergeben, z.B. 127.0.0.1 3128
        * Verwendet: {@link "http://abdera.apache.org/docs/api/org/apache/abdera/Abdera.html"}<br>
        */
       @SuppressWarnings("resource")
       public static void main( String[] args ) throws IOException
       {
          String atomfeedUrl = ( args.length > 0 ) ? args[0] : "http://localhost:4434/atomfeed";
          String prettyxml   = ( args.length > 1 ) ? args[1] : null; // z.B. PrettyXML
          String alles       = ( args.length > 2 ) ? args[2] : null; // z.B. alles
          String proxyHost   = ( args.length > 3 ) ? args[3] : null; // z.B. 127.0.0.1
          String proxyPort   = ( args.length > 4 ) ? args[4] : null; // z.B. 3128
          boolean all = alles != null && alles.trim().toLowerCase().startsWith( "all" );
    
          Abdera      abdera = new Abdera();
          Parser      parser = abdera.getParser();
          URL         url = new URL( atomfeedUrl );
          InputStream inp = null;
    
          if( proxyHost != null && proxyPort != null ) {
             Proxy proxy = new Proxy( Proxy.Type.HTTP, new InetSocketAddress( proxyHost, Integer.parseInt( proxyPort ) ) );
             inp = url.openConnection( proxy ).getInputStream();
          } else {
             inp = url.openStream();
          }
    
          try {
             Document<Element> elemDoc  = parser.parse( inp, url.toString() );
             Element elem = elemDoc.getRoot();
    
             if( elem instanceof Feed ) {
                Feed feed = (Feed) elem;
                System.out.println( "\n\n---- Nur Titel ----\n" );
                System.out.println( feed.getTitle() );
                for( Entry entry : feed.getEntries() ) {
                   System.out.println( "\t" + entry.getTitle() );
                }
                if( all ) {
                   System.out.println( "\n\n---- Vollstaendig ----\n" );
                   if( prettyxml != null && prettyxml.trim().length() == 0 ) { prettyxml = null; }
                   Writer writer = abdera.getWriterFactory().getWriter( prettyxml );
                   writer.writeTo( feed, System.out );
                }
             } else {
                int i0, i1, i2;
                Iterator<Element> it0, it1, it2;
                for( i0 = 0, it0 = elem.iterator(); it0.hasNext() && i0++ < 50; ) {
                   for( i1 = 0, it1 = printNextElement( it0, all, "" ); it1 != null && it1.hasNext() && i1++ < 100; ) {
                      for( i2 = 0, it2 = printNextElement( it1, all, "" ); it2 != null && it2.hasNext() && i2++ < 50; ) {
                         printNextElement( it2, all, "    " );
                      }
                   }
                }
             }
          } finally {
             if( inp != null ) { inp.close(); }
          }
       }
    
       private static Iterator<Element> printNextElement( Iterator<Element> it, boolean all, String prefix )
       {
          if( it == null ) { return null; }
          Element el = it.next();
          if(              "title".equals( el.getQName().toString() ) ) { System.out.println( prefix + el.getText() ); }
          if( all && "description".equals( el.getQName().toString() ) ) { System.out.println( prefix + el.getText() + "\n" ); }
          return el.iterator();
       }
    }
    

    In diesem Beispiel werden nur der feed- und die entry-Titel ausgelesen, aber analog können Sie auf jedes beliebige Atom-Element zugreifen.

  2. Führen Sie im Kommandozeilenfenster aus (falls eines der Kommandos kein Ergebnis liefert, hat der Anbieter wahrscheinlich seinen Dienst entweder eingestellt oder geändert):

    cd \MeinWorkspace\AtomAbdera

    mvn clean package

    java -cp target/test-classes;target/AtomAbdera/WEB-INF/lib/* atomfeed.AtomFeedPrint "http://news.google.com/news?ned=us&topic=h&output=atom" PrettyXML

    java -cp target/test-classes;target/AtomAbdera/WEB-INF/lib/* atomfeed.AtomFeedPrint "http://www.heise.de/newsticker/heise-atom.xml"

    Testen Sie auch weitere Atomfeed-URLs.

  3. Falls Sie die Exception "java.net.ConnectException: Connection timed out: connect" erhalten, weil Sie sich hinter einer Proxy-Firewall befinden, geben Sie zusätzlich die Proxy-Parameter an, zum Beispiel so:

    java -cp target/test-classes;target/AtomAbdera/WEB-INF/lib/* atomfeed.AtomFeedPrint "http://news.google.com/news?ned=us&topic=h&output=atom" PrettyXML "" 127.0.0.1 3128

    java -cp target/test-classes;target/AtomAbdera/WEB-INF/lib/* atomfeed.AtomFeedPrint "http://www.heise.de/newsticker/heise-atom.xml" "" "" 127.0.0.1 3128

    Alternativ hätten Sie die Proxy-Parameter auch als VM-Argumente übergeben können:

    java -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=3128 ...

Atomfeed-Server

  1. Erzeugen Sie im src\main\java\atomfeed-Verzeichnis die Atomfeed-JAX-RS-Service-Klasse: SimpleAtomFeedService.java

    package atomfeed;
    
    import javax.ws.rs.*;
    import javax.ws.rs.core.*;
    import org.apache.abdera.model.*;
    
    @Path( "atomfeed" )
    public class SimpleAtomFeedService
    {
       @GET @Produces( MediaType.APPLICATION_ATOM_XML )
       public static Feed getFeed( @Context UriInfo uri )
       {
          Feed feed = AbderaJaxRsProvider.getAbdera().getFactory().newFeed();
          SimpleDatenDAO.fillFeedData( feed, uri.getAbsolutePath().toString() );
          for( Long id : SimpleDatenDAO.getListOfIDs() ) {
             Entry entry = feed.addEntry();
             SimpleDatenDAO.fillEntryData( entry, id, uri.getAbsolutePath().toString() );
          }
          return feed;
       }
    
       @Path( "entry/{id}" )
       @GET @Produces( MediaType.APPLICATION_ATOM_XML )
       public static Entry getEntry( @PathParam("id") Long id, @Context UriInfo uri )
       {
          Entry entry = AbderaJaxRsProvider.getAbdera().getFactory().newEntry();
          SimpleDatenDAO.fillEntryData( entry, id, uri.getAbsolutePath().toString() );
          return entry;
       }
    }
    
  2. Erzeugen Sie im src\main\java\atomfeed-Verzeichnis die JAX-RS-Provider-Klasse: AbderaJaxRsProvider.java

    package atomfeed;
    
    import java.io.*;
    import java.lang.annotation.Annotation;
    import java.lang.reflect.Type;
    import javax.ws.rs.*;
    import javax.ws.rs.core.*;
    import javax.ws.rs.ext.*;
    import org.apache.abdera.Abdera;
    import org.apache.abdera.model.*;
    import org.apache.abdera.writer.Writer;
    
    /**
     * JAX-RS-Provider-Klasse, damit JAX-RS die Klassen Feed und Entry lesen und schreiben kann.<br>
     * Siehe auch:<br>
     * org.apache.wink.providers.abdera.AbderaAtomFeedProvider und AbderaAtomEntryProvider
     * sowie<br>
     * org.jboss.resteasy.plugins.providers.atom.AbderaFeedProvider und AbderaEntryProvider
     */
    @Provider
    @Produces( MediaType.APPLICATION_ATOM_XML )
    @Consumes( MediaType.APPLICATION_ATOM_XML )
    public class AbderaJaxRsProvider implements MessageBodyReader<Object>, MessageBodyWriter<Object>
    {
       // Die Abdera-Klasse ist threadsafe:
       private final static Abdera abdera = new Abdera();
    
       public static Abdera getAbdera()
       {
          return abdera;
       }
    
       @Override
       public long getSize( Object obj, Class<?> clss, Type type, Annotation[] annot, MediaType mediaType )
       {
          return -1;
       }
    
       @Override
       public boolean isReadable( Class<?> clss, Type type, Annotation[] annot, MediaType mediaType )
       {
          return (Feed.class.isAssignableFrom( clss ) || Entry.class.isAssignableFrom( clss ));
       }
    
       @Override
       public boolean isWriteable( Class<?> clss, Type type, Annotation[] annot, MediaType mediaType )
       {
          return (Feed.class.isAssignableFrom( clss ) || Entry.class.isAssignableFrom( clss ));
       }
    
       @Override
       public Object readFrom( Class<Object> feedOrEntry, Type type, Annotation[] annot, MediaType mediaType,
             MultivaluedMap<String, String> headers, InputStream inputStream ) throws IOException
       {
          Document<Element> doc = getAbdera().getParser().parse( inputStream );
          Element el = doc.getRoot();
          if( feedOrEntry.isAssignableFrom( el.getClass() ) ) {
             return el;
          }
          throw new IOException( "Falsche Klasse '" + el.getClass().getName() + "' statt '" + feedOrEntry.getName() + "'." );
       }
    
       @Override
       public void writeTo( Object feedOrEntry, Class<?> clss, Type type, Annotation[] annot, MediaType mediaType,
             MultivaluedMap<String, Object> headers, OutputStream outputStream ) throws IOException, WebApplicationException
       {
          Writer writer = getAbdera().getWriterFactory().getWriter( "PrettyXML" );
          writer.writeTo( ( feedOrEntry instanceof Feed ) ? (Feed) feedOrEntry : (Entry) feedOrEntry, outputStream );
       }
    }
    
  3. Erzeugen Sie im src\main\java\atomfeed-Verzeichnis eine simple Datenlieferant-Simulationsklasse: SimpleDatenDAO.java

    package atomfeed;
    
    import java.util.*;
    import org.apache.abdera.model.*;
    
    /**
     * Simple Simulation von Daten-Lieferanten.
     * In der Realitaet koennten die Daten aus einer Datenbank kommen.
     */
    public class SimpleDatenDAO
    {
       public static void fillFeedData( Feed feed, String uri )
       {
          feed.setId(       uri );
          feed.setUpdated(  new Date() );
          feed.setTitle(    "Mein Feed-Titel" );
          feed.setSubtitle( "Mein Feed-Untertitel" );
          feed.addAuthor(   "ich" );
          feed.addLink(     "http://www.torsten-horn.de/techdocs/jee-rest.htm" );
          feed.addLink(     uri, "self" );
       }
    
       public static void fillEntryData( Entry entry, Long idNum, String uri )
       {
          long id = ( idNum != null ) ? idNum.longValue() : 4711;
          entry.setId(      uri + "/entry/" + id );
          entry.setUpdated( new Date( new Date().getTime() - (long)(100000000 * Math.exp( -id / 100. )) ) );
          entry.setTitle(   "Titel zum Entry mit der ID " + id );
          entry.setContent( "Hier ist der Inhalt des Entrys mit der ID " + id );
          entry.addAuthor(  "ich" );
       }
    
       public static List<Long> getListOfIDs()
       {
          return Arrays.asList( new Long[] { Long.valueOf( 4711 ), Long.valueOf( 42 ), Long.valueOf( 7 ) } );
       }
    }
    
  4. Erzeugen Sie im src\test\java\atomfeed-Verzeichnis einen JUnit-Test: SimpleAtomFeedServiceTest.java

    package atomfeed;
    
    import java.net.URL;
    import com.sun.jersey.api.container.grizzly2.GrizzlyServerFactory;
    import org.apache.abdera.Abdera;
    import org.apache.abdera.model.*;
    import org.apache.abdera.parser.Parser;
    import org.glassfish.grizzly.http.server.HttpServer;
    import org.junit.*;
    
    public class SimpleAtomFeedServiceTest
    {
       @Test
       public void testSimpleAtomFeedService() throws Exception
       {
          String urlSrvr  = "http://localhost:4434";
          String urlFeed  = urlSrvr + "/atomfeed";
          String urlEntry = urlSrvr + "/atomfeed/entry/13";
          Parser parser   = new Abdera().getParser();
    
          // Testserver:
          HttpServer srv = GrizzlyServerFactory.createHttpServer( urlSrvr );
    
          try {
    
             // Teste Entry:
             URL             urlE = new URL( urlEntry );
             Document<Entry> docE = parser.parse( urlE.openStream(), urlE.toString() );
             Entry           entry1 = docE.getRoot();
             Assert.assertEquals( "Titel zum Entry mit der ID 13", entry1.getTitle() );
             Assert.assertEquals( "Hier ist der Inhalt des Entrys mit der ID 13", entry1.getContent() );
             Assert.assertEquals( "ich", entry1.getAuthor().getName() );
    
             // Teste Feed:
             URL            urlF = new URL( urlFeed );
             Document<Feed> docF = parser.parse( urlF.openStream(), urlF.toString() );
             Feed           feed = docF.getRoot();
             Assert.assertEquals( "Mein Feed-Titel", feed.getTitle() );
             Assert.assertEquals( "ich", feed.getAuthor().getName() );
             Assert.assertEquals( 3, feed.getEntries().size() );
             for( Entry entry : feed.getEntries() ) {
                Assert.assertTrue( entry.getTitle().startsWith( "Titel zum Entry mit der ID " ) );
                Assert.assertTrue( entry.getContent().startsWith( "Hier ist der Inhalt des Entrys " ) );
             }
    
          } finally {
             // Testserver beenden:
             srv.stop();
          }
       }
    }
    
  5. Erzeugen Sie im src\test\java\atomfeed-Verzeichnis eine Klasse für einen einfachen JAX-RS-Server: JaxRsTestServer.java

    package atomfeed;
    
    import com.sun.jersey.api.container.grizzly2.GrizzlyServerFactory;
    import org.glassfish.grizzly.http.server.HttpServer;
    
    public class JaxRsTestServer
    {
       public static void main( String[] args ) throws Exception
       {
          String url = ( args.length > 0 ) ? args[0] : "http://localhost:4434";
          String sec = ( args.length > 1 ) ? args[1] : "10";
    
          HttpServer srv = GrizzlyServerFactory.createHttpServer( url );
    
          System.out.println( "URL: " + url );
          Thread.sleep( 1000 * Integer.parseInt( sec ) );
          srv.stop();
       }
    }
    
  6. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\AtomAbdera]
     |- [src]
     |   |- [main]
     |   |   |- [java]
     |   |   |   '- [atomfeed]
     |   |   |       |- AbderaJaxRsProvider.java
     |   |   |       |- SimpleAtomFeedService.java
     |   |   |       '- SimpleDatenDAO.java
     |   |   '- [webapp]
     |   |       '- [WEB-INF]
     |   |           '- web.xml
     |   '- [test]
     |       '- [java]
     |           '- [atomfeed]
     |               |- AtomFeedPrint.java
     |               |- JaxRsTestServer.java
     |               '- SimpleAtomFeedServiceTest.java
     '- pom.xml
    
  7. Führen Sie im Kommandozeilenfenster aus (z.B. per Batchdatei):

    call mvn clean package

    start java -cp target/test-classes;target/AtomAbdera/WEB-INF/classes;target/AtomAbdera/WEB-INF/lib/* atomfeed.JaxRsTestServer http://localhost:4434 15

    @ping -n 6 127.0.0.1 >nul

    java -cp target/test-classes;target/AtomAbdera/WEB-INF/lib/* atomfeed.AtomFeedPrint http://localhost:4434/atomfeed

  8. Natürlich können Sie (solange der JaxRsTestServer läuft) den Atomfeed auch per cURL abrufen:

    Feed mit Header:  curl -i -H "Accept:application/atom+xml" http://localhost:4434/atomfeed
    Feed ohne Header:  curl -H "Accept:application/atom+xml" http://localhost:4434/atomfeed
    Einzelnen Entry:  curl -H "Accept:application/atom+xml" http://localhost:4434/atomfeed/entry/0815

Atomfeed mit JAXB-Content

  1. Erzeugen Sie im src\main\java\atomfeed-Verzeichnis die Atomfeed-JAX-RS-Service-Klasse: JaxbAtomFeedService.java

    package atomfeed;
    
    import javax.ws.rs.*;
    import javax.ws.rs.core.*;
    import javax.xml.bind.JAXBException;
    import org.apache.abdera.model.*;
    
    @Path( "atomfeedjaxb" )
    public class JaxbAtomFeedService
    {
       @GET @Produces( MediaType.APPLICATION_ATOM_XML )
       public static Feed getFeed( @Context UriInfo uri ) throws JAXBException
       {
          Feed feed = AbderaJaxRsProvider.getAbdera().getFactory().newFeed();
          JaxbDatenDAO.fillFeedData( feed, uri.getAbsolutePath().toString() );
          for( Long id : JaxbDatenDAO.getListOfIDs() ) {
             Entry entry = feed.addEntry();
             JaxbDatenDAO.fillEntryData( entry, id, uri.getAbsolutePath().toString() );
          }
          return feed;
       }
    
       @Path( "entry/{id}" )
       @GET @Produces( MediaType.APPLICATION_ATOM_XML )
       public static Entry getEntry( @PathParam("id") Long id, @Context UriInfo uri ) throws JAXBException
       {
          Entry entry = AbderaJaxRsProvider.getAbdera().getFactory().newEntry();
          JaxbDatenDAO.fillEntryData( entry, id, uri.getAbsolutePath().toString() );
          return entry;
       }
    
       @Path( "entry" )
       @PUT @Consumes( MediaType.APPLICATION_ATOM_XML )
       public String putEntry( Entry entry ) throws JAXBException
       {
          return JaxbDatenDAO.storeContent( entry );
       }
    }
    

    JaxbAtomFeedService ist sehr ähnlich zu obiger SimpleAtomFeedService-Klasse, allerdings gibt es jetzt auch eine PUT-Methode, um Entities hinzufügen zu können.

  2. Erzeugen Sie im src\main\java\atomfeed-Verzeichnis für den JAXB-Content eine simple Daten-Klasse mit @XmlRootElement-Annotation (um es etwas komplizierter zu machen, fügen wir auch eine Namespace-Angabe hinzu): MeineDaten.java

    package atomfeed;
    
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement( namespace="de.meinefirma.meinnamespace" )
    public class MeineDaten
    {
       public long   i;
       public String s;
    }
    
  3. Erzeugen Sie im src\main\java\atomfeed-Verzeichnis eine simple Datenspeicher-Simulationsklasse, welche die Atomfeed-Content-Inhalte per JAXB liest und schreibt: JaxbDatenDAO.java

    package atomfeed;
    
    import java.io.*;
    import java.util.*;
    import java.util.concurrent.ConcurrentHashMap;
    import javax.xml.bind.*;
    import org.apache.abdera.model.*;
    
    /**
     * Simple Simulation eines Datenspeichers.
     * In der Realitaet koennten die Daten in einer Datenbank gespeichert werden.
     */
    public class JaxbDatenDAO
    {
       public static final String HTTP_201_CREATED = "201 Created";
    
       // Map als Datenbank-Simulation:
       private static Map<Long,MeineDaten> daten = new ConcurrentHashMap<Long,MeineDaten>();
    
       public static void fillFeedData( Feed feed, String uri )
       {
          feed.setId(       uri );
          feed.setUpdated(  new Date() );
          feed.setTitle(    "Mein Feed-Titel" );
          feed.setSubtitle( "Mein Feed-Untertitel" );
          feed.addAuthor(   "ich" );
          feed.addLink(     "http://www.torsten-horn.de/techdocs/jee-rest.htm" );
          feed.addLink(     uri, "self" );
       }
    
       public static void fillEntryData( Entry entry, Long idNum, String uri ) throws JAXBException
       {
          Long id = ( idNum != null ) ? idNum : Long.valueOf( 0 );
          entry.setId(      uri + "/entry/" + id );
          entry.setUpdated( new Date() );
          entry.setTitle(   "Titel zum Entry mit der ID " + id );
    
          MeineDaten md = daten.get( id );
          if( md != null ) {
             JAXBContext jaxbCtx = JAXBContext.newInstance( MeineDaten.class );
             StringWriter writer = new StringWriter();
             jaxbCtx.createMarshaller().marshal( md, writer );
             entry.setContent( writer.toString(), "application/vnd.meinmediatyp+xml" );
          }
       }
    
       public static String storeContent( Entry entry ) throws JAXBException
       {
          JAXBContext jaxbCtx = JAXBContext.newInstance( MeineDaten.class );
          MeineDaten  md = (MeineDaten) jaxbCtx.createUnmarshaller().unmarshal( new StringReader( entry.getContent() ) );
          daten.put( new Long( md.i ), md );
          return HTTP_201_CREATED;
       }
    
       public static Set<Long> getListOfIDs()
       {
          return daten.keySet();
       }
    }
    

    Bitte beachten Sie, dass im Beispiel der spezielle Mediatyp "application/vnd.meinmediatyp+xml" gesetzt wurde ("vnd" steht für "Vendor-spezifisch", also proprietär).

  4. Erzeugen Sie im src\test\java\atomfeed-Verzeichnis einen JUnit-Test: JaxbAtomFeedServiceTest.java

    package atomfeed;
    
    import java.io.IOException;
    import java.net.URL;
    import javax.ws.rs.core.MediaType;
    import com.sun.jersey.api.client.*;
    import com.sun.jersey.api.container.grizzly2.GrizzlyServerFactory;
    import org.apache.abdera.Abdera;
    import org.apache.abdera.model.*;
    import org.apache.abdera.parser.ParseException;
    import org.glassfish.grizzly.http.server.HttpServer;
    import org.junit.*;
    
    public class JaxbAtomFeedServiceTest
    {
       static final Abdera abdera = new Abdera();
    
       @Test
       public void testJaxbAtomFeedService() throws IllegalArgumentException, IOException
       {
          final String urlSrvr  = "http://localhost:4434";
          final String urlFeed  = urlSrvr + "/atomfeedjaxb";
          final String urlEntry = urlSrvr + "/atomfeedjaxb/entry";
          final long   idNum13  = 13;
          final long   idNum42  = 42;
    
          // Testserver:
          HttpServer srv = GrizzlyServerFactory.createHttpServer( urlSrvr );
    
          try {
    
             // Schreibe zwei Entries:
             Assert.assertEquals( JaxbDatenDAO.HTTP_201_CREATED, writeEntry( urlEntry, idNum13 ) );
             Assert.assertEquals( JaxbDatenDAO.HTTP_201_CREATED, writeEntry( urlEntry, idNum42 ) );
    
             // Lies und teste beide Entries:
             readAndCheckEntry( urlEntry, idNum13 );
             readAndCheckEntry( urlEntry, idNum42 );
    
             // Teste Feed:
             URL            urlF = new URL( urlFeed );
             Document<Feed> docF = abdera.getParser().parse( urlF.openStream(), urlF.toString() );
             Feed           feed = docF.getRoot();
             Assert.assertEquals( "Mein Feed-Titel", feed.getTitle() );
             Assert.assertEquals( "ich", feed.getAuthor().getName() );
             Assert.assertEquals( 2, feed.getEntries().size() );
             for( Entry entry : feed.getEntries() ) {
                Assert.assertTrue( entry.getTitle().startsWith( "Titel zum Entry mit der ID " ) );
                Assert.assertTrue( entry.getContent().startsWith( "<ns2:meineDaten xmlns:ns2=\"de.meinefirma.meinnamespace\">" ) );
             }
    
          } finally {
             // Testserver beenden:
             srv.stop();
          }
       }
    
       private static String writeEntry( String urlEntry, long idNum )
       {
          WebResource wrs = Client.create().resource( urlEntry );
          return wrs.type( MediaType.APPLICATION_ATOM_XML + "; charset=utf-8" )
                  .accept( MediaType.APPLICATION_ATOM_XML + "; charset=utf-8" ).put( String.class,
                        "<entry xmlns='http://www.w3.org/2005/Atom'>" +
                        "<id>http://localhost:4434/atomfeedjaxb/entry/" + idNum + "</id>" +
                        "<updated>2011-01-02T00:00:00.000Z</updated>" +
                        "<title type='text'>Titel zum Entry mit der ID " + idNum + "</title>" +
                        "<content type='application/vnd.meinmediatyp+xml'>" +
                        "<ns2:meineDaten xmlns:ns2='de.meinefirma.meinnamespace'>" +
                        "<i xmlns=''>" + idNum + "</i>" +
                        "<s xmlns=''>Mein JAXB-Daten-Text " + idNum + "</s>" +
                        "</ns2:meineDaten></content></entry>" );
       }
    
       private static Entry readAndCheckEntry( String urlEntry, long idNum ) throws ParseException, IOException
       {
          URL url = new URL( urlEntry + "/" + idNum );
          Entry entry = (Entry) abdera.getParser().parse( url.openStream(), url.toString() ).getRoot();
          Assert.assertEquals( "Titel zum Entry mit der ID " + idNum, entry.getTitle() );
          Assert.assertEquals( "<ns2:meineDaten xmlns:ns2=\"de.meinefirma.meinnamespace\">\n" +
                               "      <i>" + idNum + "</i>\n" +
                               "      <s>Mein JAXB-Daten-Text " + idNum + "</s>\n" +
                               "    </ns2:meineDaten>", entry.getContent() );
          return entry;
       }
    }
    
  5. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\AtomAbdera]
     |- [src]
     |   |- [main]
     |   |   |- [java]
     |   |   |   '- [atomfeed]
     |   |   |       |- AbderaJaxRsProvider.java
     |   |   |       |- JaxbAtomFeedService.java
     |   |   |       |- JaxbDatenDAO.java
     |   |   |       |- MeineDaten.java
     |   |   |       |- SimpleAtomFeedService.java
     |   |   |       '- SimpleDatenDAO.java
     |   |   '- [webapp]
     |   |       '- [WEB-INF]
     |   |           '- web.xml
     |   '- [test]
     |       '- [java]
     |           '- [atomfeed]
     |               |- AtomFeedPrint.java
     |               |- JaxbAtomFeedServiceTest.java
     |               |- JaxRsTestServer.java
     |               '- SimpleAtomFeedServiceTest.java
     '- pom.xml
    
  6. Führen Sie im Kommandozeilenfenster aus (z.B. per Batchdatei):

    call mvn clean package

    start java -cp target/test-classes;target/AtomAbdera/WEB-INF/classes;target/AtomAbdera/WEB-INF/lib/* atomfeed.JaxRsTestServer http://localhost:4434 15

    @ping -n 6 127.0.0.1 >nul

    curl -i -H Content-type:application/atom+xml --request PUT -d "<entry xmlns='http://www.w3.org/2005/Atom'><id>http://localhost:4434/atomfeedjaxb/entry/42</id><updated>2011-01-02T00:00:00.000Z</updated><title type='text'>Titel zum Entry mit der ID 42</title><content type='application/vnd.meinmediatyp+xml'><ns2:meineDaten xmlns:ns2='de.meinefirma.meinnamespace'><i xmlns=''>42</i><s xmlns=''>Mein JAXB-Daten-Text 42</s></ns2:meineDaten></content></entry>" "http://localhost:4434/atomfeedjaxb/entry"

    curl -i -H Content-type:application/atom+xml "http://localhost:4434/atomfeedjaxb/entry/42"

    curl -i -H Content-type:application/atom+xml "http://localhost:4434/atomfeedjaxb"

XSD-Schema

RFC 4287 enthält im Anhang ein "informatives" Relax-NG-Schema, aber leider gibt es weder ein "offizielles" noch ein 100 % vollständiges XSD-Schema für das Atomfeed-Format. Aber wenn Sie im Web nach "Atom.xsd" suchen, werden Sie mehrere "Näherungen" finden, die für viele Anwendungen ausreichen, zum Beispiel:

http://tools.oasis-open.org/version-control/browse/wsvn/cmis/trunk/SchemaProject/schema/ATOM.xsd?sc=1

Die Validierung können Sie mit einem beliebigen XSD-Validierungstool durchführen, zum Beispiel mit XsdValidation.java.

Falls Sie beim Validieren eine Exception ähnlich zu folgender erhalten:

org.xml.sax.SAXParseException: src-resolve: Cannot resolve the name 'xml:base' to a(n) 'attribute declaration' component.

Dann konnte wahrscheinlich eine "<xs:import ..."-Anweisung nicht ausgeführt werden.
Falls die betreffende "<xs:import ..."-Anweisung auf eine lokale Datei verweist, zum Beispiel so:

<xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="xml.xsd" />

Dann muss die entsprechende Datei (im Beispiel xml.xsd) im aktuellen Verzeichnis existieren (oder durch eine Internetadresse ersetzt werden).
Falls die betreffende "<xs:import ..."-Anweisung auf eine Internet-URL verweist, etwa so:

<xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2001/03/xml.xsd" />

Dann muss Ihr Internet-Zugriff funktionieren. Falls Sie sich hinter einer Proxy-Firewall befinden, müssen Sie die Proxy-Parameter angeben, zum Beispiel so:

java -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=3128 XsdValidation %1 %2

Alternativ können Sie auch einen Online-Validierungsdienst verwenden, wie zum Beispiel: http://www.validome.org/rss-atom/lang/ge oder http://www.feedvalidator.org.



Links auf weiterführende Informationen




Weitere Themen: andere TechDocs | SOA | SOAP Web Services mit JAX-WS
© 2009-2011 Torsten Horn, Aachen