Eine Anwendung mit javax.ws.rs läuft auf keinem aktuellen Servlet-Container mehr. Das ist die kurze Fassung. Die längere ist unangenehmer, denn der Namensraum ist nur die sichtbarste von etwa einem Dutzend Änderungen, und mehrere davon fallen beim Kompilieren nicht auf.
Dieser Artikel ist für den Fall gedacht, dass eine bestehende JAX-RS-Anwendung auf einen gepflegten Stand soll. Wer neu anfängt, findet am Ende einen Absatz dazu, warum das vermutlich gar nicht die richtige Frage ist.
Die Landkarte
Vier Generationen, und zwischen der zweiten und der dritten liegt der Bruch.
| Plattform | Spezifikation | Jersey | Namensraum | Min. JDK |
|---|---|---|---|---|
| Java EE 7 / 8 | JAX-RS 2.0 / 2.1 | 2.x | javax.ws.rs | 8 |
| Jakarta EE 9 / 9.1 | Jakarta REST 3.0 | 3.0.x | jakarta.ws.rs | 8 |
| Jakarta EE 10 | Jakarta REST 3.1 | 3.1.x | jakarta.ws.rs | 11 |
| Jakarta EE 11 | Jakarta REST 4.0 | 4.0.x | jakarta.ws.rs | 17 |
Wer noch älter unterwegs ist, hat einen Sprung mehr vor sich: JAX-RS 1.x wurde von Jersey 1.x umgesetzt, und dessen Klassen liegen unter com.sun.jersey. Ab Jersey 2.0 heißen sie org.glassfish.jersey. Diese Umbenennung hat mit Jakarta noch gar nichts zu tun, sie trifft einen nur zufällig im selben Aufwasch.
Jersey 2.x wird weiterhin gepflegt und bleibt bei javax.ws.rs. Wer aus Zeitgründen nicht migrieren kann, ist dort also nicht sofort verloren. Neue Spezifikationsstände kommen dort aber nicht mehr an.
Der Container ist das erste Hindernis
Die Umstellung des eigenen Codes nützt nichts, wenn der Server nicht mitzieht. Der Servlet-Namensraum hat denselben Sprung gemacht: Tomcat 9 spricht javax.servlet, Tomcat 10 jakarta.servlet. Ein WAR aus der alten Welt lässt sich in Tomcat 10 nicht einfach ablegen.
Für Jersey 3.x kommen unter anderem GlassFish 6, Grizzly 3, Jetty 11, Payara 6 und Tomcat 10 in Frage. Wer die Migration plant, sollte mit dieser Zeile anfangen, weil sie den Zeitpunkt bestimmt: Der Container muss vorher stehen.
Der Namensraum
Der mechanische Teil ist schnell erzählt. Alle Importe von javax.ws.rs nach jakarta.ws.rs, dazu javax.servlet nach jakarta.servlet, javax.xml.bind nach jakarta.xml.bind und so weiter. Suchen und Ersetzen erledigt fast alles.
Für große Codebasen oder für Abhängigkeiten, deren Quelltext man nicht hat, gibt es den Eclipse Transformer. Er schreibt Bytecode und Ressourcen um, auch in fremden JARs, und ist der übliche Weg, wenn eine Bibliothek noch keine Jakarta-Fassung hat. Apache Tomcat bringt für denselben Zweck ein eigenes Migrationswerkzeug mit.
Ein Detail, das dabei gern durchrutscht: Auch Konfigurationsdateien tragen Namensräume. Die web.xml verweist auf ein anderes Schema, und Bindings für JAXB ebenfalls. Der Transformer fängt das ab, ein Suchen und Ersetzen im Editor nicht unbedingt.
Was sonst noch bricht
web.xml wird überflüssig. Die alte Registrierung des Jersey-Servlets kann ersatzlos weg:
<!-- So sah es aus. Wird nicht mehr gebraucht. -->
<servlet>
<servlet-name>REST-Servlet</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>REST-Servlet</servlet-name>
<url-pattern>/services/*</url-pattern>
</servlet-mapping>Code language: HTML, XML (xml)
An ihre Stelle tritt eine Klasse:
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
@ApplicationPath("/services")
public class RestApplication extends Application {
}Code language: Java (java)
Achtung bei Jersey 3.1: Dort wird @ApplicationPath von jedem Container ausgewertet, nicht mehr nur vom Servlet-Container. Wer die Annotation bisher irgendwo stehen hatte, wo sie ignoriert wurde, bekommt plötzlich ein anderes Routing. Das trifft vor allem Tests mit eingebettetem Server.
@Context ist abgekündigt. Seit Jakarta REST 3.1 gilt die Annotation als deprecated, weil die Spezifikation sich Richtung CDI bewegt. Sie funktioniert weiter, aber neuer Code sollte @Inject verwenden.
Jackson 1 ist weg. Jersey 3.0 hat die Unterstützung entfernt. Wer noch org.codehaus.jackson im Projekt hat, muss auf Jackson 2 oder auf JSON-B umstellen.
getRequestHeader(String) liefert null. Ab Jersey 3.1 gibt die Methode der Klasse ClientRequest bei einem fehlenden Header null zurück statt einer leeren Liste. Das ist die Sorte Änderung, die beim Kompilieren durchgeht und im Betrieb eine NullPointerException wirft.
ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE steht ab Jersey 3.0 per Vorgabe auf true. Wer mit CompletionStage arbeitet, sollte prüfen, ob das Verhalten noch stimmt.
Beim Sprung auf 4.0 fällt JAX-B. Jakarta REST 4.0 hat die an JAX-B gebundenen Klassen entfernt, konkret jakarta.ws.rs.core.Link.JaxbLink und Link.JaxbAdapter samt der zugehörigen Methoden, weil JAX-B nicht mehr Teil des Core Profile von Jakarta EE 11 ist. Außerdem wurde jersey-apache-connector durch jersey-apache5-connector ersetzt, weil der Apache HTTP Client 4 abgelöst ist.
Die Abhängigkeiten
<properties>
<jersey.version>3.1.11</jersey.version>
</properties>
<dependencies>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>${jersey.version}</version>
</dependency>
<!-- Ohne einen Injection-Provider startet Jersey nicht und meldet,
es finde keine InjectionManagerFactory. -->
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-binding</artifactId>
<version>${jersey.version}</version>
</dependency>
</dependencies>Code language: HTML, XML (xml)
Das mit jersey-hk2 ist eine der häufigsten Startschwierigkeiten nach der Migration. In Jersey 1.x war die Injektion eingebaut, seit 2.x ist sie ein eigenes Modul.
Die Ressource, vorher und nachher
Eine typische Ressourcenklasse von damals:
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
@Path("calculator")
@Produces("application/xml")
public class CalculatorService {
@GET
@Path("calc")
public ServiceResult add(@QueryParam("a") Integer a, @QueryParam("b") Integer b) {
ServiceResult r = new ServiceResult();
if (a == null || b == null) {
r.setMessage("No Values!");
r.setReturncode(8);
return r;
}
// ...
}
}Code language: Java (java)
Und dieselbe Sache heute:
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
@Path("calculator")
@Produces(MediaType.APPLICATION_JSON)
public class CalculatorResource {
public record Calculation(String formula, int result) {}
public record CalculationResult(String message, List<Calculation> calculations) {}
@GET
@Path("calc")
public CalculationResult add(@QueryParam("a") Integer a, @QueryParam("b") Integer b) {
if (a == null || b == null) {
// Der Fehler gehört in den HTTP-Status, nicht in den Rumpf.
throw new BadRequestException("Die Parameter a und b sind erforderlich");
}
return new CalculationResult(
"Ergebnisse für a = %d, b = %d".formatted(a, b),
List.of(new Calculation("a + b", a + b),
new Calculation("a - b", a - b),
new Calculation("a * b", a * b)));
}
}Code language: JavaScript (javascript)
Drei Änderungen, die über den Namensraum hinausgehen und sich beim Anfassen ohnehin lohnen.
Die Antwort ist JSON statt XML. Für einen Dienst, der 2026 gebaut oder angefasst wird, ist das die Vorgabe; jersey-media-json-binding erledigt die Umwandlung über JSON-B, und für Records braucht es keine Annotationen mehr.
Der Fehlerfall wird ein HTTP-Status. Ein returncode im Rumpf bei HTTP 200 zwingt jeden Aufrufer, den Erfolg zweimal zu prüfen, und Zwischenschichten wie Proxies oder Clients können damit nichts anfangen. BadRequestException liefert 400, und die passenden Ausnahmen gibt es für alle üblichen Fälle.
Die Datenklassen werden Records. Aus zwei Klassen mit je vier Gettern und Settern werden zwei Zeilen.
WADL kann weg
Die alte Anleitung wies auf /services/application.wadl hin, eine maschinenlesbare Beschreibung aller Ressourcen. Das war 2009 ein vernünftiger Gedanke, aber durchgesetzt hat sich WADL nie. Wer heute eine Beschreibung braucht, nimmt OpenAPI, und für Jersey gibt es das über swagger-jaxrs2-jakarta.
Wann man nicht migrieren sollte
Eine ehrliche Zwischenfrage vor der Arbeit: Lohnt der Umstieg oder lohnt der Neubau?
Für eine gewachsene Anwendung mit vielen Ressourcen und laufendem Betrieb ist die Migration meist günstiger, gerade weil die Schritte oben mechanisch sind und sich gut in kleinen Häppchen erledigen lassen.
Handelt es sich dagegen um einen überschaubaren Dienst, sollte man nachrechnen. Jakarta REST ist eine Spezifikation für Anwendungsserver, und dieses Modell hat an Boden verloren. Ein Dienst dieser Größe entsteht heute eher mit Spring Boot oder Quarkus, und beide bringen Konfiguration, Metriken, Health Checks und Testunterstützung mit, für die man sich im klassischen Aufbau alles einzeln zusammensucht. Quarkus setzt dabei intern weiterhin auf Jakarta REST, das Wissen aus der Migration ist also nicht verloren.
Wer den Dienst ohnehin in eine Lambda-Funktion oder einen Container stecken will, sollte zudem prüfen, ob ein Servlet-Container überhaupt noch im Bild sein muss.
Sehr gutes Tutorial.
Und hier gibt es den passenden PHP Client:
http://54.246.58.55/2009/05/php-client-rest-webservice/
Pingback:Java, Webprogrammierung, Linux und mehr
Den Client um den WebService mit Java abzufragen gibt es hier:
http://54.246.58.55/2009/05/java-client-fuer-restful-web-services/
Zitat: “Nach dem deployen in einem ServletContainer wie Tomcat….”, wie schaut dieser Deployvorgang aus? bzw. wie deployt man diesen WebService im Glassfish?
lg
Deine Entwicklungsumgebung sollte dir eine WAR-Datei oder eine EAR Datei erstellen können. Das kommt auf dein Projekt an. Wie man dann deployed hängt vom ServletContainer/Applicationserver ab.
Beim Tomcat einfach die Datei in den TOMCAT_HOME/webapps Ordner ablegen.
http://tomcat.apache.org/tomcat-5.5-doc/deployer-howto.html
Beim Classfish gibt es da verschiedene Wege:
https://glassfish.dev.java.net/javaee5/deployment/index.html
Könntest du mir auch noch einmal sagen, wie ich das dann deploye?
danke =)
ach so ein mist – hier gibt es ja das +-Zeichen *gg*
danke dann lese ich mir das mit dem deployen durch – danke
Exportiere dein Anwendung die du z.B. in Eclipse erstellt hast einfach als WAR-Datei (in Eclipse –> Export… –> Web –> WAR file) und kopiere die WAR Datei in den Deploy Pfad deines Anwendungsservers, bei Tomcat ist das dann das Verzeichnis webapps.
Hi danke für die schnelle Antwort und ja das mitn deploy-Vorgang ist auch kein Problem, eine Frage hätt ich noch, wie ist es möglich, dass ich dem WebService Objekte von eigenen definierten Klassen übergebe? also keine Longs Strings etc…
Meines Wissens kannst du bei REST WebServices nur mit einfachen Datentypen wie String, Integer und Long arbeiten. Wenn du komplexe Parameter brauchst solltest du dir die SAOP basierten WebServices ansehen.
Cooles Tutorial! Danke!
wollte das ganze gerne über Glassfish starten – er findet aber irgendwie den service nicht 🙁
gibts da irgendwas besonderes zu beachten?
@Niels
Du musst aufpassen, daß die URI richtig/komplett ist
bspw.
http://localhost:{port}/{contextPath}/resources/{ResourcePath}
context Deiner App sei beispielsweise TestWeb und im @Path der ResourceKlasse steht @Path(“myTest”)
http://localhost:8080/TestWeb/resources/myTest
Bei Vista und Win7 sollte Glassfish zudem nicht unter C:Program Files installiert sein (was er standardmäßig tut), da hier die Zugriffsrechte vom Betriebssystem eingeschränkt sind.
ich habs mit eclipse probiert, das war file zu erstellen und im tomcat deployed. ich hab das gleiche problem, er findet den service nicht,
http://localhost:8080/RESTServlet/calculator/calc?a=234&b=456
oder http://localhost:8080/calculator/calc?a=234&b=456
funktioniert es nicht, das war file heißt RESTServlet.war
kann mir wer helfen?
mfg freak
Und funktioniert es mit dieser URL?
http://localhost:8080/RESTServlet/services/calculator/calc?a=234&b=456
Kannst du die Übersicht der Services aufrufen?
http://localhost:8080/RESTServlet/services/application.wadl
hey,
danek für das tutorial. Leider funktioniert es allerdings bei mir auch nicht wirklich. Ich kann die Übersicht auch nicht aufrufen.
ich erhalte den fehler
The requested resource (/wstest/services/calculator/calc) is not available
währe cool wenn du vielleicht eine idee hättest was da noch faul sein kann.
lg
ah cool danke, geht
da war mein fehler 😉
hallo,
statt: @QueryParam(“a”) Integer a
vllt: @QueryParam(“obj”) Objekt o
gibs eine möglichkeit objekte zu übergeben, oder muesste man da @GET vllt nehmen, hat da wer vielleicht ein bsp, wäre super 🙂
mfg freak
Siehe weiter oben, REST kann nur mit einfache Datentypen arbeiten.
ja, das hab ich gelesen, das kann ich mir aber nicht vorstellen? Irgendeine art Serialisation muesste es doch geben, oder hier: Bei
“Extracting Request Parameters” http://wikis.sun.com/display/Jersey/Overview+of+JAX-RS+1.0+Features
ist das nicht so ein bsp?
ich bekomms nur nicht zum laufen ;(
Danke für das Tutorial, hat auf anhieb geklappt.