RESTful Web Services mit JAX-RS 2.1 und Jersey 2.26

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


Jersey-Logo 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 370 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 2.1, meistens mit Jersey 2.26. Die meisten Demos funktionieren auch mit JAX-RS 2.0 und mit anderen Jersey 2.x-Versionen. Falls Sie Infos zu JAX-RS 1.1 und Jersey 1.x suchen, sehen Sie sich bitte an: REST mit JAX-RS 1.1.

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. Versionierung von REST-Schnittstellen
  5. Infos zu JAX-RS und Jersey
  6. JAX-RS-REST-HelloWorld-Programmierbeispiel, vorerst ohne Maven
    Libs, HalloWeltService.java, HalloWeltTestServer.java, HalloWeltTestClient.java, Projektstruktur, Build und Test, WADL
  7. JAX-RS-REST-HelloWorld-Programmierbeispiel mit Maven
    pom.xml, web.xml, HalloWeltServiceTest.java, Deployment im Tomcat und WebLogic
  8. JAX-RS mit XML-Daten per JAXB
    InputTO, ResultTO, XmlJaxbService, XmlJaxbServiceTest
  9. "Contract-First"-REST-Service (ausgehend von Schema-XSD-Datei)
    Contract-First, Schema-XSD-Datei, xjc, XmlRootElement, ContractfirstService, RestServerTestUtil, ContractfirstServiceTest, JAXBElement
  10. "Contract-First"-REST-Client (ausgehend von Schema-XSD-Datei)
    fuer-test / test-jar, maven-jar-plugin, ContractFirstClient, ContractFirstClientTest
  11. Authentifizierung, Integrationstest und Tomcat
    pom.xml, web.xml, JaxRsAuthenticationIntegrTest, tomcat-users.xml
  12. JAX-RS-REST-Service mit embedded Jetty-Server
  13. JAX-RS-REST-Service mit dem Jetty-Maven-Plugin
  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. Performance-Vergleich zwischen REST (mit JAX-RS) und SOAP (mit JAX-WS)
  16. JAX-RS-REST-Service mit Dropwizard
  17. Der Kommandozeilen-JSON-Prozessor jp
  18. JSON-REST-Client mit HttpURLConnection und JsonObject
    Projektbasis und JsonObject-/HttpURLConnection-Utility, JSON-REST-Client für den Dollar-Kurs, JSON-REST-Client für den Bitcoin-Kurs, JSON-REST-Client für Länderkürzel
  19. Javax-MVC mit Ozark, JAX-RS und Jetty
  20. Kommandozeilen-Client-Tools
    cURL, Wget, TShark
  21. Webbrowser-Client-Tools
    Firebug, RESTClient, Poster
  22. TCP/IP-Monitore
    Wireshark, Apache TCPMon, Eclipse TCP/IP Monitor, TcpTrace, TCP/IP-Monitor-Konfiguration bei mehreren Rechnern



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 370)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


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:



Infos zu JAX-RS und Jersey

Jersey-Logo Jersey ist die JAX-RS (JSR 370) Reference Implementation und wird in den meisten der folgenden Programmierbeispiele verwendet. Infos zu JAX-RS, JSR 370 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 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 (Java 8 oder Java 9).
  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

    tree /F

    Die Projektstruktur sieht jetzt so aus:

    [\MeinWorkspace\JaxRsHelloWorld]
     |- [bin]
     |- [lib]
     '- [src]
         '- [minirestwebservice]
    
  3. Downloaden Sie das "Jersey JAX-RS 2.1 RI bundle" (jaxrs-ri-2.26.zip) von https://jersey.github.io.

    Entzippen Sie das Jersey-Archiv in ein temporäres Verzeichnis und kopieren Sie in das JaxRsHelloWorld/lib-Verzeichnis entweder einfach alle 32 .jar-Libraries oder nur folgende 12:
    aus jaxrs-ri/api:
    javax.ws.rs-api-2.1.jar,
    aus jaxrs-ri/ext:
    hk2-api-2.5.0-b42.jar, hk2-locator-2.5.0-b42.jar, hk2-utils-2.5.0-b42.jar, javax.annotation-api-1.2.jar, javax.inject-2.5.0-b42.jar, javax.json.bind-api-1.0.jar, validation-api-1.1.0.Final.jar,
    aus jaxrs-ri/lib:
    jersey-client.jar, jersey-common.jar, jersey-hk2.jar, jersey-server.jar.

  4. Downloaden Sie zusätzlich für den Grizzly-Server die beiden jar-Lib-Dateien grizzly-http-all-2.4.2.jar und jersey-container-grizzly2-http-2.26.jar, und kopieren Sie sie in das JaxRsHelloWorld/lib-Verzeichnis.
    Bitte beachten Sie, dass die Versionen der jar-Libs zusammen passen müssen, siehe hierzu auch beispielsweise Project Dependencies, sowie 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( HalloWeltService.webContextPath )
    public class HalloWeltService
    {
       static final String webContextPath = "/helloworld";
    
       @GET @Produces( MediaType.TEXT_PLAIN )
       public String halloPlainText( @QueryParam("name") String name )
       {
          return "Plain-Text: 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. application/json, application/xml oder text/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.

  6. RESTful-Webservice-Server: HalloWeltTestServer.java

    package minirestwebservice;
    
    import java.io.IOException;
    import java.net.URI;
    import org.glassfish.grizzly.http.server.HttpServer;
    import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
    import org.glassfish.jersey.server.ResourceConfig;
    
    public class HalloWeltTestServer
    {
       public static void main( String[] args ) throws IOException, InterruptedException 
       {
          String baseUrl = ( args.length > 0 ) ? args[0] : "http://localhost:4434";
    
          final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(
                URI.create( baseUrl ), new ResourceConfig( HalloWeltService.class ), false );
          Runtime.getRuntime().addShutdownHook( new Thread( new Runnable() {
             @Override
             public void run() {
                server.shutdownNow();
             }
          } ) );
          server.start();
    
          System.out.println( String.format( "\nGrizzly-HTTP-Server gestartet mit der URL: %s\n"
                                             + "Stoppen des Grizzly-HTTP-Servers mit:      Strg+C\n",
                                             baseUrl + HalloWeltService.webContextPath ) );
    
          Thread.currentThread().join();
       }
    }
    

    Sehen Sie sich die GrizzlyHttpServerFactory-Klasse, die createHttpServer()-Methode und die resultierende HttpServer-Klasse, sowie die HttpServer-Doku an.

  7. RESTful-Webservice-Client: HalloWeltTestClient.java

    package minirestwebservice;
    
    import javax.ws.rs.client.*;
    import javax.ws.rs.core.MediaType;
    
    public class HalloWeltTestClient
    {
       public static void main( String[] args )
       {
          String name           = ( args.length > 0 ) ? args[0] : "ich";
          String baseUrl        = ( args.length > 1 ) ? args[1] : "http://localhost:4434";
          String webContextPath = "/helloworld";
          System.out.println( "\nAngefragte URL: " + baseUrl + webContextPath + "?name=" + name );
    
          Client c = ClientBuilder.newClient();
          WebTarget target = c.target( baseUrl );
          
          System.out.println( "\nTextausgabe:" );
          System.out.println( target.path( webContextPath ).queryParam( "name", name ).request( MediaType.TEXT_PLAIN ).get( String.class ) );
          System.out.println( "\nHTML-Ausgabe:" );
          System.out.println( target.path( webContextPath ).queryParam( "name", name ).request( MediaType.TEXT_HTML ).get( String.class ) );
       }
    }
    

    Sehen Sie sich die Client- und WebTarget-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.4.2.jar
     |   |- hk2-api-2.5.0-b42.jar
     |   |- hk2-locator-2.5.0-b42.jar
     |   |- hk2-utils-2.5.0-b42.jar
     |   |- javax.annotation-api-1.2.jar
     |   |- javax.inject-2.5.0-b42.jar
     |   |- javax.json.bind-api-1.0.jar
     |   |- javax.ws.rs-api-2.1.jar
     |   |- jersey-client.jar
     |   |- jersey-common.jar
     |   |- jersey-container-grizzly2-http-2.26.jar
     |   |- jersey-hk2.jar
     |   |- jersey-server.jar
     |   |- validation-api-1.1.0.Final.jar
     '- [src]
         '- [minirestwebservice]
             |- HalloWeltService.java
             |- HalloWeltTestClient.java
             '- HalloWeltTestServer.java
    
  9. Öffnen Sie ein Kommandozeilenfenster ('Windows-Taste' + 'R', 'cmd') und bauen Sie das Projekt:

    cd \MeinWorkspace\JaxRsHelloWorld

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

    Bis Java 8 starten Sie so den den RESTful-Webservice-Server in einem eigenen Kommandozeilenfenster:

    start java -cp bin;lib/* minirestwebservice.HalloWeltTestServer

    Ab Java 9 starten Sie so den den RESTful-Webservice-Server in einem eigenen Kommandozeilenfenster:

    start java --add-modules java.xml.bind -cp bin;lib/* minirestwebservice.HalloWeltTestServer

    Warten Sie ca. eine Sekunde, bis der Server fertig gestartet ist, und starten Sie den RESTful-Webservice-Client. Ersetzen Sie dabei ich durch Ihren Namen.

    Bis Java 8:

    java -cp bin;lib/* minirestwebservice.HalloWeltTestClient ich

    Ab Java 9:

    java --add-modules java.xml.bind -cp bin;lib/* minirestwebservice.HalloWeltTestClient ich

    Sie erhalten im Kommandozeilenfenster:

    Angefragte URL: http://localhost:4434/helloworld?name=ich
    
    Textausgabe:
    Plain-Text: Hallo ich
    
    HTML-Ausgabe:
    <html><title>HelloWorld</title><body><h2>Html: Hallo ich</h2></body></html>
    
  10. Rufen Sie folgende Webseiten auf:

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

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

    start http://localhost:4434/application.wadl?detail=true

    Im ersten Webbrowser-Fenster erscheint das Ergebnis der HTML-GET-Methode:

    Html: Hallo ich

    Im zweiten und dritten Webbrowser-Fenster bieten MS Internet Explorer und Edge den Download der WADL-XML-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". Weiter unten wird gezeigt, wie die WADL-Datei sehr einfach mit cURL angezeigt werden kann.

    Jersey produziert eine kurze und mit detail=true eine lange Variante der WADL-Datei. Die Kurzversion lautet:

    <?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: 2.26 2017-09-05 11:50:34"/>
        <doc xmlns:jersey="http://jersey.java.net/"
                jersey:hint="This is simplified WADL with user and core resources only.
                             To get full WADL with extended resources use the query parameter detail.
                             Link: http://localhost:4434/application.wadl?detail=true"/>
        <grammars/>
        <resources base="http://localhost:4434/">
            <resource path="/helloworld">
                <method id="halloPlainText" 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 WADL Support an.

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

  12. Sehen Sie sich weiter unten die Installationsbeschreibung zu cURL und die Erläuterungen zu den folgenden cURL-Kommandos an. Führen Sie aus:

    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. Sehen Sie sich auch weiter unten die anderen Kommandozeilen-Client-Tools sowie die Webbrowser-Client-Tools und die TCP/IP-Monitore zur Analyse der REST-Kommunikation an.

  14. Beenden Sie den HalloWeltTestServer mit: Strg+C.



JAX-RS-REST-HelloWorld-Programmierbeispiel mit Maven

Maven-Logo Das folgende Programmierbeispiel ist ähnlich wie obiges JAX-RS-REST-HelloWorld-Programmierbeispiel, vorerst ohne Maven. Allerdings werden diesmal die benötigten Libs nicht manuel downgeloadet und hinzugefügt. Stattdessen werden sie mit Hilfe des Build-Tools Maven automatisch hinzugefügt.

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) 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

    tree /F

  3. Falls Sie das obige Programmierbeispiel JAX-RS-REST-HelloWorld-Programmierbeispiel, vorerst ohne Maven durchgeführt haben, kopieren Sie daraus die drei Java-Dateien:

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

    Andernfalls legen Sie im neuen src\main\java\minirestwebservice-Verzeichnis folgende drei Java-Dateien an: HalloWeltService.java, HalloWeltTestServer.java und HalloWeltTestClient.java.

  4. 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/xsd/maven-4.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.7.0</version>
            <configuration>
              <source>1.8</source>
              <target>1.8</target>
            </configuration>
          </plugin>
          <!-- Diese surefire-Konfiguration ist ab Java 9 notwendig,
               aber muss fuer Java 8 entfernt werden: -->
          <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.21.0</version>
            <configuration>
              <argLine>--add-modules java.xml.bind</argLine>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencyManagement>
        <dependencies>
          <dependency>
            <groupId>org.glassfish.jersey</groupId>
            <artifactId>jersey-bom</artifactId>
            <version>2.26</version>
            <type>pom</type>
            <scope>import</scope>
          </dependency>
        </dependencies>
      </dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-servlet-core</artifactId>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-grizzly2-http</artifactId>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.inject</groupId>
          <artifactId>jersey-hk2</artifactId>
        </dependency>
        <dependency>
          <groupId>javax.activation</groupId>
          <artifactId>activation</artifactId>
          <version>1.1.1</version>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    </project>
    

    Achtung: Beachten Sie die unterschiedliche Konfiguration für Java 8 und Java 9:

    Bis Java 8 muss der oben gezeigte maven-surefire-plugin-Konfigurationsblock entfernt werden!

    Ab Java 9 haben Sie zwei Möglichkeiten:
    Enweder Sie fügen den maven-surefire-plugin-Konfigurationsblock so wie oben gezeigt ein.
    Oder Sie fügen den benötigten Parameter bei jedem Maven-Aufruf auf der Kommandozeile hinzu, z.B. so:

    mvn -DargLine="--add-modules java.xml.bind" package

  5. 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>
        <servlet-name>REST-Servlet</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
          <param-name>jersey.config.server.provider.packages</param-name>
          <param-value>minirestwebservice;xmljaxb;contractfirstservice</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
      </servlet>
      <servlet-mapping>
        <servlet-name>REST-Servlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
      </servlet-mapping>
    </web-app>
    

    Unter dem Servlet-Parameter jersey.config.server.provider.packages sind drei Package-Angaben eingetragen: Für dieses erste Beispiel hätte die Package-Angabe minirestwebservice genügt. Die anderen beiden Packages werden erst in den weiter unten folgenden Beispielen verwendet.

  6. Fügen Sie im src\test\java\minirestwebservice-Testverzeichnis eine JUnit-Modultestklasse hinzu: HalloWeltServiceTest.java

    package minirestwebservice;
    
    import java.net.URI;
    import javax.ws.rs.client.*;
    import javax.ws.rs.core.MediaType;
    import org.glassfish.grizzly.http.server.HttpServer;
    import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
    import org.glassfish.jersey.server.ResourceConfig;
    import org.junit.*;
    
    public class HalloWeltServiceTest
    {
       @Test
       public void testRESTfulWebService()
       {
          String baseUrl        = "http://localhost:4434";
          String webContextPath = "/helloworld";
          String name           = "MeinName";
    
          // Testserver:
          HttpServer server = GrizzlyHttpServerFactory.createHttpServer(
                URI.create( baseUrl ), new ResourceConfig( HalloWeltService.class ) );
    
          try {
             // Testclient:
             Client c = ClientBuilder.newClient();
             WebTarget target = c.target( baseUrl );
    
             // Pruefungen:
             String txt = target.path( webContextPath ).queryParam( "name", name ).request( MediaType.TEXT_PLAIN ).get( String.class );
             String htm = target.path( webContextPath ).queryParam( "name", name ).request( MediaType.TEXT_HTML ).get( String.class );
             Assert.assertEquals( "Plain-Text: Hallo MeinName", txt );
             Assert.assertEquals( "<html><title>HelloWorld</title><body><h2>Html: Hallo MeinName</h2></body></html>", htm );
    
          } finally {
             // Testserver beenden:
             server.shutdown();
          }
       }
    }
    
  7. Bitte beachten Sie, dass Sie diesmal nicht manuell Libs zum Projekt hinzukopieren müssen, weil sich darum Maven kümmert.

  8. 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
    
  9. Führen Sie den JUnit-Modultest aus:

    mvn test

  10. Beachten Sie, dass im JUnit-Modultest während der Dauer des Tests ein Grizzly-Webserver als embedded Server temporär gestartet ("INFORMATION: [HttpServer] Started") 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

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

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

  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. Beenden Sie den Grizzly-Webserver mit: Strg+C.

  14. Die WAR-Datei im target-Verzeichnis können Sie in Java EE Webserver, Application Server und Servlet-Container deployen.

    Falls Sie statt des gewünschten Ergebnisses die Fehlermeldung "HTTP Status 404 - Not Found" erhalten, überprüfen Sie Folgendes:
    Ist die URL und insbesondere die Groß/Kleinschreibung in der URL korrekt?
    Entspricht der erste Teil in der URL nach der Portnummer (im Beispiel JaxRsMitMaven) dem Namen der WAR-Datei?
    Entspricht der sich anschließende URL-Teil (im Beispiel rest) dem Eintrag in der web.xml unter url-pattern?
    Entspricht der sich daran anschließende URL-Teil (im Beispiel helloworld) dem @Path-Eintrag in der REST-Service-Java-Datei (im Beispiel HalloWeltService.java)?
    Ist das Package des REST-Services in der web.xml unter jersey.config.server.provider.packages eingetragen?

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

  16. Falls Sie Java 8 verwenden und folgende Fehlermeldung erhalten:

    [ERROR] The forked VM terminated without properly saying goodbye. VM crash or System.exit called?
    [ERROR] Command was cmd.exe /X /C ""C:\Program Files\Java\jdk1.8\jre\bin\java" --add-modules java.xml.bind -jar ...

    Dann haben Sie den Parameter "--add-modules java.xml.bind" verwendet, der nur ab Java 9 verwendet werden darf. Sehen Sie sich die obigen Erläuterungen an.

    Falls Sie Java 9 oder höher verwenden und folgende Fehlermeldung erhalten:

    java.lang.NoClassDefFoundError: javax/xml/bind/PropertyException
    Caused by: java.lang.ClassNotFoundException: javax.xml.bind.PropertyException

    Dann fehlt der Parameter "--add-modules java.xml.bind", der ab Java 9 für diese Demo benötigt wird. Sehen Sie sich die obigen Erläuterungen an.

    Sehen Sie sich hierzu auch an: NoClassDefFoundError: javax/xml/bind/JAXBException.



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 java.net.URI;
    import javax.ws.rs.client.*;
    import org.glassfish.grizzly.http.server.HttpServer;
    import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
    import org.glassfish.jersey.server.ResourceConfig;
    import org.junit.*;
    
    public class XmlJaxbServiceTest
    {
       @Test
       public void testXmlJaxbService()
       {
          String xmlUtf8        = "text/xml; charset=utf-8";
          String baseUrl        = "http://localhost:4434";
          String webContextPath = "/xmljaxb";
    
          // Testserver:
          HttpServer server = GrizzlyHttpServerFactory.createHttpServer(
                URI.create( baseUrl ), new ResourceConfig( XmlJaxbService.class ) );
    
          try {
             // Testclient:
             Client c = ClientBuilder.newClient();
             WebTarget target = c.target( baseUrl );
    
             // Mit JAXB und mit bequemen Java-Objekten:
             InputTO inpTO = new InputTO();
             inpTO.i = 42;
             inpTO.s = "abc xyz";
             ResultTO resTO = target.path( webContextPath ).request( xmlUtf8 ).accept( xmlUtf8 ).post(
                   Entity.entity( inpTO, xmlUtf8 ), ResultTO.class );
             Assert.assertEquals(  84,             resTO.i );
             Assert.assertEquals( "abc xyz - ret", resTO.s );
    
             // Ohne JAXB und mit XML-Strings:
             String resXml = target.path( webContextPath ).request( xmlUtf8 ).accept( xmlUtf8 ).post(
                   Entity.entity( "<inputTO><i>42</i><s>abc xyz</s></inputTO>", xmlUtf8 ), String.class );
             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:
             // try {  Thread.sleep( 20000 ); } catch( java.lang.InterruptedException e ) { /* ok */ }
             // Testserver beenden:
             server.shutdown();
          }
       }
    }
    

    Sehen Sie sich die Client- und WebTarget-Klassen sowie die post()-Methode an.

  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. Sie können mit mvn package eine WAR-Datei im target-Verzeichnis erzeugen und diese WAR-Datei in einen Java EE Webserver, Application Server oder Servlet-Container deployen. Dann können Sie den REST-Service beispielsweise folgendermaßen ansprechen:

  9. Falls Sie nicht mit Java-Dateien beginnen wollen ("Code-First"), 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 JAX-RS mit XML-Daten per JAXB:

    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.net.URI;
    import javax.ws.rs.client.*;
    import org.glassfish.grizzly.http.server.HttpServer;
    import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
    import org.glassfish.jersey.server.ResourceConfig;
    
    public class RestServerTestUtil implements AutoCloseable
    {
       private String baseUrlClnt;
       private HttpServer server;
    
       // Fuer normale Modultests:
       public RestServerTestUtil( String baseUrl, Class<?>... restServiceClasses )
       {
          this( baseUrl, baseUrl, restServiceClasses );
       }
    
       // Falls ein HTTP-Monitor oder TCP/IP-Monitor zwischengeschaltet werden soll:
       public RestServerTestUtil( String baseUrlSrvr, String baseUrlClnt, Class<?>... restServiceClasses )
       {
          // Grizzly-Testserver starten:
          this.baseUrlClnt = baseUrlClnt;
          server = GrizzlyHttpServerFactory.createHttpServer(
                URI.create( baseUrlSrvr ), new ResourceConfig( restServiceClasses ) );
       }
    
       // Testclient:
       public WebTarget geWebTarget()
       {
          return ClientBuilder.newClient().target( baseUrlClnt );
       }
    
       @Override
       public void close()
       {
          if( server != null ) {
             // Eventuell verzoegerte Beendigung:
             // try {  Thread.sleep( 20000 ); } catch( java.lang.InterruptedException e ) { /* ok */ }
             // Grizzly-Testserver beenden:
             server.shutdown();
             server = null;
          }
       }
    }
    
  10. Erstellen Sie im Testverzeichnis src\test\java\contractfirstservice die JUnit-Modultestklasse: ContractfirstServiceTest.java

    package contractfirstservice;
    
    import javax.ws.rs.client.*;
    import javax.xml.bind.JAXBElement;
    import org.junit.*;
    import contractfirstgenerated.*;
    
    public class ContractfirstServiceTest
    {
       @Test
       public void testContractfirstService() throws Exception
       {
          String xmlUtf8        = "text/xml; charset=utf-8";
          String baseUrl        = "http://localhost:4434";
          String webContextPath = "/contractfirst";
    
          try( RestServerTestUtil restServerTestUtil = new RestServerTestUtil( baseUrl, ContractfirstService.class ) )
          {
             WebTarget target = restServerTestUtil.geWebTarget();
    
             // 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 = target.path( webContextPath ).request( xmlUtf8 ).accept( xmlUtf8 ).post(
                   Entity.entity( inpJaxb, xmlUtf8 ), ResultTO.class );
             Assert.assertEquals(  84,             resTO.getI() );
             Assert.assertEquals( "abc xyz - ret", resTO.getS() );
    
             // Ohne JAXB und mit XML-Strings:
             String resXml = target.path( webContextPath ).request( xmlUtf8 ).accept( xmlUtf8 ).post(
                   Entity.entity( "<inputTO xmlns=\"mein.ns\"><i>42</i><s>abc xyz</s></inputTO>", xmlUtf8 ), String.class );
             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 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 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 diese WAR-Datei in einen Java EE Webserver, Application Server oder Servlet-Container deployen. Dann können Sie den REST-Service beispielsweise folgendermaßen ansprechen:

    Da die Schema-XSD-Datei im src\main\webapp-Verzeichnis liegt, können Sie sie per cURL oder per Webbrowser abfragen, beispielsweise für WebLogic mit Port 7001:

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

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

    Falls Ihr Standard-Webbrowser die XSD-Datei nicht sinnvoll darstellt, und falls Sie den Microsoft-Edge-Browser installiert haben, verwenden Sie folgenden Aufruf:

    start microsoft-edge:http://localhost:7001/JaxRsContractFirstService/MeinInpResSchema.xsd

  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: Verwenden Sie den zweiten RestServerTestUtil-Konstruktor und übergeben Sie zwei verschiedene URLs, beispielsweise http://localhost:4434 und http://localhost:4435.

    Konfigurieren Sie in einem beliebigen HTTP-Monitor oder TCP/IP-Monitor die beiden gewählten URLs als Server-Host und als Local-Port und führen Sie ContractfirstServiceTestMitHttpMonitor aus. Weiter unten 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. Führen Sie im neuen JaxRsContractFirstClient-Projektverzeichnis in der kopierten pom.xml zwei Änderungen durch. Ersetzen Sie:
    a) die Zeile "<artifactId>JaxRsContractFirstService</artifactId>" durch "<artifactId>JaxRsContractFirstClient</artifactId>", und
    b) die Zeile "<packaging>war</packaging>" durch "<packaging>jar</packaging>".

  3. Damit im Client ein JUnit-Modultest implementiert werden kann, welcher den REST-Server startet, wird Zugriff auf zwei Klassen benötigt, die beide im Server-Modul enthalten sind, auf die aber nicht so einfach zugegriffen werden kann, da es das Server-Modul bislang nur als WAR-Datei gibt:
    - Zum einen wird die REST-Server-Test-Util-Klasse RestServerTestUtil benötigt.
    - Zum anderen wird die REST-Service-Klasse ContractfirstService benötigt.

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

        <!-- Dependency zu JaxRsContractFirstService-tests.jar,
             welche die Test-Utility-Klasse RestServerTestUtil.class enthaelt: -->
        <dependency>
          <groupId>minirestwebservice</groupId>
          <artifactId>JaxRsContractFirstService</artifactId>
          <version>1.0-SNAPSHOT</version>
          <type>test-jar</type>
          <scope>test</scope>
        </dependency>
        <!-- Dependency zu JaxRsContractFirstService-fuer-test.jar,
             welche die im Client für den Test benoetigte Server-Klasse ContractfirstService.class enthaelt
             (die JAR wird benoetigt, weil diese Klasse nicht aus der WAR-Datei verwendet werden kann): -->
        <dependency>
          <groupId>minirestwebservice</groupId>
          <artifactId>JaxRsContractFirstService</artifactId>
          <classifier>fuer-test</classifier>
          <version>1.0-SNAPSHOT</version>
          <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>3.0.2</version>
            <executions>
              <execution>
                <id>tests</id>
                <goals>
                  <!-- Zur Erzeugung der JaxRsContractFirstService-tests.jar,
                       welche die Test-Utility-Klasse RestServerTestUtil.class enthaelt: -->
                  <goal>test-jar</goal>
                </goals>
              </execution>
              <execution>
                <id>fuer-test</id>
                <goals>
                  <!-- Zur Erzeugung der JaxRsContractFirstService-fuer-test.jar,
                       welche die im Client für den Test benoetigte Server-Klasse ContractfirstService.class enthaelt
                       (die JAR wird benoetigt, weil diese Klasse nicht aus der WAR-Datei verwendet werden kann): -->
                  <goal>jar</goal>
                </goals>
                <configuration>
                  <classifier>fuer-test</classifier>
                </configuration>
              </execution>
            </executions>
          </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.ws.rs.client.*;
    import javax.xml.bind.JAXBElement;
    import contractfirstgenerated.*;
    
    public class ContractFirstClient
    {
       static final String XML_UTF8 = "text/xml; charset=utf-8";
    
       public static void main( String[] args )
       {
          System.out.println( "Es werden vier Parameter benoetigt: Basis-URL, Web-Context-Pfad, i, s.\n"
                            + "Z.B.: http://localhost:8080 /JaxRsContractFirstService/rest/contractfirst 33 aa" );
          if( args.length == 4 ) {
             ResultTO resTO = callContractfirstService( args[0], args[1], Integer.parseInt( args[2] ), args[3] );
             System.out.println( "Ergebnis: resTO.i=" + resTO.getI() + ", resTO.s=" + resTO.getS() );
          }
       }
    
       public static ResultTO callContractfirstService( String baseUrl, String webContextPath, int i, String s )
       {
          InputTO inpTO = new InputTO();
          inpTO.setI( i );
          inpTO.setS( s );
          return callContractfirstService( baseUrl, webContextPath, inpTO );
       }
    
       public static ResultTO callContractfirstService( String baseUrl, String webContextPath, InputTO inpTO )
       {
          WebTarget target = ClientBuilder.newClient().target( baseUrl );
    
          JAXBElement<InputTO> inpJaxb = (new ObjectFactory()).createInputTO( inpTO );
    
          // HTTP-POST zum REST-Service:
          return target.path( webContextPath ).request( XML_UTF8 ).accept( XML_UTF8 ).post(
                Entity.entity( inpJaxb, XML_UTF8 ), ResultTO.class );
       }
    }
    
  6. Erstellen Sie im Testverzeichnis src\test\java\contractfirstclient die JUnit-Modultestklasse: ContractFirstClientTest.java

    package contractfirstclient;
    
    import org.junit.*;
    import contractfirstgenerated.*;
    import contractfirstservice.ContractfirstService;
    import contractfirstservice.RestServerTestUtil;
    
    public class ContractFirstClientTest
    {
       static final String BASE_URL         = "http://localhost:4434";
       static final String WEB_CONTEXT_PATH = "/contractfirst";
    
       @Test
       public void testContractFirstClient()
       {
          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( BASE_URL, ContractfirstService.class ) )
          {
             // Aufruf des REST-Services ueber den REST-Client:
             ResultTO resTO = ContractFirstClient.callContractfirstService( BASE_URL, WEB_CONTEXT_PATH, 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.

  10. Falls Sie den REST-Service JaxRsContractFirstService im Tomcat deployt haben, können Sie folgendermaßen den REST-Service mit dem neuen REST-Client ContractFirstClient per Kommandozeile abrufen (alles in einer einzigen Zeile):

    java -cp target/classes;../JaxRsContractFirstService/target/JaxRsContractFirstService/WEB-INF/lib/* contractfirstclient.ContractFirstClient http://localhost:8080 /JaxRsContractFirstService/rest/contractfirst 33 aa

    Falls Sie WebLogic verwenden: Tauschen Sie die Tomcat-Portnummer 8080 gegen die WebLogic-Portnummer 7001 aus.



Authentifizierung, Integrationstest und Tomcat

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

  1. Installieren Sie Tomcat 9:
    Downloaden Sie apache-tomcat-9.0.1.zip von https://tomcat.apache.org/download-90.cgi.
    Entzippen Sie den Inhalt des apache-tomcat-9.0.1-Verzeichnisses beispielsweise nach D:\Tools\Tomcat.

  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/xsd/maven-4.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>tomcat9x</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.6.5</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>
                  -->
                  <!-- Diese cargo.jvmargs-Konfiguration ist ab Java 9 zwingend notwendig,
                       aber muss fuer Java 8 entfernt werden: -->
                  <cargo.jvmargs>--add-modules java.xml.bind</cargo.jvmargs>
                </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.21.0</version>
             <configuration>
                <excludes>
                   <exclude>**/integrationstest/*Test.java</exclude>
                </excludes>
                <!-- Diese argLine-Konfiguration ist ab Java 9 zwingend notwendig,
                     aber muss fuer Java 8 entfernt werden: -->
                <argLine>--add-modules java.xml.bind</argLine>
             </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.7.0</version>
            <configuration>
              <source>1.8</source>
              <target>1.8</target>
            </configuration>
          </plugin>
         </plugins>
      </build>
      <dependencyManagement>
        <dependencies>
          <dependency>
            <groupId>org.glassfish.jersey</groupId>
            <artifactId>jersey-bom</artifactId>
            <version>2.26</version>
            <type>pom</type>
            <scope>import</scope>
          </dependency>
        </dependencies>
      </dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-servlet-core</artifactId>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-grizzly2-http</artifactId>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.inject</groupId>
          <artifactId>jersey-hk2</artifactId>
        </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.

    Achtung: Beachten Sie die unterschiedliche Konfiguration für Java 8 und Java 9:
    Für Java 9 müssen die beiden oben gezeigten kommentierten Konfigurationen wie gezeigt eingefügt bleiben, aber für Java 8 müssen sie entfernt werden!

  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>JaxRsAuthentication</display-name>
    
      <servlet>
        <servlet-name>REST-Servlet</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
          <param-name>jersey.config.server.provider.packages</param-name>
          <param-value>contractfirstservice</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
      </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.ws.rs.client.*;
    import javax.xml.bind.JAXBElement;
    import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
    import org.junit.*;
    import contractfirstgenerated.*;
    
    public class JaxRsAuthenticationIntegrTest
    {
       @Test
       public void testJaxRsAuthentication() throws Exception
       {
          // Testclient:
          String xmlUtf8        = "text/xml; charset=utf-8";
          String baseUrl        = "http://localhost:8080/JaxRsAuthentication/rest";
          String webContextPath = "/contractfirst";
          String usr            = "MeinName";
          String pwd            = "MeinPasswort";
    
          Client c = ClientBuilder.newClient();
          HttpAuthenticationFeature feature = HttpAuthenticationFeature.basic( usr, pwd );
          c.register( feature );
          WebTarget target = c.target( baseUrl );
    
          // 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 = target.path( webContextPath ).request( xmlUtf8 ).accept( xmlUtf8 ).post(
                Entity.entity( inpJaxb, xmlUtf8 ), ResultTO.class );
          Assert.assertEquals(  84,             resTO.getI() );
          Assert.assertEquals( "abc xyz - ret", resTO.getS() );
    
          // Ohne JAXB und mit XML-Strings:
          String resXml = target.path( webContextPath ).request( xmlUtf8 ).accept( xmlUtf8 ).post(
                Entity.entity( "<inputTO xmlns=\"mein.ns\"><i>42</i><s>abc xyz</s></inputTO>", xmlUtf8 ), String.class );
          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 mit Edge abfragen:

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

    start microsoft-edge: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.



JAX-RS-REST-Service mit embedded Jetty-Server

Jetty-Logo Die folgende Anwendung besteht aus nur zwei Java-Dateien und benötigt keine web.xml. Trotzdem beinhaltet sie einen embedded Jetty-Webserver und einen JAX-RS-REST-Service mit Jersey, und ist direkt standalone ausführbar.

Der JAX-RS-REST-Service wird nicht in den Jetty-Webserver deployt, sondern stattdessen wird umgekehrt der Jetty-Webserver aus der Anwendung gestartet. Die Anwendung wird mit dem maven-shade-plugin zu einem direkt ausführbaren Standalone-Fat-Jar gebündelt, welches beispielsweise als Microservice ausgeführt werden kann.

Doku zu Jetty finden Sie in: Jetty - The Definitive Reference. Das API ist beschrieben in der Jetty-API-Javadoc. Die in diesem Beispiel verwendete Jetty-Version setzt Java 8 voraus. Hinweise zu den verschiedenen Jetty-Versionen finden Sie unter: What Jetty Version Do I Use?.

Doku zu Jersey finden Sie im Jersey User Guide.

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

    cd \MeinWorkspace

    md JaxRsMitJetty

    cd JaxRsMitJetty

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

    tree /F

  2. Erstellen Sie im JaxRsMitJetty-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/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>de.meinefirma.meinprojekt</groupId>
      <artifactId>JaxRsMitJetty</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>jar</packaging>
      <name>${project.artifactId}</name>
      <properties>
        <jetty.version>9.4.7.v20170914</jetty.version>
        <jersey.version>2.26</jersey.version>
        <project.build.sourceEncoding>ISO-8859-1</project.build.sourceEncoding>
      </properties>
      <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.7.0</version>
            <configuration>
              <source>1.8</source>
              <target>1.8</target>
            </configuration>
          </plugin>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.1.0</version>
            <configuration>
              <createDependencyReducedPom>true</createDependencyReducedPom>
              <filters>
                <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                    <exclude>META-INF/*.SF</exclude>
                    <exclude>META-INF/*.DSA</exclude>
                    <exclude>META-INF/*.RSA</exclude>
                  </excludes>
                </filter>
              </filters>
            </configuration>
            <executions>
              <execution>
                <phase>package</phase>
                <goals>
                  <goal>shade</goal>
                </goals>
                <configuration>
                  <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                      <manifestEntries>
                        <Main-Class>de.meinefirma.meinprojekt.MeineApp</Main-Class>
                      </manifestEntries>
                    </transformer>
                  </transformers>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>org.eclipse.jetty</groupId>
          <artifactId>jetty-server</artifactId>
          <version>${jetty.version}</version>
        </dependency>
        <dependency>
          <groupId>org.eclipse.jetty</groupId>
          <artifactId>jetty-servlet</artifactId>
          <version>${jetty.version}</version>
        </dependency>
        <dependency>
          <groupId>org.eclipse.jetty</groupId>
          <artifactId>jetty-util</artifactId>
          <version>${jetty.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.core</groupId>
          <artifactId>jersey-server</artifactId>
          <version>${jersey.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-servlet-core</artifactId>
          <version>${jersey.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-jetty-http</artifactId>
          <version>${jersey.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.media</groupId>
          <artifactId>jersey-media-moxy</artifactId>
          <version>${jersey.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.inject</groupId>
          <artifactId>jersey-hk2</artifactId>
          <version>${jersey.version}</version>
          <exclusions>
            <exclusion>
              <groupId>javax.inject</groupId>
              <artifactId>javax.inject</artifactId>
            </exclusion>
          </exclusions>
        </dependency>
      </dependencies>
    </project>
    
  3. Erstellen Sie im Verzeichnis src\main\java\de\meinefirma\meinprojekt die Hauptanwendungsklasse mit der main()-Methode: MeineApp.java

    package de.meinefirma.meinprojekt;
    
    import org.eclipse.jetty.server.Server;
    import org.eclipse.jetty.servlet.ServletContextHandler;
    import org.eclipse.jetty.servlet.ServletHolder;
    
    public class MeineApp
    {
       public static void main( String[] args ) throws Exception
       {
          // Web-Kontext:
          ServletContextHandler context = new ServletContextHandler( ServletContextHandler.SESSIONS );
          context.setContextPath( "/JaxRsMitJetty" );
    
          // JAX-RS mit Jersey:
          ServletHolder jerseyServlet = context.addServlet( org.glassfish.jersey.servlet.ServletContainer.class, "/rest/*" );
          jerseyServlet.setInitParameter( "jersey.config.server.provider.packages", "de.meinefirma.meinprojekt" );
          jerseyServlet.setInitOrder( 0 );
    
          // Jetty-Server:
          int port = ( args != null && args.length > 0 ) ? Integer.parseInt( args[0] ) : 8080;
          Server jettyServer = new Server( port );
          jettyServer.setHandler( context );
    
          try {
             jettyServer.start();
             jettyServer.join();
          } finally {
             jettyServer.destroy();
          }
       }
    }
    
  4. Erstellen Sie im Verzeichnis src\main\java\de\meinefirma\meinprojekt die REST-Service-Klasse: MeinRestService.java

    package de.meinefirma.meinprojekt;
    
    import javax.ws.rs.*;
    import javax.ws.rs.core.MediaType;
    
    @Path( MeinRestService.webContextPath )
    public class MeinRestService
    {
       public static final String webContextPath = "/helloworld";
    
       @GET @Produces( MediaType.TEXT_PLAIN )
       public String halloPlainText( @QueryParam("name") String name )
       {
          return "Plain-Text: 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>";
       }
    }
    
  5. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\JaxRsMitJetty]
     |- [src]
     |   '- [main]
     |       '- [java]
     |           '- [de]
     |               '- [meinefirma]
     |                   '- [meinprojekt]
     |                       |- MeineApp.java
     |                       '- MeinRestService.java
     '- pom.xml
    
  6. Bauen Sie das Projekt:

    cd \MeinWorkspace\JaxRsMitJetty

    mvn clean package

    dir target\*.?ar

  7. Starten Sie die Anwendung. Dabei wird der embedded Jetty-Webserver mit dem JAX-RS-Jersey-REST-Service gestartet (statt der 8080 können Sie auch eine andere Portnummer wählen).

    Falls Sie Java 8 verwenden, rufen Sie auf:

    java -jar target/JaxRsMitJetty.jar 8080

    Ab Java 9 rufen Sie auf:

    java --add-modules java.xml.bind -jar target/JaxRsMitJetty.jar 8080

  8. Warten Sie bis der Webserver gestartet ist und folgende Zeile erscheint:

    ...INFO:oejs.Server:main: Started ...
    
  9. Rufen Sie den REST-Service ab (ersetzen Sie "ich" durch einen Namen). Verwenden Sie entweder den Webbrowser:

    start http://localhost:8080/JaxRsMitJetty/rest/helloworld?name=ich

    oder cURL:

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

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

  10. Lassen Sie sich die WADL anzeigen:

    curl http://localhost:8080/JaxRsMitJetty/rest/application.wadl



JAX-RS-REST-Service mit dem Jetty-Maven-Plugin

Jetty-Logo Die folgende Anwendung ist sehr ähnlich zum letzten Beispiel JAX-RS-REST-Service mit embedded Jetty-Server (JaxRsMitJetty). Anders als beim letzten Beispiel wird diesmal eine web.xml verwendet und das jetty-maven-plugin eingesetzt.

Doku zu Jetty finden Sie in: Jetty - The Definitive Reference. Das API ist beschrieben in der Jetty-API-Javadoc. Die in diesem Beispiel verwendete Jetty-Version setzt Java 8 voraus. Hinweise zu den verschiedenen Jetty-Versionen finden Sie unter: What Jetty Version Do I Use?.

Doku zu Jersey finden Sie im Jersey User Guide.

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

    cd \MeinWorkspace

    md JaxRsMitJettyMavenPlugin

    cd JaxRsMitJettyMavenPlugin

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

    md src\main\webapp\WEB-INF

    tree /F

  2. Erstellen Sie im JaxRsMitJettyMavenPlugin-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/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>de.meinefirma.meinprojekt</groupId>
      <artifactId>JaxRsMitJettyMavenPlugin</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
      <name>${project.artifactId}</name>
      <properties>
        <jetty.version>9.4.7.v20170914</jetty.version>
        <jersey.version>2.26</jersey.version>
        <project.build.sourceEncoding>ISO-8859-1</project.build.sourceEncoding>
      </properties>
      <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.7.0</version>
            <configuration>
              <source>1.8</source>
              <target>1.8</target>
            </configuration>
          </plugin>
          <plugin>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-maven-plugin</artifactId>
            <version>${jetty.version}</version>
            <configuration>
              <jvmArgs>--add-modules java.xml.bind</jvmArgs>
              <scanIntervalSeconds>10</scanIntervalSeconds>
              <webApp>
                <contextPath>/${project.artifactId}</contextPath>
              </webApp>
              <httpConnector>
                <port>8080</port>
              </httpConnector>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>org.eclipse.jetty</groupId>
          <artifactId>jetty-server</artifactId>
          <version>${jetty.version}</version>
        </dependency>
        <dependency>
          <groupId>org.eclipse.jetty</groupId>
          <artifactId>jetty-servlet</artifactId>
          <version>${jetty.version}</version>
        </dependency>
        <dependency>
          <groupId>org.eclipse.jetty</groupId>
          <artifactId>jetty-util</artifactId>
          <version>${jetty.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.core</groupId>
          <artifactId>jersey-server</artifactId>
          <version>${jersey.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-servlet-core</artifactId>
          <version>${jersey.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-jetty-http</artifactId>
          <version>${jersey.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.media</groupId>
          <artifactId>jersey-media-moxy</artifactId>
          <version>${jersey.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.inject</groupId>
          <artifactId>jersey-hk2</artifactId>
          <version>${jersey.version}</version>
        </dependency>
      </dependencies>
    </project>
    
  3. Erstellen Sie im Verzeichnis src\main\webapp\WEB-INF die Web-App-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>
        <servlet-name>REST-Servlet</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
          <param-name>jersey.config.server.provider.packages</param-name>
          <param-value>de.meinefirma.meinprojekt</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
      </servlet>
      <servlet-mapping>
        <servlet-name>REST-Servlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
      </servlet-mapping>
    </web-app>
    
  4. Erstellen Sie im Verzeichnis src\main\java\de\meinefirma\meinprojekt die REST-Service-Klasse: MeinRestService.java

    package de.meinefirma.meinprojekt;
    
    import javax.ws.rs.*;
    import javax.ws.rs.core.MediaType;
    
    /** http://localhost:8080/JaxRsMitJettyMavenPlugin/rest/helloworld?name=ich */
    @Path( MeinRestService.webContextPath )
    public class MeinRestService
    {
       public static final String webContextPath = "/helloworld";
    
       @GET @Produces( MediaType.TEXT_PLAIN )
       public String halloPlainText( @QueryParam("name") String name )
       {
          return "Plain-Text: 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>";
       }
    }
    
  5. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\JaxRsMitJettyMavenPlugin]
     |- [src]
     |   '- [main]
     |       |- [java]
     |       |   '- [de]
     |       |       '- [meinefirma]
     |       |           '- [meinprojekt]
     |       |               '- MeinRestService.java
     |       '- [webapp]
     |           '- [WEB-INF]
     |               '- web.xml
     '- pom.xml
    
  6. Bauen Sie das Projekt:

    cd \MeinWorkspace\JaxRsMitJettyMavenPlugin

    mvn clean package

    dir target\*.?ar

    Sie erhalten eine WAR-Datei, die Sie in einen Servlet-Container oder Application Server (z.B. WebLogic 12.2.1) deployen könnten.

  7. Alternativ starten Sie mit dem jetty-maven-plugin einen Jetty-Server mit der deployten Anwendung.

    Falls Sie Java 8 verwenden, rufen Sie auf:

    mvn jetty:run

    Falls Sie Java 9 verwenden, rufen Sie auf:

    mvn jetty:run-forked

    Warten Sie bis der Webserver gestartet ist und folgende Zeile erscheint:

    [INFO] Started Jetty Server
    
  8. Rufen Sie den REST-Service ab (ersetzen Sie "ich" durch einen Namen). Verwenden Sie entweder den Webbrowser:

    start http://localhost:8080/JaxRsMitJettyMavenPlugin/rest/helloworld?name=ich

    oder cURL:

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

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

  9. Lassen Sie sich die WADL anzeigen:

    curl http://localhost:8080/JaxRsMitJettyMavenPlugin/rest/application.wadl



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

Maven-Webprojekt für die Bücherverwaltung

  1. Tomcat-Logo Installieren Sie Tomcat 9:
    Downloaden Sie apache-tomcat-9.0.1.zip von https://tomcat.apache.org/download-90.cgi.
    Entzippen Sie den Inhalt des apache-tomcat-9.0.1-Verzeichnisses beispielsweise nach D:\Tools\Tomcat.

  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 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/xsd/maven-4.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.7.0</version>
            <configuration>
              <source>1.8</source>
              <target>1.8</target>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencyManagement>
        <dependencies>
          <dependency>
            <groupId>org.glassfish.jersey</groupId>
            <artifactId>jersey-bom</artifactId>
            <version>2.26</version>
            <type>pom</type>
            <scope>import</scope>
          </dependency>
        </dependencies>
      </dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-servlet-core</artifactId>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-grizzly2-http</artifactId>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.inject</groupId>
          <artifactId>jersey-hk2</artifactId>
        </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>
        <servlet-name>REST-Servlet</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
          <param-name>jersey.config.server.provider.packages</param-name>
          <param-value>de.meinefirma.meinprojekt.rest</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
      </servlet>
      <servlet-mapping>
        <servlet-name>REST-Servlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
      </servlet-mapping>
    </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<>();
    
       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<>();
       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<>();
          List<BuchDO> snapshotList;
          if( id == null && isEmpty( isbn ) && isEmpty( titel ) )
             return Collections.unmodifiableList( new ArrayList<>( 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<>( 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 );
          }
       }
    
       // Alle Buecher loeschen:
       public void deleteAlleBuecher()
       {
          synchronized( buecherPool ) {
             buecherPool.clear();
          }
       }
    
       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 e ) {/*ok*/}
          return null;
       }
    
       private static Double doubleFromString( String s )
       {
          if( !BuecherUtil.isEmpty( s ) ) try { return new Double( s.trim() ); } catch( NumberFormatException e ) {/*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).

    Falls Sie Java 8 verwenden, setzen Sie:

    set "JAVA_OPTS="

    Ab Java 9 setzen Sie:

    set "JAVA_OPTS=--add-modules=ALL-SYSTEM"

    Fahren Sie fort mit:

    cd \MeinWorkspace\JaxRsBuecherverwaltung

    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

    Warten Sie ein paar Sekunden, bis das Deployment fertig ist.

    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.client.*;
    import javax.ws.rs.core.*;
    
    /** RESTful-Webservice-Client */
    public class BuecherRestClient
    {
       public static void main( String[] args )
       {
          Client c = ClientBuilder.newClient();
          WebTarget webTarget = c.target( "http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher" );
          Response response = null;
    
          if( args.length > 1 && args[0].toLowerCase().equals( "getbuchbyid" ) ) {
             System.out.println( "\n--- Buch mit ID " + args[1] + ":" );
             System.out.println( getBuchById( webTarget, args[1] ) );
          }
          else if( args.length > 2 && args[0].toLowerCase().equals( "findebuecher" ) ) {
             System.out.println( "\n--- Finde Buecher mit ISBN " + args[1] + " oder Titel '" + args[2] + "':" );
             System.out.println( findeBuecher( webTarget, null, args[1], args[2] ) );
          }
          else if( args.length > 4 && args[0].toLowerCase().equals( "putbuch" ) ) {
             System.out.println( "\n--- Aendere Daten zum Buch mit der ID " + args[1] + ":" );
             response = putBuch( webTarget, args[1], args[2], args[3], args[4] );
          }
          else if( args.length > 3 && args[0].toLowerCase().equals( "postbuch" ) ) {
             System.out.println( "\n--- Neues Buch anlegen:" );
             response = postBuch( webTarget, args[1], args[2], args[3] );
          }
          else if( args.length > 1 && args[0].toLowerCase().equals( "deletebuch" ) ) {
             System.out.println( "\n--- Loesche Buch mit ID " + args[1] + ":" );
             response = deleteBuch( webTarget, args[1] );
          }
          if( response != null ) {
             System.out.println( response.getStatus() + " " + response.getStatusInfo() );
             System.out.println( response );
          }
       }
    
       static String getBuchById( WebTarget webTarget, String id )
       {
          return webTarget.path( id ).request().get( String.class );
       }
    
       static String findeBuecher( WebTarget webTarget, String id, String isbn, String titel )
       {
          WebTarget wt = webTarget;
          if( id    != null ) wt = wt.queryParam( "id",    id );
          if( isbn  != null ) wt = wt.queryParam( "isbn",  isbn );
          if( titel != null ) wt = wt.queryParam( "titel", titel );
          return wt.request().get( String.class );
       }
    
       static Response putBuch( WebTarget webTarget, String id, String isbn, String titel, String preis )
       {
          MultivaluedMap<String,String> formData = new MultivaluedHashMap<>();
          formData.add( "isbn",  isbn );
          formData.add( "titel", titel );
          formData.add( "preis", preis );
          return webTarget.path( id ).request().put( Entity.form( formData ) );
       }
    
       static Response postBuch( WebTarget webTarget, String isbn, String titel, String preis )
       {
          MultivaluedMap<String,String> formData = new MultivaluedHashMap<>();
          formData.add( "isbn",  isbn );
          formData.add( "titel", titel );
          formData.add( "preis", preis );
          return webTarget.request().post( Entity.form( formData ) );
       }
    
       static Response deleteBuch( WebTarget webTarget, String id )
       {
          return webTarget.path( id ).request().delete();
       }
    }
    
  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 _BROWSER_PREFIX=microsoft-edge:
    set _MEIN_PROJEKT_NAME=JaxRsBuecherverwaltung
    set CLASSPATH=target/%_MEIN_PROJEKT_NAME%/WEB-INF/classes;target/%_MEIN_PROJEKT_NAME%/WEB-INF/lib/*
    
    @echo off
    @for /f tokens^=2-5^ delims^=.-+_^" %%j in ('java -fullversion 2^>^&1') do (
       set "JVER_FULL=%%j.%%k.%%l"
       set "JVER_MAIN=%%j"
    )
    set JAVA_OPTS=
    set JAVA_OPTS_CLIENT=
    if not %JVER_MAIN% == 8 (
      set "JAVA_OPTS=--add-modules=ALL-SYSTEM"
      set "JAVA_OPTS_CLIENT=--add-modules=java.xml.bind"
    )
    @echo on
    @echo Java-Version:     %JVER_MAIN% (%JVER_FULL%)
    @echo JAVA_OPTS:        %JAVA_OPTS%
    @echo JAVA_OPTS_CLIENT: %JAVA_OPTS_CLIENT%
    @echo TOMCAT_HOME:      %TOMCAT_HOME%
    
    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. 16 Sekunden warten (eventuell Wartezeit anpassen)
    @ping -n 16 127.0.0.1 >nul
    @echo.
    @echo .................................................................
    java %JAVA_OPTS_CLIENT% de.meinefirma.meinprojekt.client.BuecherRestClient postBuch 1234567891 "MeinTitel1" 12.34
    java %JAVA_OPTS_CLIENT% de.meinefirma.meinprojekt.client.BuecherRestClient postBuch 1234567892 "MeinTitel2" 22.34
    @echo.
    start %_BROWSER_PREFIX%http://localhost:8080/%_MEIN_PROJEKT_NAME%/rest/Artikel/Buecher
    curl http://localhost:8080/%_MEIN_PROJEKT_NAME%/rest/Artikel/Buecher
    @echo.
    @echo.
    @ping -n 2 127.0.0.1 >nul
    java %JAVA_OPTS_CLIENT% de.meinefirma.meinprojekt.client.BuecherRestClient getBuchById 4711
    java %JAVA_OPTS_CLIENT% de.meinefirma.meinprojekt.client.BuecherRestClient findeBuecher 1234567891 "MeinTitel2"
    java %JAVA_OPTS_CLIENT% de.meinefirma.meinprojekt.client.BuecherRestClient putBuch 4712 1234567898 "Client-PUT-Titel" 111
    java %JAVA_OPTS_CLIENT% de.meinefirma.meinprojekt.client.BuecherRestClient postBuch 9876543210 "Client-POST-Titel" 222
    java %JAVA_OPTS_CLIENT% de.meinefirma.meinprojekt.client.BuecherRestClient deleteBuch 4711
    @echo.
    start %_BROWSER_PREFIX%http://localhost:8080/%_MEIN_PROJEKT_NAME%/rest/Artikel/Buecher
    curl http://localhost:8080/%_MEIN_PROJEKT_NAME%/rest/Artikel/Buecher
    @echo.
    @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.

    Der Browser-Prefix "microsoft-edge:" funktioniert natürlich nur, wenn Sie den Microsoft-Edge-Webbrowser installiert haben.

  3. Falls Sie Java 9 oder höher verwenden:

    Beachten Sie, wie in der Batchdatei für den Tomcat-Aufruf die Umgebungsvariable JAVA_OPTS auf --add-modules=java.se.ee gesetzt wird.
    Und beachten Sie, dass der BuecherRestClient nicht mit --add-modules=java.se.ee gestartet werden kann, sondern stattdessen mit --add-modules=java.xml.bind gestartet werden muss, weil Sie sonst eine Exception erhalten würden:

    java.lang.NoClassDefFoundError: javax/annotation/Priority
    Caused by: java.lang.ClassNotFoundException: javax.annotation.Priority

    Sehen Sie sich hierzu die Erläuterungen an unter: Problem mit "add-modules".

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 weiter unten beschrieben.
    Lassen Sie sich nach jeder Änderung die Liste aller Bücher anzeigen über:

    start microsoft-edge:http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher

    Starten Sie Tomcat.

    Falls Sie Java 8 verwenden, setzen Sie:

    set "JAVA_OPTS="

    Ab Java 9 setzen Sie:

    set "JAVA_OPTS=--add-modules=ALL-SYSTEM"

    Fahren Sie fort mit:

    pushd .

    cd /D D:\Tools\Tomcat\bin

    startup.bat

    popd

  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.



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

Im Folgenden wird das letzte Beispiel (REST-Webservice mit JAX-RS) 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 );
       void loescheAlleBuecher();
    }
    
  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
       {
          BuchDO buNeu = dao.createBuch( bu );
          return BuecherUtil.erzeugeBuecherTO( "Buch hinzugefuegt", buNeu );
       }
    
       @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() );
       }
    
       @Override public void loescheAlleBuecher()
       {
          dao.deleteAlleBuecher();
       }
    }
    
  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>
        <servlet-name>REST-Servlet</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
          <param-name>jersey.config.server.provider.packages</param-name>
          <param-value>de.meinefirma.meinprojekt.rest</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
      </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>
    </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.ws.rs.client.*;
    import javax.xml.namespace.QName;
    import javax.xml.ws.*;
    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] ); // $FALL-THROUGH$
             default:
          }
          // SOAP- und REST-SUT (System under Test):
          SoapSut soapSut = new SoapSut();
          test( soapSut,       "SOAP", urlBasis + "/ws/BuecherSoapService", anzahlBuecher, true );
          soapSut.loescheAlleBuecher();
          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
    {
       WebTarget webTarget;
    
       @Override public void initialize( String url ) throws Exception
       {
          Client c = ClientBuilder.newClient();
          webTarget = c.target( url );
          webTarget.request().get( BuecherTO.class );
          Thread.sleep( 1000 );
       }
    
       @Override public BuecherTO createBuch( BuchDO bu ) throws Exception
       {
          return webTarget.request( "text/xml; charset=utf-8" ).post( Entity.entity( bu, "text/xml; charset=utf-8" ), BuecherTO.class );
       }
    
       @Override public BuecherTO getBuchById( Long id )
       {
          return webTarget.path( "" + id ).request().get( BuecherTO.class );
       }
    
       @Override public BuecherTO findeBuecher( BuchDO bu )
       {
          return webTarget.request().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;
                   Thread.sleep( 1000 );
                }
             }
             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 );
       }
    
       public void loescheAlleBuecher()
       {
          buecherService.loescheAlleBuecher();
       }
    }
    
    // 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.



JAX-RS-REST-Service mit Dropwizard

Dropwizard ist ein Java-Framework für die Entwicklung performanter Operating-freundlicher RESTful-Webservices. Es ist besonders gut zur Entwicklung von Microservices geeignet. In Teilbereichen konkurriert es mit Spring Boot.

Dropwizard verwendet den Webserver Jetty, JAX-RS mit Jersey, JSON mit Jackson, Validierung mit Hibernate Validator, Resilience mit Hystrix und Tenacity, Metrics, OAuth2, Logback, SLF4J, und zur Konfiguration das YAML-Format.

Das folgende Beispiel konzentriert sich auf nur wenige Features von Dropwizard. Hauptsächlich implementiert es einen JAX-RS-Jersey-REST-Service, inklusive JUnit-Modultest, Health-Check und Ausgabe vieler Metriken. Die Ergebnisse werden im JSON-Format geliefert.

Der JAX-RS-REST-Service wird nicht in den Jetty-Webserver deployt, sondern stattdessen wird umgekehrt der Jetty-Webserver aus der Anwendung gestartet. Die Anwendung wird mit dem maven-shade-plugin und dem maven-jar-plugin zu einem direkt ausführbaren Standalone-Fat-Jar gebündelt, welches beispielsweise als Microservice ausgeführt werden kann.

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

    cd \MeinWorkspace

    md DropwizardDemo

    cd DropwizardDemo

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

    md src\test\java\de\meinefirma\meinprojekt

    tree /F

  2. Erstellen Sie im DropwizardDemo-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/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>de.meinefirma.meinprojekt</groupId>
      <artifactId>DropwizardDemo</artifactId>
      <version>1.0-SNAPSHOT</version>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.1.0</version>
            <executions>
              <execution>
                <phase>package</phase>
                <goals>
                  <goal>shade</goal>
                </goals>
                <configuration>
                  <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                      <mainClass>de.meinefirma.meinprojekt.MeineApp</mainClass>
                    </transformer>
                  </transformers>
                </configuration>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.0.2</version>
            <configuration>
              <archive>
                <manifest>
                  <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                </manifest>
              </archive>
            </configuration>
          </plugin>
          <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.7.0</version>
            <configuration>
              <source>1.8</source>
              <target>1.8</target>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>io.dropwizard</groupId>
          <artifactId>dropwizard-core</artifactId>
          <version>1.2.0</version>
        </dependency>
        <dependency>
          <groupId>io.dropwizard</groupId>
          <artifactId>dropwizard-testing</artifactId>
          <version>1.2.0</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
        </dependency>
      </dependencies>
    </project>
    
  3. Erstellen Sie im DropwizardDemo-Projektverzeichnis die Dropwizard-Konfigurationsdatei (im YAML-Format): local.config.yml

    meinConfigParm: AbcXyz
    
    server:
      applicationContextPath: /DropwizardDemo
      applicationConnectors:
        - type: http
          port: 8080
      adminConnectors:
        - type: http
          port: 8081
    
    logging:
      level: INFO
      appenders:
        - type: console
        - type: file
          threshold: DEBUG
          logFormat: "%-6level [%d{HH:mm:ss.SSS}] [%t] %logger{5} - %X{code} %msg %n"
          currentLogFilename: ./logs/application.log
          archivedLogFilenamePattern: ./logs/application-%d{yyyy-MM-dd}.log
          archivedFileCount: 7
          timeZone: UTC
    
  4. Erstellen Sie im Verzeichnis src\main\java\de\meinefirma\meinprojekt die folgenden fünf Java-Klassen:

    Konfigurationsparameter: MeineConfiguration.java

    package de.meinefirma.meinprojekt;
    
    import org.hibernate.validator.constraints.NotEmpty;
    import io.dropwizard.Configuration;
    
    public class MeineConfiguration extends Configuration
    {
       @NotEmpty
       private String meinConfigParm;
    
       public String getMeinConfigParm() { return meinConfigParm; }
    
       public void setMeinConfigParm( String meinConfigParm ) { this.meinConfigParm = meinConfigParm; }
    }
    

    Ergebnis-Transferobjekt: ResultTO.java

    package de.meinefirma.meinprojekt;
    
    public class ResultTO
    {
       private String meinConfigParm;
       private String meinAufrufParm;
    
       public ResultTO() {}
    
       public ResultTO( String meinConfigParm, String meinAufrufParm ) {
          this.meinConfigParm = meinConfigParm;
          this.meinAufrufParm = meinAufrufParm;
       }
    
       public String getMeinConfigParm() { return meinConfigParm; }
       public String getMeinAufrufParm() { return meinAufrufParm; }
    
       public void setMeinConfigParm( String meinConfigParm ) { this.meinConfigParm = meinConfigParm; }
       public void setMeinAufrufParm( String meinAufrufParm ) { this.meinAufrufParm = meinAufrufParm; }
    }
    

    REST-Service: MeinRestService.java

    package de.meinefirma.meinprojekt;
    
    import javax.ws.rs.*;
    import javax.ws.rs.core.MediaType;
    import com.codahale.metrics.annotation.Timed;
    
    @Path( MeinRestService.meinWebContextPfad )
    @Produces( MediaType.APPLICATION_JSON )
    public class MeinRestService
    {
       public static final String meinWebContextPfad = "/zeigeparms";
       private final String meinConfigParm;
    
       public MeinRestService( String meinConfigParm )
       {
          this.meinConfigParm = meinConfigParm;
       }
    
       @GET
       @Timed
       public ResultTO zeigeParms( @QueryParam("meinAufrufParm") String meinAufrufParm )
       {
          return new ResultTO( meinConfigParm, meinAufrufParm );
       }
    }
    

    Health-Check: MeinRestServiceHealthCheck.java

    package de.meinefirma.meinprojekt;
    
    import com.codahale.metrics.health.HealthCheck;
    
    public class MeinRestServiceHealthCheck extends HealthCheck
    {
       private final String meinConfigParm;
    
       public MeinRestServiceHealthCheck( String meinConfigParm )
       {
          this.meinConfigParm = meinConfigParm;
       }
    
       @Override
       protected Result check() throws Exception
       {
          if( meinConfigParm == null ) {
             return Result.unhealthy( "MeinConfigParm ist nicht definiert." );
          }
          if( meinConfigParm.length() < 2 ) {
             return Result.unhealthy( "MeinConfigParm ist zu kurz." );
          }
          if( !Character.isLetter( meinConfigParm.charAt( 0 ) ) ) {
             return Result.unhealthy( "MeinConfigParm beginnt nicht mit einem Buchstaben." );
          }
          return Result.healthy();
       }
    }
    

    Hauptanwendungsklasse mit der main()-Methode: MeineApp.java

    package de.meinefirma.meinprojekt;
    
    import io.dropwizard.Application;
    import io.dropwizard.setup.Environment;
    
    public class MeineApp extends Application<MeineConfiguration>
    {
       public static void main( String[] args ) throws Exception
       {
          new MeineApp().run( args );
       }
    
       @Override
       public void run( MeineConfiguration conf, Environment env ) throws Exception
       {
          MeinRestService meinRestService = new MeinRestService( conf.getMeinConfigParm() );
          MeinRestServiceHealthCheck meinHealthCheck = new MeinRestServiceHealthCheck( conf.getMeinConfigParm() );
          env.jersey().register( meinRestService );
          env.healthChecks().register( "MeinRestServiceHealthCheck", meinHealthCheck );
       }
    }
    
  5. Erstellen Sie im Testverzeichnis src\test\java\de\meinefirma\meinprojekt die JUnit-Modultestklasse: MeinRestServiceTest.java

    package de.meinefirma.meinprojekt;
    
    import io.dropwizard.testing.junit.DropwizardClientRule;
    import javax.ws.rs.client.*;
    import org.junit.*;
    
    public class MeinRestServiceTest
    {
       private static final String MEIN_CONFIG_PARM = "Test-Parm";
       private final Client client = ClientBuilder.newClient();
    
       @ClassRule
       public final static DropwizardClientRule dwClntRule = new DropwizardClientRule( new MeinRestService( MEIN_CONFIG_PARM ) );
    
       @Test
       public void testeMeinRestService() throws Exception
       {
          WebTarget target = client.target( dwClntRule.baseUri() ).path( MeinRestService.meinWebContextPfad );
          ResultTO response = target.request().get( ResultTO.class );
          Assert.assertEquals( MEIN_CONFIG_PARM, response.getMeinConfigParm() );
       }
    }
    
  6. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\DropwizardDemo]
     |- [src]
     |   |- [main]
     |   |   '- [java]
     |   |       '- [de]
     |   |           '- [meinefirma]
     |   |               '- [meinprojekt]
     |   |                   |- MeineApp.java
     |   |                   |- MeineConfiguration.java
     |   |                   |- MeinRestService.java
     |   |                   |- MeinRestServiceHealthCheck.java
     |   |                   '- ResultTO.java
     |   '- [test]
     |       '- [java]
     |           '- [de]
     |               '- [meinefirma]
     |                   '- [meinprojekt]
     |                       '- MeinRestServiceTest.java
     |- local.config.yml
     '- pom.xml
    
  7. Bauen Sie das Projekt und führen Sie dabei den JUnit-Modultest aus, und starten Sie die Anwendung, dabei wird der embedded Webserver mit dem REST-Service gestartet.

    Falls Sie Java 8 verwenden, rufen Sie auf:

    cd \MeinWorkspace\DropwizardDemo

    mvn clean package

    java -jar target/DropwizardDemo-1.0-SNAPSHOT.jar server local.config.yml

    Ab Java 9 rufen Sie auf:

    cd \MeinWorkspace\DropwizardDemo

    mvn -DargLine="--add-modules java.xml.bind" clean package

    java --add-modules java.xml.bind -jar target/DropwizardDemo-1.0-SNAPSHOT.jar server local.config.yml

  8. Warten Sie bis der Webserver gestartet ist und folgende Zeile erscheint:

    INFO  [...] org.eclipse.jetty.server.Server: Started ...
    
  9. Öffnen Sie folgende URLs im Webbrowser und sehen Sie sich die Ergebnisse im JSON-Format an:

    start http://localhost:8080/DropwizardDemo/zeigeparms?meinAufrufParm=Hallo

    {"meinConfigParm":"AbcXyz","meinAufrufParm":"Hallo"}
    

    start http://localhost:8081/healthcheck?pretty=true

    {
      "MeinRestServiceHealthCheck" : {
        "healthy" : true
      },
      "deadlocks" : {
        "healthy" : true
      }
    }
    

    start http://localhost:8081/metrics?pretty=true

    {
      "version" : "3.0.0",
      "gauges" : {
        ...
        "jvm.memory.heap.committed" : {
          "value" : 156237824
        },
        "jvm.memory.heap.init" : {
          "value" : 201326592
        },
        "jvm.memory.heap.max" : {
          "value" : 2861563904
        },
        "jvm.memory.heap.usage" : {
          "value" : 0.007411276040473846
        },
        "jvm.memory.heap.used" : {
          "value" : 21207840
        },
        ...
      },
      "histograms" : { },
      "meters" : {
        ...
      },
      "timers" : {
        "de.meinefirma.meinprojekt.MeinRestService.zeigeParms" : {
          "count" : 1,
          "max" : 0.007328108000000001,
          "mean" : 0.007328108000000001,
          "min" : 0.007328108000000001,
          ...
        },
        ...
      }
    }
    

    Beachten Sie, dass die Annotation @Timed in der REST-Service-Klasse MeinRestService.java dazu führt, dass bei den Metriken die Laufzeiten dieser Methode unter "timers" angezeigt wird.

  10. Alternativ zur JSON-Webschnittstelle können Sie die Metriken auch über JMX-MBeans abrufen, beispielsweise mit JConsole:
    Dropwizard-JConsole


Der Kommandozeilen-JSON-Prozessor jp

"jp" ist ein "lightweight and flexible command-line JSON processor", also ein leichtgewichtiger flexibler Kommandozeilen-JSON-Prozessor. Sie können ihn downloaden unter https://stedolan.github.io/jq/.

Im Folgenden wird nicht auf die vielen Features von jp eingegangen. Sehen Sie sich hierzu das jq Tutorial und das ausführliche jq Manual an.

Hier wird jp lediglich als sehr praktisches Tool vorgestellt, mit dem sehr einfach und bequem die Antworten von JSON-REST-Schnittstellen ausgewertet werden können.

Es folgen ein paar konkrete Beispiele zur Anwendung von jp unter Windows (unter Linux ist die Syntax sehr ähnlich) (Infos zu den hier verwendeten JSON-REST-Schnittstellen finden Sie im folgenden Kapitel).

Fragen Sie die unformatierte JSON-Antwort vom Euro-Kurs zum Dollar und zum Britischen Pfund ab, vom Fixer.io-JSON-REST-Service:

curl http://api.fixer.io/latest?symbols=USD,GBP

{"base":"EUR","date":"2017-10-20","rates":{"GBP":0.89623,"USD":1.1818}}

Formatieren Sie die JSON-Ausgabe, damit sie leichter lesbar ist:

curl -s http://api.fixer.io/latest?symbols=USD,GBP | jq-win64

{
  "base": "EUR",
  "date": "2017-10-20",
  "rates": {
    "GBP": 0.89623,
    "USD": 1.1818
  }
}

Extrahieren Sie einen bestimmten Wert aus dem JSON-Ergebnis (in diesem Beispiel den Dollar-Kurs zum Euro):

curl -s http://api.fixer.io/latest?symbols=USD,GBP | jq-win64 .rates.USD

1.1818

Testen Sie analoge Kommandos für den Bitcoin-Kurs mit dem Blockchain.info-JSON-REST-Service:

curl http://blockchain.info/de/ticker

curl -s http://blockchain.info/de/ticker | jq-win64

curl -s http://blockchain.info/de/ticker | jq-win64 -r .EUR

curl -s http://blockchain.info/de/ticker | jq-win64 -r .EUR.last

curl -s http://blockchain.info/de/ticker | jq-win64 -r ".EUR|((.last|tostring)+\" \"+.symbol)" > Bitcoin.txt

type Bitcoin.txt

Testen Sie analoge Kommandos für den Bitcoin-Kurs mit dem Coindesk.com-JSON-REST-Service:

curl http://api.coindesk.com/v1/bpi/currentprice.json

curl -s http://api.coindesk.com/v1/bpi/currentprice.json | jq-win64

curl -s http://api.coindesk.com/v1/bpi/currentprice.json | jq-win64 -r .bpi.EUR.rate

Testen Sie den JSON-REST-Service zur Ermittlung von Länderkürzeln (im Beispiel zu "Fiji") mit dem GroupKT.com-JSON-REST-Service:

curl http://services.groupkt.com/country/get/all

curl -s http://services.groupkt.com/country/get/all | jq-win64 ".RestResponse.result[] | select(.name==\"Fiji\")"

curl -s http://services.groupkt.com/country/get/all | jq-win64 ".RestResponse.result[] | select(.name==\"Fiji\") .alpha2_code"

Testen Sie den JSON-REST-Service zur Ermittlung des Geschlechts zu Vornamen (im Beispiel zu "Kim") mit dem Genderize.io-JSON-REST-Service:

curl -s https://api.genderize.io/?name=Kim | jq-win64

curl -s https://api.genderize.io/?name=Kim | jq-win64 -r .gender


JSON-REST-Client mit HttpURLConnection und JsonObject

Außer JAX-RS gibt es viele weitere Alternativen, um REST-Clients zu implementieren. Eine mögliche Alternative ist, HttpURLConnection zu verwenden. Zur Auswertung der JSON-Ergebnisse ist JsonObject sehr gut geeignet.

Das folgende Beispiel demonstriert:

Unter Spring-Boot mit REST-Client mit JsonObject finden Sie sehr ähnliche Beispiele, allerdings wird dort nicht HttpURLConnection verwendet, sondern stattdessen das Spring RestTemplate.

Projektbasis und JsonObject-/HttpURLConnection-Utility

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

    cd \MeinWorkspace

    md JsonRestClientMitHttpURLConnection

    cd JsonRestClientMitHttpURLConnection

    md src\main\java\restclient

    tree /F

  2. Erstellen Sie im JsonRestClientMitHttpURLConnection-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/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>restclient</groupId>
      <artifactId>JsonRestClientMitHttpURLConnection</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>jar</packaging>
      <build>
        <finalName>RestClient</finalName>
        <plugins>
          <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.7.0</version>
            <configuration>
              <source>1.8</source>
              <target>1.8</target>
            </configuration>
          </plugin>
          <plugin>
            <artifactId>maven-assembly-plugin</artifactId>
            <version>3.1.0</version>
            <configuration>
              <descriptorRefs>
                <descriptorRef>jar-with-dependencies</descriptorRef>
              </descriptorRefs>
              <archive>
                <manifest>
                  <!-- Hier die gewuenschte Main-Klasse eintragen: -->
                  <mainClass>restclient.BitcoinRestClient</mainClass>
                </manifest>
              </archive>
            </configuration>
            <executions>
              <execution>
                <id>make-assembly</id>
                <phase>package</phase>
                <goals>
                  <goal>single</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>org.glassfish</groupId>
          <artifactId>javax.json</artifactId>
          <version>1.1</version>
        </dependency>
      </dependencies>
    </project>
    
  3. Erstellen Sie im src\main\java\restclient-Verzeichnis die JsonObject-/HttpURLConnection-Utility-Klasse: JsonObjectFromUrlUtil.java

    package restclient;
    
    import javax.json.*;
    import java.io.*;
    import java.net.*;
    
    public class JsonObjectFromUrlUtil
    {
       public static JsonObject getJsonObjectFromUrl( String url )
       {
          try {
             HttpURLConnection conn = (HttpURLConnection) (new URL( url )).openConnection();
             conn.setRequestProperty( "Accept", "application/json" );
             if( conn.getResponseCode() != HttpURLConnection.HTTP_OK ) {
                throw new RuntimeException( "Problem mit der Url " + url + ": " + conn.getResponseMessage() );
             }
             try( JsonReader jsonRdr = Json.createReader( (InputStream) conn.getContent() ) ) {
                return jsonRdr.readObject();
             }
          } catch( IOException ex ) {
             throw new RuntimeException( "Problem mit der Url: " + url, ex );
          }
       }
    }
    

JSON-REST-Client für den Dollar-Kurs

  1. Erstellen Sie im src\main\java\restclient-Verzeichnis die REST-Client-Klasse: DollarKursRestClient.java

    package restclient;
    
    import javax.json.*;
    
    /**
     * JSON-REST-Client fuer den Dollar-Kurs,
     * {@link "http://fixer.io"}
     * {@link "http://api.fixer.io/latest?symbols=USD,GBP"}
     */
    public class DollarKursRestClient
    {
       public static void main( String[] args )
       {
          final String url = "http://api.fixer.io/latest?symbols=USD,GBP";
          JsonObject jsonObj = JsonObjectFromUrlUtil.getJsonObjectFromUrl( url );
    
          System.out.println( "\n\n------------ Ausgabe aller Root-Key/Values:\n" );
          jsonObj.entrySet().forEach( e -> System.out.println( "key=" + e.getKey() + ", val=" + e.getValue() + "\n" ) );
    
          System.out.println( "\n------------ Ausgabe aller Key/Values zu 'rates':\n" );
          JsonObject rates = jsonObj.getJsonObject( "rates" );
          rates.entrySet().forEach( e -> System.out.println( "key=" + e.getKey() + ", val=" + e.getValue() + "\n" ) );
    
          System.out.println( "\n------------ Ermittlung einzelner Elemente:\n" );
          String     date    = jsonObj.getString( "date" );
          String     base    = jsonObj.getString( "base" );
          JsonNumber rateUSD = rates.getJsonNumber( "USD" );
          JsonNumber rateGBP = rates.getJsonNumber( "GBP" );
          System.out.printf(
                "Ein %s kostet %.3f USD bzw. %.3f GBP (%s).\n", base, rateUSD.doubleValue(), rateGBP.doubleValue(), date );
    
          System.out.println( "\n-----------------------------------------------------\n\n" );
       }
    }
    
  2. Bauen Sie das Projekt und führen Sie es aus:

    cd \MeinWorkspace\JsonRestClientMitHttpURLConnection

    mvn clean package

    java -cp target\RestClient-jar-with-dependencies.jar restclient.DollarKursRestClient

    Sie erhalten (gekürzt):

    ------------ Ausgabe aller Root-Key/Values:
    
    key=base, val="EUR"
    key=date, val="2017-07-21"
    key=rates, val={"GBP":0.8961,"USD":1.1642}
    
    ------------ Ausgabe aller Key/Values zu 'rates':
    
    key=GBP, val=0.8961
    key=USD, val=1.1642
    
    ------------ Ermittlung einzelner Elemente:
    
    Ein EUR kostet 1,164 USD bzw. 0,896 GBP (2017-07-21).
    
    -----------------------------------------------------
    

JSON-REST-Client für den Bitcoin-Kurs

  1. Erstellen Sie im src\main\java\restclient-Verzeichnis die REST-Client-Klasse: BitcoinRestClient.java

    package restclient;
    
    import javax.json.*;
    
    /**
     * JSON-REST-Client fuer den Bitcoin-Kurs, Powered by CoinDesk,
     * {@link "http://www.coindesk.com/api/"}
     * {@link "http://www.coindesk.com/price/"}
     * {@link "http://api.coindesk.com/v1/bpi/currentprice.json"}
     */
    public class BitcoinRestClient
    {
       public static void main( String[] args )
       {
          final String url = "http://api.coindesk.com/v1/bpi/currentprice.json";
          JsonObject jsonObj = JsonObjectFromUrlUtil.getJsonObjectFromUrl( url );
    
          System.out.println( "\n\n------------ Ausgabe aller Root-Key/Values:\n" );
          jsonObj.entrySet().forEach( e -> System.out.println( "key=" + e.getKey() + ", val=" + e.getValue() + "\n" ) );
    
          System.out.println( "\n------------ Ausgabe aller Key/Values zu 'time':\n" );
          JsonObject time = jsonObj.getJsonObject( "time" );
          time.entrySet().forEach( e -> System.out.println( "key=" + e.getKey() + ", val=" + e.getValue() + "\n" ) );
    
          System.out.println( "\n------------ Ausgabe aller Key/Values zu 'bpi':\n" );
          JsonObject bpi = jsonObj.getJsonObject( "bpi" );
          bpi.entrySet().forEach( e -> System.out.println( "key=" + e.getKey() + ", val=" + e.getValue() + "\n" ) );
    
          System.out.println( "\n------------ Ausgabe aller Key/Values zu 'bpi.EUR':\n" );
          JsonObject bpiEur = bpi.getJsonObject( "EUR" );
          bpiEur.entrySet().forEach( e -> System.out.println( "key=" + e.getKey() + ", val=" + e.getValue() + "\n" ) );
    
          System.out.println( "\n------------ Ermittlung einzelner Elemente:\n" );
          String     zeitpunkt  = time.getString( "updatedISO" );
          String     name       = jsonObj.getString( "chartName" );
          String     bpiEurCode = bpiEur.getString( "code" );
          JsonNumber bpiEurRate = bpiEur.getJsonNumber( "rate_float" );
          System.out.println( "Ein " + name + " kostet " + bpiEurRate + " " + bpiEurCode + " (" + zeitpunkt.replace( 'T', ' ' ) + ")." );
    
          System.out.println( "\n-------------------------------------------------------------\n\n" );
       }
    }
    
  2. Bauen Sie das Projekt und führen Sie es aus:

    cd \MeinWorkspace\JsonRestClientMitHttpURLConnection

    mvn clean package

    java -cp target\RestClient-jar-with-dependencies.jar restclient.BitcoinRestClient

    Da BitcoinRestClient in der pom.xml als Main-Klasse eingetragen ist, kann auch direkt die Jar-Datei ausgeführt werden:

    java -jar target\RestClient-jar-with-dependencies.jar

    Sie erhalten (gekürzt):

    ------------ Ausgabe aller Root-Key/Values:
    
    key=time,       val={"updated": ...
    key=disclaimer, val=" ...
    key=chartName,  val="Bitcoin"
    key=bpi,        val={ ...
    
    ------------ Ausgabe aller Key/Values zu 'time':
    
    key=updated,    val=...
    key=updatedISO, val=...
    
    ------------ Ausgabe aller Key/Values zu 'bpi':
    
    key=USD, val={"code":"USD",...
    key=GBP, val={"code":"GBP",...
    key=EUR, val={"code":"EUR",...
    
    ------------ Ausgabe aller Key/Values zu 'bpi.EUR':
    
    key=code,        val="EUR"
    key=symbol,      val="&euro;"
    key=rate,        val="1,961.1835"
    key=description, val="Euro"
    key=rate_float,  val=1961.1835
    
    ------------ Ermittlung einzelner Elemente:
    
    Ein Bitcoin kostet 1961.1835 EUR (2017-07-19 20:00:00+00:00).
    
    -------------------------------------------------------------
    

JSON-REST-Client für Länderkürzel

  1. Erstellen Sie im src\main\java\restclient-Verzeichnis die REST-Client-Klasse: LaenderRestClient.java

    package restclient;
    
    import javax.json.*;
    
    /**
     * JSON-REST-Client fuer Laenderkuerzel,
     * {@link "http://www.groupkt.com/post/f2129b88/free-restful-web-services-to-consume-and-test.htm"}
     * {@link "http://www.groupkt.com/post/c9b0ccb9/country-and-other-related-rest-webservices.htm"}
     * {@link "http://services.groupkt.com/country/get/all"}
     */
    public class LaenderRestClient
    {
       public static void main( String[] args )
       {
          final String url = "http://services.groupkt.com/country/get/all";
          JsonObject jsonObj = JsonObjectFromUrlUtil.getJsonObjectFromUrl( url );
    
          System.out.println( "\n\n------------ Ausgabe aller Root-Key/Values:\n" );
          jsonObj.entrySet().forEach( e -> System.out.println( "key=" + e.getKey() + ", val=" + e.getValue() + "\n" ) );
    
          System.out.println( "\n------------ Ausgabe aller Key/Values zu 'RestResponse':\n" );
          JsonObject restResponse = jsonObj.getJsonObject( "RestResponse" );
          restResponse.entrySet().forEach( e -> System.out.println( "key=" + e.getKey() + ", val=" + e.getValue() + "\n" ) );
    
          System.out.println( "\n------------ Ausgabe der Inhalte aller Elemente des 'RestResponse.result'-Arrays:\n" );
          JsonArray result = restResponse.getJsonArray( "result" );
          result.getValuesAs( JsonObject.class ).forEach(
                e -> System.out.println( e.getString( "alpha2_code" ) + " / " +
                                         e.getString( "alpha3_code" ) + " : " + e.getString( "name" ) ) );
    
          System.out.println( "\n-------------------------------------------------------------\n\n" );
       }
    }
    
  2. Bauen Sie das Projekt und führen Sie es aus:

    cd \MeinWorkspace\JsonRestClientMitHttpURLConnection

    mvn clean package

    java -cp target\RestClient-jar-with-dependencies.jar restclient.LaenderRestClient

    Sie erhalten (gekürzt):

    ------------ Ausgabe aller Root-Key/Values:
    
    key=RestResponse, val={ ...
    
    ------------ Ausgabe aller Key/Values zu 'RestResponse':
    
    key=messages, val=[" ...
    key=result, val=[{ ...
    
    ------------ Ausgabe der Inhalte aller Elemente des 'RestResponse.result'-Arrays:
    
    AF / AFG : Afghanistan
    AX / ALA : Åland Islands
    ...
    DE / DEU : Germany
    ...
    ZW / ZWE : Zimbabwe
    
    -------------------------------------------------------------
    

Javax-MVC mit Ozark, JAX-RS und Jetty

Es gibt mehrere Webframeworks, die auf REST-Services und insbesondere auf JAX-RS aufsetzen. Eines davon ist das neue Webframework "MVC 1.0 (JSR 371)", welches ursprünglich für Java EE 8 vorgesehen war, dann aber aus Zeitmangel verschoben wurde. Mittlerweile gibt es eine Early-Draft-Version, mit der erste Versuche gestartet werden können (Stand von 2017).

MVC steht für "Model View Controller". Damit ist die Entkopplung des Datenmodells von der Präsentation und der Steuerung gemeint. MVC-Webframeworks sind so genannte "aktionsbasierte Webframeworks", im Gegensatz zu den "komponentenbasierten Webframeworks" wie beispielsweise JSF.

Infos zu MVC 1.0 (JSR 371) finden Sie unter: JSR 371: Model-View-Controller Specification (Christian Kaltepoth, Ivar Grimstad), MVC 1.0 - Das neue Webframework (Christian Kaltepoth), EnterpriseTales: MVC 1.0 (Sven Kölpin, Lars Röwekamp), Das neue MVC-Webframework (Guido Oelmann), Why Another MVC? JSF versus MVC 1.0 (Ed Burns).

Die Referenzimplementierung von MVC 1.0 (JSR 371) ist Ozark.

Javax-MVC konkurriert mit Spring MVC.

Die folgende Javax-MVC-Demo zeigt einen ersten Einstieg unter Verwendung von MVC 1.0 (javax.mvc-api-1.0-edr2), Ozark (ozark-1.0.0-m02), Weld-CDI, Jersey-JAX-RS und Jetty. Das Besondere an dieser Demo ist, dass nicht nur eine in Java EE Application Servern deploybare WAR-Datei erzeugt wird, sondern eine im Jetty-Servlet-Container ausführbare Version erstellt wird, die mit dem jetty-maven-plugin direkt ausgeführt werden kann.

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

    cd \MeinWorkspace

    md OzarkJavaxMvcMitJetty

    cd OzarkJavaxMvcMitJetty

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

    md src\main\webapp\WEB-INF\views

    tree /F

  2. Erstellen Sie im OzarkJavaxMvcMitJetty-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/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>de.meinefirma.meinprojekt</groupId>
      <artifactId>OzarkJavaxMvcMitJetty</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>war</packaging>
      <name>${project.artifactId}</name>
      <properties>
        <jetty.version>9.4.7.v20170914</jetty.version>
        <jersey.version>2.26</jersey.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      </properties>
      <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.7.0</version>
            <configuration>
              <source>1.8</source>
              <target>1.8</target>
            </configuration>
          </plugin>
          <plugin>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-maven-plugin</artifactId>
            <version>${jetty.version}</version>
            <configuration>
              <jvmArgs>--add-modules java.xml.bind</jvmArgs>
              <scanIntervalSeconds>10</scanIntervalSeconds>
              <webApp>
                <contextPath>/${project.artifactId}</contextPath>
              </webApp>
              <httpConnector>
                <port>8080</port>
              </httpConnector>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>javax</groupId>
          <artifactId>javaee-api</artifactId>
          <version>7.0</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>javax.mvc</groupId>
          <artifactId>javax.mvc-api</artifactId>
          <version>1.0-edr2</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.ozark</groupId>
          <artifactId>ozark</artifactId>
          <version>1.0.0-m02</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.containers</groupId>
          <artifactId>jersey-container-servlet</artifactId>
          <version>${jersey.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.ext.cdi</groupId>
          <artifactId>jersey-cdi1x</artifactId>
          <version>${jersey.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.ext</groupId>
          <artifactId>jersey-bean-validation</artifactId>
          <version>${jersey.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.jersey.inject</groupId>
          <artifactId>jersey-hk2</artifactId>
          <version>${jersey.version}</version>
        </dependency>
        <dependency>
          <groupId>org.glassfish.hk2</groupId>
          <artifactId>hk2-api</artifactId>
          <version>2.5.0-b59</version>
        </dependency>
        <dependency>
          <groupId>javax.enterprise</groupId>
          <artifactId>cdi-api</artifactId>
          <version>2.0</version>
        </dependency>
        <dependency>
          <groupId>org.jboss.weld.servlet</groupId>
          <artifactId>weld-servlet-core</artifactId>
          <version>2.4.4.Final</version>
        </dependency>
        <dependency>
          <groupId>org.eclipse.jetty</groupId>
          <artifactId>jetty-servlet</artifactId>
          <version>${jetty.version}</version>
        </dependency>
      </dependencies>
    </project>
    

    Sie können im Maven-Repository nachsehen, ob es mittlerweile neuere Versionen gibt: javax.mvc-api, ozark-1.0.0-m02, ozark-jersey-1.0.0-m03, jersey, jetty-maven-plugin, hk2-api.

  3. Erstellen Sie im src\main\webapp\WEB-INF-Verzeichnis die Web-Konfigurationsdatei: web.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
             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_3_0.xsd"
             version="3.0">
      <listener>
        <listener-class>org.jboss.weld.environment.servlet.Listener</listener-class>
      </listener>
      <servlet>
        <servlet-name>REST-Servlet</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
          <param-name>jersey.config.server.provider.packages</param-name>
          <param-value>de.meinefirma.meinprojekt</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
      </servlet>
      <servlet-mapping>
        <servlet-name>REST-Servlet</servlet-name>
        <url-pattern>/mvc/*</url-pattern>
      </servlet-mapping>
    </web-app>
    
  4. Erstellen Sie im src\main\webapp\WEB-INF-Verzeichnis die Beans-Konfigurationsdatei: beans.xml

    <?xml version="1.0"?>
    <beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
           version="1.1" bean-discovery-mode="all">
    </beans>
    
  5. Erstellen Sie im src\main\webapp\WEB-INF\views-Verzeichnis die JSP-View: mvcdemo.jsp

    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <html>
    <head>
       <title>Javax-MVC-Demo mit Ozark</title>
       <meta http-equiv="content-type" content="text/html;charset=utf-8">
    </head>
    <body>
    
       <h2>${demotitel}</h2>
    
       <c:if test="${not empty messages}">
          <h2>Eingabefehler:</h2>
          <ul>
             <c:forEach var="msg" items="${messages}">
                <li>Fehler: ${msg}</li>
             </c:forEach>
          </ul>
       </c:if>
    
       <h2>Dateneingabe:</h2>
       <form action="./mvcdemo" method="POST">
          Name:   <input type="text"   name="name"  />
          E-Mail: <input type="email"  name="email" />
          Alter:  <input type="number" name="alter" />
          <input type="submit" />
       </form>
    
       <c:if test="${not empty form}">
          <h2>Ergebnis:</h2>
          <ul>
             <li>Name:   ${form.name}</li>
             <li>E-Mail: ${form.email}</li>
             <li>Alter:  ${form.alter}</li>
          </ul>
       </c:if>
    
    </body>
    </html>
    

    Der Einfachheit halber verwendet die Demo für die View JSP (hier zusammen mit der Core-JSP-Tag-Bibliothek). Javax-MVC kann aber auch mit vielen anderen Template-Engines kombiniert werden, beispielsweise mit Thymeleaf.

    Mit den "${...}"-EL-Ausdrücken können direkt die in Models gespeicherten Objekte referenziert werden und logische Ausdrücke ausgewertet werden. Es können alle mit @Named annotierten CDI-Beans in der View verwendet werden.

  6. Erstellen Sie im src\main\java\de\meinefirma\meinprojekt-Verzeichnis die Formulardaten-Klasse: MvcdemoForm.java

    package de.meinefirma.meinprojekt;
    
    import org.hibernate.validator.constraints.*;
    import javax.validation.constraints.Min;
    import javax.ws.rs.FormParam;
    
    public class MvcdemoForm
    {
       @FormParam( "name" )  @NotBlank  private String name;
       @FormParam( "email" ) @Email     private String email;
       @FormParam( "alter" ) @Min( 18 ) private int    alter;
    
       public String getName()  { return name;  }
       public String getEmail() { return email; }
       public int    getAlter() { return alter; }
    
       public void setName(  String name  ) { this.name  = name;  }
       public void setEmail( String email ) { this.email = email; }
       public void setAlter( int    alter ) { this.alter = alter; }
    }
    

    Mit der @FormParam-Annotation können übermittelte Formulardaten an Properties gebunden werden. @NotBlank, @Email und @Min bewirken eine Validierung der Formulardaten. Sehen Sie sich auch die vielen weiteren "Bean Validation constraints" und "Hibernate Validator constraints" an.

  7. Erstellen Sie im src\main\java\de\meinefirma\meinprojekt-Verzeichnis die REST- und MVC-Controller-Klasse: MvcdemoController.java

    package de.meinefirma.meinprojekt;
    
    import javax.inject.Inject;
    import javax.mvc.Models;
    import javax.mvc.annotation.Controller;
    import javax.mvc.binding.BindingResult;
    import javax.validation.Valid;
    import javax.ws.rs.*;
    
    @Controller
    @Path( "/mvcdemo" )
    public class MvcdemoController
    {
       @Inject private Models models;
       @Inject private BindingResult bindingResult;
    
       @GET
       public String render()
       {
          models.put( "demotitel", "Hallo Javax-MVC-Demo mit Ozark, Weld-CDI, Jersey-JAX-RS und Jetty" );
          return "mvcdemo.jsp";
       }
    
       @POST
       public String mvcdemo( @Valid @BeanParam MvcdemoForm form )
       {
          models.put( "demotitel", "Javax-MVC-Demo mit Ozark, Weld-CDI, Jersey-JAX-RS und Jetty" );
    
          // Falls Fehler:
          if( bindingResult.isFailed() ) {
             models.put( "messages", bindingResult.getAllMessages() );
             return "mvcdemo.jsp";
          }
    
          // Falls ok:
          models.put( "form", form );
    
          // Normalerweise Post-Redirect-Get-Pattern (PRG), also Redirect zu einer Ergebnisseite, z.B. so:
          // return "redirect:/ergebnis";
          return "mvcdemo.jsp";
       }
    }
    

    Durch die @Path-Annotation wird diese Klasse eine JAX-RS-Ressource mit REST-Services. Durch die zusätzliche @Controller-Annotation wird daraus ein MVC-Controller. Dies bedeutet beispielsweise, dass der Rückgabewert der REST-Methoden nicht an den Client geschickt wird, sondern stattdessen damit die darzustellende View ausgewählt wird.

    Durch die @BeanParam-Annotation werden Binding-Annotationen wie @FormParam gesucht und die Parameter gebunden.

    Die @Valid-Annotation bewirkt die Validierung der Formulardaten. Während bei JAX-RS-REST-Aufrufen ein Validierungsfehler zu einem "400 Bad Request" führen würde, erlaubt MVC über die BindingResult-Klasse die Fehlermeldungen zu sammeln und benutzerfreundlich anzuzeigen.

    Wenn die Daten über einen Redirect erhalten bleiben sollen, kann der spezielle MVC-Scope RedirectScoped eingesetzt werden.

    Um Angriffe per Cross Site Request Forgery (CSRF) zu verhindern, sollte die @CsrfValid-Annotation verwendet werden.

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

    [\MeinWorkspace\OzarkJavaxMvcMitJetty]
     |- [src]
     |   '- [main]
     |       |- [java]
     |       |   '- [de]
     |       |       '- [meinefirma]
     |       |           '- [meinprojekt]
     |       |               |- MvcdemoController.java
     |       |               '- MvcdemoForm.java
     |       '- [webapp]
     |           '- [WEB-INF]
     |               |- [views]
     |               |   '- mvcdemo.jsp
     |               |- beans.xml
     |               '- web.xml
     '- pom.xml
    
  9. Bauen Sie das Projekt:

    cd \MeinWorkspace\OzarkJavaxMvcMitJetty

    mvn clean package

    Falls Sie Java 8 verwenden, starten Sie Jetty mit:

    mvn jetty:run

    Falls Sie Java 9 verwenden, starten Sie Jetty mit:

    mvn jetty:run-forked

    Warten Sie bis der Webserver gestartet ist und folgende Zeile erscheint:

    [INFO] Started Jetty Server
    

    Rufen Sie die Ozark-Javax-MVC-Webseite auf:

    start http://localhost:8080/OzarkJavaxMvcMitJetty/mvc/mvcdemo

    OzarkJavaxMvcMitJetty

    OzarkJavaxMvcMitJetty



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 und lesen Sie folgendermaßen die Ausgabe des HelloWorld-Beispiels:

    cd \MeinWorkspace\JaxRsHelloWorld

    start java -cp bin;lib/* minirestwebservice.HalloWeltTestServer

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

    Sie erhalten entweder die Text- oder die HTML-Antwort.

  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"

    -->

    Plain-Text: 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/html
    ...
    
  7. Mit der Option "-v" erhalten Sie noch mehr zusätzliche Informationen.

  8. Lassen Sie sich die WADL-Datei ausgeben:

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

    curl -i "http://localhost:4434/application.wadl?detail=true"

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

  9. Wie Sie mit cURL PUT-, POST- und DELETE-Kommandos absetzen können, finden Sie weiter oben 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

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

    Sie erhalten entweder die Text- oder die HTML-Antwort:

    ...: 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/...
    ...
    

TShark als Kommandozeilen-Sniffer

Das bekannte mächtige grafische Wireshark beinhaltet das Kommandozeilen-Netzwerk-Sniffer-Tool TShark. Die TShark-Kommandozeilenparameter sind unter den Manual Pages erläutert. Anders als cURL und Wget zeigt TShark nicht nur die HTTP-Antwort, sondern auch die HTTP-Anfrage-Details.

  1. Beachten Sie die unten zu Wireshark aufgeführten Hinweise.

  2. Filtern Sie folgendermaßen nach HTTP auf dem Port 4434, und beobachten Sie die Netzwerkaufrufe zu obigem HelloWorld-Programmierbeispiel:

    tshark -i any -Y "http and tcp.port==4434"

    Sie erhalten:

     15 3.370976526    127.0.0.1 -> 127.0.0.1    HTTP 171 GET /helloworld?name=ich HTTP/1.1 
     17 3.374917776    127.0.0.1 -> 127.0.0.1    HTTP 191 HTTP/1.1 200 OK  (text/plain)
    
  3. Mit der Option "-V" geben Sie auch die Paket-Details aus:

    tshark -i any -Y "http and tcp.port==4434" -V

    Sie erhalten u.a. (gekürzt):

    ...
    Transmission Control Protocol, Src Port: 43050 (43050), Dst Port: 4434 (4434), Seq: 1, Ack: 1, Len: 103
        ...
    Hypertext Transfer Protocol
        GET /helloworld?name=ich HTTP/1.1\r\n
        ...
        [Full request URI: http://localhost:4434/helloworld?name=ich]
    
    Transmission Control Protocol, Src Port: 4434 (4434), Dst Port: 43050 (43050), Seq: 1, Ack: 104, Len: 123
        ...
    Hypertext Transfer Protocol
        HTTP/1.1 200 OK\r\n
            ...
        Content-Type: text/plain\r\n
        Content-Length: 21\r\n
        ...
    Line-based text data: text/plain
        Plain-Text: Hallo ich
    

Grafische Tools

Falls Sie statt der Kommandozeilentools grafische Tools bevorzugen, sehen Sie sich die Webbrowser-Client-Tools und die TCP/IP-Monitore 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 oben 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 http://localhost:8080/JaxRsBuecherverwaltung, klicken auf den Firebug-Käfer, aktivieren unter 'Netzwerk' die 'Netzwerk'-Checkbox, und klicken 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 in Firefox "Extras" | "REST Client", tragen unter REST Request "http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher" und bei Request Body "isbn=9876543219&titel=RESTClient-Titel&preis=777" ein, wählen als Method "POST" und klicken 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 auf das P, tragen 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 bei Actions "POST" und klicken 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 (und TShark) 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. Auch mit Wireshark kann die REST-Kommunikation beobachtet werden, sogar ohne dass eine Portnummer umgestellt werden muss. Sehen Sie sich den Wireshark User's Guide an.

Windows: Falls Sie TCP/IP-Aufrufe vom PC zu sich selbst untersuchen wollen (localhost, 127.0.0.1): Dies ist unter Windows nicht so einfach möglich, siehe hierzu: Loopback capture.

Linux: Sehen Sie sich Installation von Wireshark und ubuntuusers: Wireshark an.

Weiter oben wurde bereits gezeigt, wie mit dem in Wireshark enthaltenen Kommandozeilen-Netzwerk-Sniffer-Tool TShark die HTTP-Kommunikation beobachtet werden kann.

Der folgende Screenshot zeigt einen Mitschnitt mit dem grafischen Tool Wireshark zum oben gezeigten Programmierbeispiel JaxRsHelloWorld. In der Filterleiste von Wireshark wurde als Filter "http and tcp.port==4434" gesetzt. Darunter in der Paketliste sehen Sie die GET-Anfrage sowie das Antwortpaket mit 200 OK. Wenn Sie auf eines der beiden Pakete klicken, erscheinen darunter in den beiden Paketdetailfenstern Informationen zu den Paketen, hier im Beispiel der Ergebnistext "Plain-Text: Hallo ich" sowie weitere Informationen.

Wireshark

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 Apache TCPMon Tutorial.

Downloaden Sie die Datei tcpmon-1.0.jar, beispielsweise von Asjava TCPMon Tutorial oder Apache-Archive. 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 oben 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 oben 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 oben 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




Weitere Themen: andere TechDocs | REST mit Spring | REST mit JAX-RS 1.1 | SOAP Web Services mit JAX-WS | SOA
© 2016-2017 Torsten Horn, Aachen