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.

PlattformSpezifikationJerseyNamensraumMin. JDK
Java EE 7 / 8JAX-RS 2.0 / 2.12.xjavax.ws.rs8
Jakarta EE 9 / 9.1Jakarta REST 3.03.0.xjakarta.ws.rs8
Jakarta EE 10Jakarta REST 3.13.1.xjakarta.ws.rs11
Jakarta EE 11Jakarta REST 4.04.0.xjakarta.ws.rs17

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.

Von JAX-RS zu Jakarta REST: was beim Umstieg wirklich bricht
Tagged on:                     

22 thoughts on “Von JAX-RS zu Jakarta REST: was beim Umstieg wirklich bricht”

  • Pingback:Java, Webprogrammierung, Linux und mehr

  • 2009-06-02 at 17:18
    Permalink

    Zitat: “Nach dem deployen in einem ServletContainer wie Tomcat….”, wie schaut dieser Deployvorgang aus? bzw. wie deployt man diesen WebService im Glassfish?

    lg

    Reply
    • 2009-09-01 at 08:41
      Permalink

      Könntest du mir auch noch einmal sagen, wie ich das dann deploye?
      danke =)

      Reply
      • 2009-09-01 at 08:41
        Permalink

        ach so ein mist – hier gibt es ja das +-Zeichen *gg*
        danke dann lese ich mir das mit dem deployen durch – danke

        Reply
      • 2009-09-01 at 09:48
        Permalink

        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.

        Reply
  • 2009-06-07 at 11:00
    Permalink

    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…

    Reply
    • 2009-06-09 at 09:14
      Permalink

      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.

      Reply
  • 2010-05-20 at 12:50
    Permalink

    wollte das ganze gerne über Glassfish starten – er findet aber irgendwie den service nicht 🙁
    gibts da irgendwas besonderes zu beachten?

    Reply
    • 2010-05-29 at 19:41
      Permalink

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

      Reply
  • 2010-06-09 at 07:39
    Permalink

    ah cool danke, geht

    da war mein fehler 😉

    Reply
  • 2010-06-10 at 13:59
    Permalink

    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

    Reply
  • 2011-05-15 at 22:36
    Permalink

    Danke für das Tutorial, hat auf anhieb geklappt.

    Reply

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.