Wer wsimport auf einem aktuellen JDK aufruft, bekommt „command not found”. Das Werkzeug gibt es noch, es ist nur seit Java 11 nicht mehr Teil des JDK. Für ein Projekt, das einen SOAP-Client aus einer WSDL erzeugen soll, ändert sich dadurch weniger, als man befürchtet: Statt eines JDK-Werkzeugs nimmt man ein Maven-Plugin, und das war ohnehin der bessere Weg.

Was Java 11 entfernt hat

JEP 320 hat die Java-EE- und CORBA-Module aus dem JDK geworfen. Betroffen sind unter anderem java.xml.ws mit der JAX-WS-API, java.xml.bind mit JAXB, java.activation und jdk.xml.ws, das die Werkzeuge wsimport und wsgen enthielt.

Code, der javax.xml.ws.Service importiert, kompiliert seitdem nicht mehr, und zur Laufzeit gibt es NoClassDefFoundError. Die Implementierungen sind aber nicht verschwunden, sondern liegen als eigenständige Artefakte bei Maven Central. Das Projekt heißt heute Eclipse Metro und wird bei der Eclipse Foundation gepflegt.

Die Entscheidung vor der Einrichtung: javax oder jakarta

Mit Jakarta EE 9 wanderte der Namensraum von javax.xml.ws nach jakarta.xml.ws, und damit gibt es zwei Linien, die sich nicht mischen lassen:

NamensraumMetro-Linie
Altbestand, Java 8 bis 11javax.xml.ws2.3.x
Alles Neuerejakarta.xml.ws4.0.x

Wer ein bestehendes Projekt nur auf einem aktuellen JDK bauen will, ohne den Code anzufassen, bleibt bei 2.3.x. Wer ohnehin migriert, nimmt gleich 4.0.x, weil die alte Linie keine Zukunft mehr hat. Die Beispiele unten nutzen die Jakarta-Variante.

Einrichtung mit Maven

<properties>
    <jaxws.version>4.0.5</jaxws.version>
</properties>

<dependencies>
    <!-- Zieht die API jakarta.xml.ws-api transitiv mit -->
    <dependency>
        <groupId>com.sun.xml.ws</groupId>
        <artifactId>jaxws-rt</artifactId>
        <version>${jaxws.version}</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>com.sun.xml.ws</groupId>
            <artifactId>jaxws-maven-plugin</artifactId>
            <version>${jaxws.version}</version>
            <executions>
                <execution>
                    <id>wsimport-partner-service</id>
                    <goals>
                        <goal>wsimport</goal>
                    </goals>
                    <configuration>
                        <!-- Die WSDL liegt im Repository, nicht auf dem Server des
                             Partners. Sonst hängt der Build an dessen Verfügbarkeit,
                             und eine stille Änderung fällt erst zur Laufzeit auf. -->
                        <wsdlDirectory>${project.basedir}/src/main/wsdl</wsdlDirectory>
                        <wsdlFiles>
                            <wsdlFile>partner-service.wsdl</wsdlFile>
                        </wsdlFiles>
                        <packageName>com.example.partner.generated</packageName>
                        <sourceDestDir>
                            ${project.build.directory}/generated-sources/wsimport
                        </sourceDestDir>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>Code language: HTML, XML (xml)

Das Plugin bringt wsimport als Bibliothek mit, es braucht also kein Werkzeug im Pfad und keine bestimmte JDK-Version. mvn generate-sources erzeugt die Klassen, und die IDE findet sie, weil das Plugin das Verzeichnis als Quellordner anmeldet.

Der Kommentar zur lokalen WSDL ist keine Schönheitsfrage. Ein Build, der zur Bauzeit eine fremde URL abruft, schlägt fehl, sobald der Partner sein System wartet, und er ändert stillschweigend die erzeugten Klassen, sobald der Partner etwas an der Schnittstelle dreht. Die WSDL gehört ins Repository, und ihre Aktualisierung gehört in einen bewussten Commit.

Den erzeugten Client benutzen

import com.example.partner.generated.PartnerPort;
import com.example.partner.generated.PartnerService;
import jakarta.xml.ws.BindingProvider;

import java.util.Map;

PartnerService service = new PartnerService();
PartnerPort port = service.getPartnerPort();

Map<String, Object> context = ((BindingProvider) port).getRequestContext();
context.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, endpointUrl);
context.put(BindingProvider.USERNAME_PROPERTY, user);
context.put(BindingProvider.PASSWORD_PROPERTY, password);

// Ohne Timeouts wartet der Aufruf unbegrenzt. Das ist die häufigste Ursache
// dafür, dass ein Thread-Pool bei einer Störung beim Partner vollläuft.
context.put("com.sun.xml.ws.connect.timeout", 10_000);
context.put("com.sun.xml.ws.request.timeout", 30_000);

Rechnung rechnung = port.rechnungAbrufen(rechnungsnummer);Code language: Java (java)

Zwei Dinge, die man dem erzeugten Code nicht ansieht. Das Service-Objekt einmal anzulegen kostet spürbar Zeit, weil dabei die WSDL geparst wird; es gehört also in ein Feld und nicht in die Methode. Der daraus erzeugte Port dagegen ist nicht threadsicher. Wer ihn über mehrere Threads teilt, bekommt vermischte Anfragen, und der Fehler tritt erst unter Last auf.

Die Endpunkt-Adresse setzt man zur Laufzeit, statt sie aus der WSDL zu übernehmen. Sonst zeigt die Testumgebung auf den Produktivdienst, sobald jemand die WSDL frisch vom Partner zieht.

Wenn es doch die Kommandozeile sein muss

Für einmalige Erzeugung ohne Build-Integration gibt es die Standalone-Distribution von Eclipse Metro, die wsimport als Skript enthält. Herunterladen, entpacken, aufrufen:

./wsimport.sh -keep -verbose \
  -p com.example.partner.generated \
  -d ./generated \
  https://partner.example.com/service?wsdlCode language: Bash (bash)

Praktisch braucht das kaum jemand. Sobald die Schnittstelle sich einmal ändert, will man die Erzeugung im Build haben.

Fehler, die nach der Umstellung auftreten

ClassNotFoundException: javax.xml.ws.Service bedeutet, dass der Code noch die alte API erwartet und die Abhängigkeit fehlt. Entweder die 2.3.x-Linie ergänzen oder auf jakarta.xml.ws umstellen.

Der erzeugte Code importiert javax.*, das Projekt erwartet jakarta.*. Dann steht eine 2.3.x-Version des Plugins im POM. Die Namensräume folgen der Plugin-Version, nicht der JDK-Version.

Die Binding-Datei wird ignoriert. Auch der Namensraum der Anpassungsdateien hat sich geändert. Aus http://java.sun.com/xml/ns/jaxws wurde https://jakarta.ee/xml/ns/jaxws, und eine Datei im alten Namensraum wird von der 4.0-Linie kommentarlos übergangen:

<bindings xmlns="https://jakarta.ee/xml/ns/jaxws" version="3.0">
    <package name="com.example.partner.generated"/>
</bindings>Code language: HTML, XML (xml)

Lohnt SOAP heute noch

Für eine neue Schnittstelle, die man selbst entwirft, fast nie. Der Aufwand an Werkzeugen und XML-Konfiguration steht in keinem Verhältnis zu dem, was ein JSON-Dienst mit OpenAPI-Beschreibung kostet.

Bei bestehenden Integrationen sieht es anders aus, und dort hat der Ansatz einen Vorteil, den man in der REST-Welt selten bekommt: Der Vertrag ist verbindlich und maschinenlesbar. Ändert der Partner die Schnittstelle, bricht der Build nach dem Aktualisieren der WSDL, und zwar beim Kompilieren statt im Produktivbetrieb. Wer schon einmal eine OpenAPI-Spezifikation gesehen hat, die von der Wirklichkeit abweicht, weiß das zu schätzen.

wsimport nach Java 11: SOAP-Client aus einer WSDL erzeugen
Tagged on:                         

Leave a Reply

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