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.
Insbesondere im SOA-Umfeld wird oft zwischen folgenden Architekturstilen unterschieden, obwohl die Grenzen fließend sind:
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 Services | SOAP Web Services | |
---|---|---|
Basis-Standard | REST | W3C u.a. |
Java-Standard | JAX-RS (JSR 370) | JAX-WS (JSR 224) |
Beispiel für Java-Implementierung | Jersey | Metro |
Architekturstil | "ROA" (Resource Oriented Architecture), ressourcenorientiert mit generischer uniformer Schnittstelle (GET, PUT, POST, DELETE) | eher "SOA" (Service Oriented Architecture), schnittstellen-/nachrichtenorientiert |
Bevorzugtes Anwendungsgebiet | datenorientierte synchrone kurz laufende Services | sowohl datenorientierte als auch lang laufende prozessorientierte Services, synchron und asynchron |
Serverseitiger Zustand/Status | zustandslos | eher zustandslos, kann aber auch zustandsbehaftet sein |
Formale syntaktische Schnittstellenbeschreibung | nur zum Teil standardisiert: Nachrichtenformate können durch XML Schema Definition und WADL beschrieben werden | vollständig durch WSDL |
Nachrichtenformat, Repräsentation | Text, HTML, XML, JSON, binär, ... | XML (plus Attachments) |
Nachrichtenprotokoll, Anwendungsprotokoll | REST, HTTP | SOAP |
Transportprotokoll | HTTP | HTTP, SMTP, JMS, ... |
Asynchrone Kommunikation | nicht direkt (nur über Umwege simuliert, z.B. über Atom-Feeds im Atom Syndication Format) | ja, per JMS und WS-Notification (WSN) |
Transaktion, sichere Zustellung | keine 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) |
Routing | möglich | per WS-Addressing |
Operationsabhängiger Zugriffsschutz | einfach, per Webserver oder Firewall | per WS-Security (WSS) |
Bookmarks/Links | ja | nein |
Caching | einfach | schwierig |
Skalierbarkeit | optimal | kann schwieriger sein |
Performance | gut | eher schlechter |
Lose Kopplung, Interoperabilität, Plattformunabhängigkeit, Internetfähigkeit | ja | ja |
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 -Bedeutung | Idempotenz, Sicherheit |
---|---|---|---|
GET | Read | 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 |
PUT | Update, Create |
http://xyz.de/Artikel/Buecher/4711 --> Update (oder Create) des per ID identifizierten Artikels |
idempotent |
POST | Create | 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 |
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:
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:
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.
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]
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.
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.
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.
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.
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.
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
Ö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>
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.
Sie können auch andere URLs beim HalloWeltTestServer- und HalloWeltTestClient-Aufruf übergeben, auch inklusive eines Web-Root-ContextPath-Anteils, beispielsweise http://localhost:4711/xyz.
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"
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.
Beenden Sie den HalloWeltTestServer mit: Strg+C.
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.
Installieren Sie Maven wie beschrieben in: maven.htm#Installation.
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
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.
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
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.
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(); } } }
Bitte beachten Sie, dass Sie diesmal nicht manuell Libs zum Projekt hinzukopieren müssen, weil sich darum Maven kümmert.
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
Führen Sie den JUnit-Modultest aus:
mvn test
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.
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
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"
Beenden Sie den Grizzly-Webserver mit: Strg+C.
Die WAR-Datei im target-Verzeichnis können Sie in Java EE Webserver, Application Server und Servlet-Container deployen.
Z.B. mit Tomcat:
Kopieren Sie die WAR-Datei ins Tomcat-webapps-Verzeichnis, warten Sie ein paar Sekunden, und rufen Sie den REST-Service auf:
start D:\Tools\Tomcat\bin\startup.bat
copy target\JaxRsMitMaven.war D:\Tools\Tomcat\webapps\
start http://localhost:8080/JaxRsMitMaven/rest/helloworld?name=ich
curl -i -H "Accept:text/plain" "http://localhost:8080/JaxRsMitMaven/rest/helloworld?name=ich"
curl -i -H "Accept:text/html" "http://localhost:8080/JaxRsMitMaven/rest/helloworld?name=ich"
curl -i "http://localhost:8080/JaxRsMitMaven/rest/application.wadl"
Z.B. mit WebLogic:
Kopieren Sie die WAR-Datei ins WebLogic-autodeploy-Verzeichnis, warten Sie ein paar Sekunden, und rufen Sie den REST-Service auf:
start C:\WebLogic\user_projects\domains\MeineDomain\startWebLogic.cmd
copy target\JaxRsMitMaven.war C:\WebLogic\user_projects\domains\MeineDomain\autodeploy\
start http://localhost:7001/JaxRsMitMaven/rest/helloworld?name=ich
curl -i -H "Accept:text/plain" "http://localhost:7001/JaxRsMitMaven/rest/helloworld?name=ich"
curl -i -H "Accept:text/html" "http://localhost:7001/JaxRsMitMaven/rest/helloworld?name=ich"
curl -i "http://localhost:7001/JaxRsMitMaven/rest/application.wadl"
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?
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.
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.
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.
Erstellen Sie im JaxRsMitMaven-Projekt im Verzeichnis src\main\java das neue Unterverzeichnis xmljaxb und darin folgende drei Java-Klassen.
package xmljaxb; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement public class InputTO { public int i; public String s; }
Ergebnis-Klasse: ResultTO.java
package xmljaxb; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement public class ResultTO { public int i; public String s; }
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; } }
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.
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
Führen Sie die JUnit-Modultests aus:
mvn test
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>äöüߧ½²€√∑</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> ...
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:
Z.B. mit Tomcat:
curl -i -H Content-type:text/xml --request POST -d "<inputTO><i>42</i><s>abc xyz</s></inputTO>" "http://localhost:8080/JaxRsMitMaven/rest/xmljaxb"
Z.B. mit WebLogic:
curl -i -H Content-type:text/xml --request POST -d "<inputTO><i>42</i><s>abc xyz</s></inputTO>" "http://localhost:7001/JaxRsMitMaven/rest/xmljaxb"
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.
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.
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
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.
Ersetzen Sie im neuen JaxRsContractFirstService-Projektverzeichnis in der kopierten pom.xml die Zeile
"<artifactId>JaxRsMitMaven</artifactId>" durch
"<artifactId>JaxRsContractFirstService</artifactId>".
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).
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
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.
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
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; } }
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; } } }
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 ); } } }
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
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.
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.
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
Führen Sie die JUnit-Modultests aus:
mvn test
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.
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:
Z.B. mit Tomcat:
curl -i -H Content-type:text/xml --request POST -d "<inputTO xmlns='mein.ns'><i>42</i><s>abc xyz</s></inputTO>" "http://localhost:8080/JaxRsContractFirstService/rest/contractfirst"
Z.B. mit WebLogic:
curl -i -H Content-type:text/xml --request POST -d "<inputTO xmlns='mein.ns'><i>42</i><s>abc xyz</s></inputTO>" "http://localhost:7001/JaxRsContractFirstService/rest/contractfirst"
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
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).
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.
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.
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).
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
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>".
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>".
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.
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 ); } }
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() ); } } }
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
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.
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.
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.
Im folgenden Beispiel wird das JaxRsContractFirstService-Beispiel um Authentifizierung und um einen Integrationstest mit Tomcat erweitert.
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.
Führen Sie in Ihrem Projekte-Workspace-Verzeichnis folgende Kommandos aus:
cd \MeinWorkspace
xcopy JaxRsContractFirstService JaxRsAuthentication\ /S
cd JaxRsAuthentication
md src\test\java\integrationstest
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!
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>
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.
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>
Starten Sie den Integrationstest:
mvn integration-test
Beachten Sie, dass Tomcat automatisch hoch- und wieder heruntergefahren wird.
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.
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.
Falls Sie eine andere Exception erhalten, suchen Sie in der \Tools\Tomcat\logs\localhost.*.log-Datei die ursprüngliche Exception.
Falls Sie WebLogic statt Tomcat verwenden wollen, sehen Sie sich die Hinweise zur Authentifizierung unter WebLogic an.
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.
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
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>
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(); } } }
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>"; } }
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
Bauen Sie das Projekt:
cd \MeinWorkspace\JaxRsMitJetty
mvn clean package
dir target\*.?ar
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
Warten Sie bis der Webserver gestartet ist und folgende Zeile erscheint:
...INFO:oejs.Server:main: Started ...
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"
Lassen Sie sich die WADL anzeigen:
curl http://localhost:8080/JaxRsMitJetty/rest/application.wadl
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.
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
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>
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>
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>"; } }
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
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.
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
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"
Lassen Sie sich die WADL anzeigen:
curl http://localhost:8080/JaxRsMitJettyMavenPlugin/rest/application.wadl
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.
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
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>
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>
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; } }
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; } }
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; } }
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; } }
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; } }
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: <input type="text" name="isbn" value="1234567891" maxlength=20> Titel: <input type="text" name="titel" value="MeinTitel1" maxlength=80> Preis: <input type="text" name="preis" value="12.34" maxlength=20> <input type="submit" value="Erstes Buch anlegen"> </form> <form method="POST" action="/JaxRsBuecherverwaltung/rest/Artikel/Buecher" enctype="application/x-www-form-urlencoded"> ISBN: <input type="text" name="isbn" value="1234567892" maxlength=20> Titel: <input type="text" name="titel" value="MeinTitel2" maxlength=80> Preis: <input type="text" name="preis" value="22.34" maxlength=20> <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: <input type="text" name="isbn" value="1472583693" maxlength=20> Titel: <input type="text" name="titel" value="Neuer Titel X" maxlength=80> Preis: <input type="text" name="preis" value="123.45" maxlength=20> <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: <input type="text" name="isbn" maxlength=20> Titel: <input type="text" name="titel" maxlength=80> ID: <input type="text" name="id" maxlength=40> <input type="submit" value="Bücher finden"> </form> </body> </html>
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).
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.
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.
Beenden Sie Tomcat:
set TOMCAT_HOME=D:\Tools\Tomcat
pushd .
cd /D %TOMCAT_HOME%\bin
shutdown.bat
popd
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(); } }
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.
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".
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
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>
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>
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>
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>
DELETE: Löschen Sie ein vorhandenes Buch:
curl --request DELETE "http://localhost:8080/JaxRsBuecherverwaltung/rest/Artikel/Buecher/4711"
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?).
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.
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.
Legen Sie eine Kopie in einem neuen Verzeichnis an:
cd \MeinWorkspace
xcopy JaxRsBuecherverwaltung JaxRsJaxWsPerformance\ /S
cd JaxRsJaxWsPerformance
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>
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(); }
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(); } }
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>
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>
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 ); }
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.
Die Batchdateien run-*.bat sind im Source-Download enthalten.
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
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:
Anbindung | Anlage von 1000 Büchern | Einzelnes Auslesen von 1000 Büchern | Gemeinsames Auslesen von 1000 Büchern |
---|---|---|---|
direkt (ohne REST/SOAP) | 30 ms | 5 ms | 0,1 ms |
REST/SOAP auf lokalem PC ("localhost") | 0,9 s | 0,5 s | 10 ms |
REST/SOAP remote über Netzwerk | 2 s | 1,2 s | 30 ms |
Falls Sie eine Exception erhalten, suchen Sie in der \Tools\Tomcat\logs\localhost.*.log-Datei die ursprüngliche Exception.
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.
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
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>
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
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 ); } }
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() ); } }
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
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
Warten Sie bis der Webserver gestartet ist und folgende Zeile erscheint:
INFO [...] org.eclipse.jetty.server.Server: Started ...
Ö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.
Alternativ zur JSON-Webschnittstelle können Sie die Metriken auch über JMX-MBeans abrufen, beispielsweise mit JConsole:
"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
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.
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
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>
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 ); } } }
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" ); } }
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). -----------------------------------------------------
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" ); } }
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="€" 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). -------------------------------------------------------------
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" ); } }
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 -------------------------------------------------------------
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.
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
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.
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>
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>
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.
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.
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.
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
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
cURL ist ein Kommandozeilentool zum Downloaden von Dateien und vielen anderen URL- und HTTP-basierenden Aufgaben.
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
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.
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>
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 ...
Mit der Option "-v" erhalten Sie noch mehr zusätzliche Informationen.
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).
Wie Sie mit cURL PUT-, POST- und DELETE-Kommandos absetzen können, finden Sie weiter oben erläutert.
Ein anderes Kommandozeilentool zum Downloaden von Dateien ist Wget.
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
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/... ...
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.
Beachten Sie die unten zu Wireshark aufgeführten Hinweise.
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)
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
Falls Sie statt der Kommandozeilentools grafische Tools bevorzugen, sehen Sie sich die Webbrowser-Client-Tools und die TCP/IP-Monitore an.
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.
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:
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:
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:
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 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.
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:
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:
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:
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:
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:
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, ...).