Spring Boot & Apache CXF – SOAP ohne XML?

46 Kommentare

Auch wenn es so aussieht, als hätte sich die Welt schon lange weitergedreht, gibt es immer noch Projekte, in denen Webservices mit SOAP gebaut oder genutzt werden. Doch warum sollten wir diese nicht auf eine aktuelle technische Basis mit Spring Boot stellen und Apache CXF die Spring-XML-Konfiguration abgewöhnen?

Spring Boot & Apache CXF – Tutorial

Part 1: Spring Boot & Apache CXF – SOAP ohne XML?
Part 2: Spring Boot & Apache CXF – SOAP Webservices testen
Part 3: Spring Boot & Apache CXF – XML-Validierung und Custom SOAP Faults
Part 4: Spring Boot & Apache CXF – Logging & Monitoring mit Logback, Elasticsearch, Logstash & Kibana
Part 5: Spring Boot & Apache CXF – Von 0 auf SOAP mit dem cxf-spring-boot-starter

Natürlich gibt es schon seit Langem einen Trend hin zu RESTful oder anderen Services. Und sicherlich kann man sich hippere Themen vorstellen, die sich viel besser in einer Kaffeeküche im Gespräch mit Kollegen oder auf Konferenzen machen. Doch trotzdem gibt es diese Projekte. Und noch viele Anwendungen mehr, in denen noch lange auf SOAP gesetzt werden wird. Nicht zuletzt hat eine normierte Schnittstellenbeschreibung durchaus ihren Charme – wie sich auch in den Bestrebungen rund um JSON-Schema ablesen lässt.

Ok, also SOAP. Dann aber bitte auf Basis aktueller Technologien!

Wenn es also SOAP sein soll und sowieso ein neues Framework aufgebaut wird, dann kann man auch auf Basis aktueller (Java-)Technologien starten und sich ein wenig von aktuellen Frameworks und Vorgehensweisen abschauen, die in anderen Feldern erfolgreich eingesetzt werden. Sei es Spring Boot, was gern in Microservice-Projekten zum Einsatz kommt, die Logfile-Analyse mit Hilfe des Elasticsearch-Logstash-Kibana-Stacks (ELK) oder ein reibungsloses Deployment per Continuous Integration & Delivery-Pipeline.

Ein gutes Beispiel…

Beginnt man mit Hilfe eines der exzellenten Starter-Guides von spring.io, hat man sehr schnell ein lauffähiges Beispiel eines SOAP-Webservices. Zum Starten reicht ein „Run as…“ in der IDE oder ein „java -jar“ auf der Kommandozeile aus und schon hat man einen SOAP-Webservice, der sich z.B. mit Hilfe des SOAP-Testclients SoapUI aufrufen lässt. Aber: Auf diesem Hello-World-Level bleibt es natürlich in realen Projekten nicht, und das fängt schon bei dem verwendeten Beispiel-Webservice an, der über ein sehr kleines XML-Schema definiert ist und keine WSDL einbindet – wie es bei SOAP-Services eigentlich der Normalfall ist.

Zusätzlich bleibt es bei vielen WSDLs nicht bei einer solchen, sondern es werden oft mehrere XDSs in die WSDL importiert, einhergehend mit ebenso vielen Namespace-Definitionen. Leider gibt es keine frei verfügbaren & vergleichbaren Webservices. Um für dieses Tutorial ein Beispiel zu bekommen, das in die Nähe realer „Enterprise-WSDLs“ kommt (wie z.B. die Webservices der BiPro, die vor allem in der Versicherungswirtschaft eingesetzt werden), musste ich improvisieren. In vielen Beispielen wird der WeatherWS-Service von CDYNE verwendet. Die frei verfügbare WSDL habe ich um viele im Enterprise-Bereich wichtige Dinge erweitert – seien es verschachtelte XSD-Imports, deutlich erweiterte Request-Messages, eigene Custom-Exceptions oder Webservice-Methoden, die Attachments (z.B. PDFs) zurückliefern. Genauere Details und eine Beschreibung, wie die WSDL für dieses Tutorial aussieht, sehen wir uns in Step 2 an…

Warum Apache CXF und nicht SpringWS…

Schnell wird beim Betrachten der genannten WSDLs und ihrer Spezifikationen klar, dass viele der möglichen WS*-Standards eingesetzt werden und damit natürlich auch durch das Webservice-Framework unterstützt werden müssen. Nach meiner Erfahrung ist es trotz aller Standardisierungsbemühungen in Extremfällen (die natürlich auch garantiert im eigenen Projekt auftreten) immer am besten, das Framework einzusetzen, das die größte Verbreitung am Markt hat. Und das ist leider nicht SpringWS – auch wenn sich dieses von Haus aus am besten in Spring Boot integriert und deshalb in den meisten Tutorials verwendet wird. Das „most widely used WebServices Framework“ ist Apache CXF. Wenn es mit CXF nicht geht, geht es meist gar nicht.
Trotzdem sehen wir nicht ein, mit Spring 4.x unsere Spring-Beans per XML konfigurieren zu müssen – auch, wenn nahezu die gesamte Apache CXF Doku auf XML-Konfigurations-Beispiele setzt. Im Tutorial sehen wir, wie Apache CXF rein Annotations-getrieben konfiguriert werden kann.

SOAP ohne XML/XSLT – was soll das denn heißen?

Es stimmt natürlich: Am Ende des Tages müssen SOAP- (und damit XML-)Requests verarbeitet und Responses zurückgegeben werden. Doch heißt das, wir müssen deshalb mit XML-Technologien hantieren? Bedeutet das, wir müssen unser eingestaubtes XSLT-Kompendium aus dem Schrank holen, erneut die Vor- und Nachteile der verschiedenen XML-Parser betrachten (DOM vs. SAX) und wieder auf unseren lieb gewonnenen Compiler in der IDE verzichten, der uns dann nicht mehr mit Warnhinweisen hilft, falls sich mal das Datenmodell (XML-Schema) ändert? Genau darauf haben wir 2016 irgendwie keine Lust mehr (auch wenn mich mancher für diese Aussage einen Kopf kürzer machen wird, etwa der Autor dieses sehr guten REST-Buches). Aber wir wollen nicht auf unseren Compiler verzichten und haben uns schon lange an den Komfort eines automatischen Mappings von und zu Java gewöhnt, wie es bei JSON bzw. Jackson der Fall ist. Doch geht das überhaupt? Ja, es geht. Und wir werden Step-by-Step sehen, wie.

Step1: Also los…

Die folgenden Schritte kann man komplett im GitHub-Repository tutorial-soap-spring-boot-cxf nachvollziehen. Das passende Projekt für den ersten Step liegt ebenfalls darin.

Spring Boot & CXF ans Laufen bringen…

Fangen wir mit dem Hochziehen unserer Spring-Boot-Anwendung an und nutzen z.B. den Spring Initializr. Hier reicht die Auswahl von Web- und den Dev-Tools. Nachdem wir das Projekt lokal in unserer IDE haben, benötigt unsere pom.xml noch die Dependency zu CXF, d.h. wir fügen unter <properties> die aktuelle CXF-Version ein (aktuell 3.1.x, siehe mvnrepository.com) und erweitern die Dependencies um cxfrtfrontendjaxws und cxfrt-transports-http. Nachdem das Projekt die Dependencies geladen hat, fügen wir der vom Initializr generierten ***Application.java zwei Beans hinzu, die CXF komplett initialisieren:

@SpringBootApplication
public class SimpleBootCxfApplication {
 
    public static void main(String[] args) {
	SpringApplication.run(SimpleBootCxfApplication.class, args);
    }
 
    @Bean
    public ServletRegistrationBean dispatcherServlet() {
        return new ServletRegistrationBean(new CXFServlet(), "/soap-api/*");
    }
 
    @Bean(name=Bus.DEFAULT_BUS_ID)
    public SpringBus springBus() {      
        return new SpringBus();
    }
}

Das CXFServlet übernimmt alle SOAP-Anfragen, die über unsere URI /soap-api/* ankommen und der cxf-SpringBus zieht als Kernkomponente des CXF-Frameworks alle nötigen Module hoch (siehe Apache CXF Architektur). Sobald wir unsere ***Application.java starten (z.B. über ein simples „Run as…“), wird Spring Boot mit Apache CXF hochgefahren und wir können in unserem Browser auf http://localhost:8080/soap-api schon die CXF-Servicelist sehen, die momentan natürlich noch keine Services enthält:

No services have been found.

So weit, so gut 🙂

Step2: Aus WSDL mach‘ Java…

Um das Ziel „no XML“ zu erreichen, kann man ein XML-Databinding-Framework einsetzen ( Java Architecture for XML Binding (JAXB) ). In Kombination mit der „Java API for XML Web Services“ (JAX-WS) ergibt sich eine sehr komfortable Möglichkeit, SOAP-Webservices mit Java-Bordmitteln anzubieten – die Referenzimplementierung (RI) ist Teil der Java-Runtime und kann somit out-of-the-box verwendet werden.

Ein gutes Beispiel gibt es nicht umsonst…

Wir erweitern unser Beispielprojekt aus Step 1 – das komplette Projekt für Step 2 liegt wieder auf GitHub.

Der erwähnte Beispielwebservice http://wsf.cdyne.com/WeatherWS/Weather.asmx?WSDL ist von der Struktur her nicht mit größeren Webservice-Schnittstellen-Definitionen zu vergleichen. Um ein vergleichbares Szenario zu bekommen, habe ich ihn angepasst – es gibt nämlich leider keine frei verfügbaren Quellen für komplexere Webservice-Definitionen. Die vollständige WSDL mit allen importierten XSDs liegt ebenfalls auf GitHub bereit.

Falls einem zwischendurch der Kopf qualmt mit all dem WSDL-Gedöns, einen netten Refresher liefert dieser blog. Falls auch das zu viel ist: WSDLs einfach immer schön von unten nach oben lesen! 🙂

Unnötiges rauswerfen…

Der in der Ausgangs-WSDL beschriebene Weather-Service hat mehrere wsdl:ports, die auf jeweils ein eigenes wsdl:binding verweisen – was für uns nur zu unnötiger Komplexität führt, deshalb gibt es in unserem Beispiel nur noch einen Port:

<wsdl:service name="Weather">
	<wsdl:port name="WeatherService" binding="weather:WeatherService">
		<soap:address location="http://localhost:8095/soap-api/WeatherSoapService_1.0"/>
	</wsdl:port>
</wsdl:service>

Da der Webservice drei Methoden (wsdl:operation) definiert, sind diese im Ausgangsbeispiel vierfach definiert, was sehr unübersichtlich ist. Im modifizierten Beispiel haben wir ein wsdl:binding, das alle drei Methoden definiert:

<wsdl:operation name=“GetWeatherInformation“></wsdl:operation>
<wsdl:operation name=“GetCityForecastByZIP“></wsdl:operation>
<wsdl:operation name=“GetCityWeatherByZIP“></wsdl:operation>

Wer in das GitHub-Repository schaut, sieht auch die Definition eines eigenen Exception-Typs. Wie man damit umgeht, erklärt ein folgender Tutorial-Step. Die wsdl:portTypes definieren dann im Detail, wie in XML-Requests und -Responses die einzelnen Methoden aussehen – sowie, was im Fehlerfall zurückgeliefert wird.

Mehrstufige XSD-Imports…

Nach der Definition der wsdl:messages folgt dann der Verweis auf einzelne Fragmente im XML-Schema. Hier ergibt sich die größte Änderung gegenüber dem Ausgangsbeispiel: Unsere WSDL importiert die zentrale Weather1.0.xsd, die wiederum weather-general.xsd und weather-exception.xsd importiert. Es folgen weitere Imports in diesen XSDs.

Dieser Aufwand war nötig, um die deutlich größeren und noch komplexeren Webservices nachzubilden, die in der Praxis zum Einsatz kommen. Natürlich kann unser Beispiel trotzdem nicht mit solchen konkurrieren. Will es auch nicht. Es geht nur darum, möglichst viele in der Praxis notwendigen Techniken anschaulich darstellen zu können. Ich war sehr gespannt, ob die Toolchain damit umgehen kann. Und sie konnte. Aber eins nach dem anderen 🙂

Jetzt aber: WSDL-2-Java

Da die WSDL in einem „Contract-First“-Ansatz die Webservice-Schnittstelle beschreibt, sollten unseren dazu passenden Java-Klassen immer auf dem aktuellen Stand der Schnittstelle basieren und idealer Weise immer daraus neu generiert werden. Da die WSDL somit die „Wahrheit“ darstellt, wollen wir keine einzige per JAXB generierte Java-Klasse in unserer Versionsverwaltung einchecken. Für ein solche Anforderung bietet sich ein Maven-Plugin an, das in der Generate-Sources-Phase alle notwendigen Bindings und Java-Klassen erzeugt – sowohl für die für den Webservice an sich erforderlichen technischen als auch die fachlich inhaltlichen Klassen.

Schaut man sich die empfohlenen Spring Getting started Guides an, so wird meist das jaxb2-maven-plugin genutzt. Wenn man sich ein bisschen umschaut, findet man noch viele weitere Plugins und entsprechende Diskussionen, welches denn das beste sei. Da wir uns für JAX-WS entschieden haben, liegt die Nutzung des JAX-WS-commons project nahe, das auch ein Maven-Plugin bereitstellt.

Aber Achtung: Das JAX-WS-Maven-Plugin steht wieder unter der Hoheit von mojohaus und wird dort weiterentwickelt. In allen Tutorial-Schritten werden wir immer das aktuellere mit der GroupId org.codehaus.mojo statt org.jvnet.jax-ws-commons verwenden.

Maven-Plugin-Konfiguration

Die Konfiguration des jaxws-maven-plugin sollte dabei nicht unterschätzt werden. Widmen wir uns also der build-section unserer pom:

<plugin>
	<groupId>org.codehaus.mojo</groupId>
	<artifactId>jaxws-maven-plugin</artifactId>
	<version>2.4.1</version>
	<configuration>...</configuration>
</plugin>

Hier ist vor allem das <configuration>-Tag interessant:

<configuration>
	<wsdlUrls>
		<wsdlUrl>src/main/resources/service-api-definition/Weather1.0.wsdl</wsdlUrl>
	</wsdlUrls>
	<sourceDestDir>target/generated-sources/wsdlimport/Weather1.0</sourceDestDir>
	<vmArgs>
		<vmArg>-Djavax.xml.accessExternalSchema=all</vmArg>
	</vmArgs>
</configuration>

Das <wsdlUrl> definiert den Ablageort unserer WSDL, <sourceDestDir>, wohin die Java-Klassen geschrieben werden sollen. Weil wir ein realistisches Beispiel gewählt haben, würde diese Konfiguration für eine WSDL mit mehreren verschachtelten XSD-Imports nicht funktionieren – also müssen wir ein <vmArg> übergeben: -Djavax.xml.accessExternalSchema=all sorgt dafür, dass alle XSDs mit einbezogen werden.

Nach der notwendigen Definition des goals wsimport nutzen wir noch ein zweites Plugin: das build-helper-maven-plugin. Hiermit fügen wir die generierten Java-Klassen unserem Classpath hinzu und können sie in unserem Projekt nutzen. Wer es selbst ausprobieren möchte: Im Projekt step2_wsdl_2_java_maven auf der Kommandozeile ein 

mvn clean generate-sources

ausführen. Dies sollte alle nötigen Klassen im Ordner target/generated-sources/wsdlimport/Weather1.0 erzeugen. Das Ergebnis ähnelt in seiner package-Struktur dem Schnitt der verschiedenen XSDs.

Damit die generierten Java-Klassen nicht versehentlich im Versionskontrollsystem eingecheckt werden, nutzen wir den /target-Ordner, dessen Inhalt grundsätzlich nicht eingecheckt werden sollte (bei git nutzen wir einen entsprechenden Eintrag in der .gitignore).

Step3: Ein lauffähiger SOAP-Endpunkt

Im nächsten Schritt bringen wir endlich einen SOAP-Endpunkt zum Leben. Dafür erweitern wir das Beispielprojekt aus Step2. Der vollständige Code findet sich wie gewohnt auf GitHub im Projekt step3_jaxws-endpoint-cxf-spring-boot.

Da wir nun eine umfangreichere Konfiguration benötigen, sollten wir eine eigene @Configuration-Klasse erstellen und dort CXF und den Endpoint initialisieren – z.B. in der WebServiceConfiguration.java. Unsere Application-Klasse zum Starten von Spring Boot reduziert sich auf die bekannte main()-Methode. Zusätzlich können wir aber ein @ComponentScan nutzen, damit wir Spring das Scannen nach Beans und Configuration-Klassen erleichtern.

In unserer Configuration-Klasse finden sich wieder die Beans SpringBus und ServletRegistrationBean. Für die Konfiguration eines Endpoints brauchen wir zwei weitere Beans. Zuerst definieren wir das Service Endpoint Interface (SEI):

@Bean
public WeatherService weatherService() {
	return new WeatherServiceEndpoint();
}

Die das SEI implementierende Klasse WeatherServiceEndpoint wird nicht generiert und wir müssen sie erstellen. Dies ist auch der Ort, an dem die fachliche Implementierung unseres Services beginnt. In diesem Step aber müssen wir nur die Klasse erstellen, damit wir sie in unserer Beandefinition instanziieren können.

Die zweite Bean ist der javax.xml.ws.Endpoint. Auch hier ist die CXF-Doku recht dünn. Der Knackpunkt ist an dieser Stelle, dass wir eine Instanz von org.apache.cxf.jaxws.EndpointImpl zurückliefern, der wir den SpringBus und unseren WeatherServiceEndpoint im Konstruktor übergeben:

@Bean
public Endpoint endpoint() {
	EndpointImpl endpoint = new EndpointImpl(springBus(), weatherService());
	endpoint.publish("/WeatherSoapService_1.0");
	endpoint.setWsdlLocation("Weather1.0.wsdl");
	return endpoint;
}

Zusätzlich wichtig ist die .publish-Konfiguration. So geben wir an, wie der letzte Teil der Service-URI lautet.

Lässt man nun Spring Boot wie gewohnt laufen, kann man unter http://localhost:8080/soap-api/ unseren WeatherService unter „Available SOAP services“ sehen, inkl. aller drei definierten Webservice-Methoden. In einem späteren Schritt werden wir sehen, wie wir unseren Service aus einem Unit- oder Integrationstest heraus aufrufen können. An dieser Stelle reicht ein Test mit SoapUI. Übergibt man in SoapUI bei „New SOAP Project“ die URI unserer WSDL, generiert es alles Notwendige, und man kann einen Request starten, der ohne Fehlermeldung eine (noch sehr schmale) Response liefert.

Und vóila. Unser erster SOAP-Endpoint mit Spring Boot, Apache CXF und JAX-WS steht. Doch es gibt noch einiges zu tun. Im nächsten Teil dieses Tutorials schauen wir uns dann an, wie man einen SOAP-Service testen kann – sei es im Unit- oder Integrationstest. Außerdem werden wir sehen, wie man die Namespace-Prefixes der Responses aufhübscht und die SOAP-Faults an ein vordefiniertes Schema anpasst, auch wenn gar kein XML an unseren Endpoint geschickt wird oder das XML-Schema nicht eingehalten wird. Natürlich wollen wir die Aufrufe ein wenig im Auge behalten und nutzen dazu einen ELK-Stack. Zuletzt gibt es noch einen Vorschlag, wie man fachliche Validierungsprüfungen, die über die XML-Schemavalidierung hinausgehen und z.B. für einen Backend-Call nötig sein können, mit Hilfe des noch recht jungen DMN-Standards und der camunda DMN-Engine umsetzen kann.

 

Jonas Hecht

Die Überzeugung, dass Softwarearchitektur und Hands-on Entwicklung zusammengehören, führte Jonas zu codecentric. Tiefgreifende Erfahrungen in allen Bereichen der Softwareentwicklung großer Unternehmen treffen auf Leidenschaft für neue Technologien. Im Fokus stand dabei immer die Integration verschiedenster Systeme (und Menschen).

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentare

  • 18. Februar 2016 von Mario Steinhoff

    Tz, erst über XML herziehen und dann mit Maven pom’s um die Ecke kommen!!!11
    Eine richtige 2016-Version müsste doch eigentlich gradle benutzen 🙂

    Aber im ernst, sehr schöner Artikel, gut geschrieben, pragmatisch und ohne viel Enterprise-Bullshit. Gleichzeitig auch einigermaßen ausgewogen, also tiefergehend als das übliche HelloWorld-ja-und-jetzt?-Geschwurbel ohne dabei zu komplex zu werden.

    Gefällt mir sehr gut!

    Ich konnte mich bis jetzt zum Glück vor dem ganzen WSDL-Mist drücken, aber falls ich das doch mal machen muss weiß ich ja wo ich zuerst gucke. Schönen Gruß in die Nachbarschaft 🙂

  • 4. März 2016 von Hendrik

    Der Beitrag kam genau zur richtigen Zeit 🙂
    Ich bedanke mich für die ausführliche Übersicht und Einführung.

  • 10. März 2016 von Adam Giemza

    Sehr schöner Beitrag, hat mir sehr geholfen. Ich bin aber evtl. auf einen Fehler und daraus resultierend auf eine Frage gestoßen:

    endpoint.publish(„/WeatherSoapService_1.0“);
    endpoint.setWsdlLocation(„Weather1.0.wsdl“);

    Die zweite Zeile hat keine Wirkung, nachdem der Service schon „gepublished“ wurde (der Pfad ist zu der WSDL-Datei ist auch falsch). Vertauscht man die beiden Dateien und korrigiert den Pfad …

    endpoint.setWsdlLocation(„service-api-definition/Weather1.0.wsdl“);
    endpoint.publish(„/WeatherSoapService_1.0“);

    … dann gibt es auf ein mal zwei Services in der ausgelieferten „?wsdl“.

    Wie bekommt man es denn hin, dass CXF mit Spring Boot auch im „contract-first“-Ansatz funktioniert?

    • Jonas Hecht

      21. März 2016 von Jonas Hecht

      Vielen Dank für die detaillierte Frage! Die bisherige Lösung kann man getrost als contract-first bezeichnen, schließlich arbeiten wir nur mit aus der WSDL generierten Artefakten. Trotzdem ist ihre Feststellung korrekt: der Aufruf der Methode

      endpoint.setWsdlLocation(“Weather1.0.wsdl);

      hat keinen Effekt, nach dem endpoint.publish() und es entstehen zwei Services, wenn man die Methodenaufrufe umdreht. Zusätzlich wird bei der bisherigen Lösung eine Art Wrapper-WSDL um die eigentliche WSDL durch CXF generiert, die eigentlich unnötig ist. Wir haben ja eine vordefinierte WSDL und wollen die auch so ausliefern.

      Es gibt aber einen 100%-Lösung: Der Schlüssel dazu ist das Wissen, dass die JAX-WS-Implementierung von Apache CXF (speziell in der Methode create der Klasse WSDLServiceFactory.class) auf dem korrekt gesetzten Service-Namen aufsetzt. Wird dieser nicht bei der Konfiguration der Klasse org.apache.cxf.jaxws.EndpointImpl korrekt gesetzt, spinnt sich CXF einen eigenen Namen aus der das SEI implementierenden Klasse zurecht (der zweite Service in dem Fall, wenn man die Methoden vertauscht). Diesen Namen kann man manuell setzen, was aber unschön ist.

      Hier kommt der von JAX-WS generierte Service ins Spiel – dabei ist nicht das SEI gemeint, sondern tatsächlich die den Service repräsentierende Klasse – in unserem Fall die Klasse de.codecentric.namespace.weatherservice.Weather (nicht zu verwechseln mit unserem SEI de.codecentric.namespace.weatherservice.WeatherService). Diese „kennt“ den korrekten Service-Namen als javax.xml.namespace.QName und nebenbei auch noch die korrekte WSDLLocation (sofern man einmal korrekt per mvn generate-sources alles nötige erzeugt hat). Die Lösung habe ich in ein neues Projekt auf github veröffentlicht – speziell unsere WebServiceConfiguration.java interessiert hier:

      @Bean
      public Endpoint endpoint() {
          EndpointImpl endpoint = new EndpointImpl(springBus(), weatherService());        
          endpoint.setServiceName(weather().getServiceName());
          endpoint.setWsdlLocation(weather().getWSDLDocumentLocation().toString());
          endpoint.publish("/WeatherSoapService_1.0");
          return endpoint;
      }
       
      @Bean
      public Weather weather() {
          return new Weather();
      }

      Führt man diese Konfiguration aus, hat man 100% contract-first mit Apache CXF und SpringBoot 🙂

      • 21. März 2016 von Adam Giemza

        Besten Dank! Funktioniert 🙂

      • 27. September 2016 von Thomas Arand

        Die Lösung hat eine Haken: new Weather() ruft ja eine generierte Klasse auf, und der Default-Constructor benutzt als file-URL die (absolute!) Stelle der wsdl-Datei auf dem Entwicklungsrechner.
        Ich habe das gemerkt, als ich zwischen PC und LINUX-System hin und her gewechselt habe. Dann bekommt man nämliche ein FileNotFound-Exception.
        Helfen kann ich mir mit
        return new Weather(new URL(„file:src/main/resources/service-api-definition/Weather1.0.wsdl“));
        Dann läuft es zumindest in dieser heterogenen Entwicklungsumgebung. Allerdings ist das für einen Produktivcode nicht geeignet.

        • Jonas Hecht

          27. September 2016 von Jonas Hecht

          Da haben Sie natürlich absolut recht und genau das ist uns im Projekt auch schon passiert. Das Problem ist der Inhalt der Konstante private final static URL WEATHER_WSDL_LOCATION der generierten Klasse Weather.class, wie Sie schon korrekt bemerkt haben. Diese enthält den harten Link innerhalb eines konkreten Filesystems – nämlich genau dort, wo wir das jaxws-maven-plugin ausgeführt haben.

          Doch auch hier gibt es eine elegante Lösung, die uns weiterhin die Möglichkeit gibt, auf den von JAX-WS generierten Service wie beschrieben zu verwenden: Wir passen die Konfiguration des jaxws-maven-plugin leicht an. Statt des im Artikel beschriebenen Tags

          <wsdlUrls>
              <wsdlUrl>src/main/resources/service-api-definition/Weather1.0.wsdl</wsdlUrl>
          </wsdlUrls>

          setzen wir auf eine Kombination der tags wsdlDirectory und wsdlLocation. Ich bitte aber schon hier um erhöhte Aufmerksamkeit – hier muss jeder „/“ stimmen 🙂 Sonst kann man hierbei Stunden beim Suchen verbringen…

          <wsdlDirectory>${basedir}/src/main/resources/service-api-definition/</wsdlDirectory>
          <wsdlLocation>/service-api-definition/Weather1.0.wsdl</wsdlLocation>

          Haben wir so unser jaxws-maven-plugin konfiguriert und führen ein erneutes mvn generate-sources aus, dann enthält die Konstante unserer generierten JAX-WS Klasse Weather.class den Wert de.codecentric.namespace.weatherservice.Weather.class.getResource("/service-api-definition/Weather1.0.wsdl"), was unsere Lösung wieder Plattformunabhängig und produktiv-fähig machen sollte.

          Zumindest unser Jenkins beschwert sich nicht mehr – egal ob Linux Master oder Windows Slave 😉

          • 28. September 2016 von Thomas Arand

            Na, auf die Idee muss man erstmal kommen! Die Beschreibung der Plugin-Konfiguration gibt das ja nicht her (wie leider so oft bei diesen Dingen…).
            Danke auf jeden Fall!

  • 25. März 2016 von J. Albers

    Sehr guter Artikel!!!

  • 29. April 2016 von Thomas Koch

    Sehr gelungener Artikel.

    Wir wollten zunächst auf spring-boot und spring-ws aufsetzen. Nach schnellen Erfolgen und der damit einhergehenden Euphorie wurden wir bereits beim Versuch Webservice-BusinessFaults abzubilden herb enttäuscht. Letztlich zeigte sich spring-ws hier als so unausgereift, dass wir einen Schwenk in Richtung Apache CXF vollzogen haben. Da kam dieser Artikel natürlich gerade recht, da wir das ganze natürlich gerne Annotation-basiert also ohne Spring-XML-Konfiguration aufsetzen wollten. Das hat alles gut geklappt und die Lösung ist recht elegant. Lediglich das jaxws-maven-plugin hat bei uns in der Version 2.3 nicht funktioniert, hier mussten wir auf die Version 2.2 zurückgehen.

    Wir freuen uns schon auf den zweiten Teil.

    • Jonas Hecht

      29. April 2016 von Jonas Hecht

      Vielen Dank!

      Hier wäre es interessant zu erfahren, was mit der 2.3er Version nicht funktioniert hat.

      Aktuell stelle ich gerade auf die 2.4.1 um – die gibt es laut http://mvnrepository.com/artifact/org.jvnet.jax-ws-commons/jaxws-maven-plugin auf den ersten Blick gar nicht, aber das Projekt wurde kürzlich erst wieder zu http://www.mojohaus.org/jaxws-maven-plugin/ umgezogen (also wieder dahin zurück, wo es 2007 mal her kam…) und wird dort weiterentwickelt – das findet man aber nur heraus, wenn man explizit danach sucht, da sich eben die groupId wieder geändert hat. Hier auch der entsprechende github-link – vielleicht ist der Fehler gefixed worden?!

      In der pom sieht das dann so aus:

      <groupId>org.codehaus.mojo</groupId>
      <artifactId>jaxws-maven-plugin</artifactId>
      <version>2.4.1</version>
  • 29. April 2016 von Adam Giemza

    Wir benutzen das cxf-codegen-plugin, was auch wunderbar mit dieser Anleitung funktioniert.

    So in etwa als build plugin einbinden:

    org.apache.cxf
    cxf-codegen-plugin
    ${cxf.version}

    generate-sources
    generate-sources

    usw.

  • 13. Juli 2016 von Ferdinand Schneider

    Hey Jonas,
    zufälligerweise hatte ich genau dafür einen Anwendungsfall im Projekt und kam dank deines Posts recht schnell voran bei der Erstellung eines SOAP-Service auf Spring-Boot Basis.
    Danke und Gruß.

    • Jonas Hecht

      13. Juli 2016 von Jonas Hecht

      Hi Ferdinand, schön von Dir zu hören. Es ist mir natürlich eine besondere Freude, das zu hören! Viele Grüße in den Süden Deutschlands – gerne auch an die Kollegen 🙂

  • 28. Juli 2016 von Dennis Kieselhorst

    Guter Beitrag, inzwischen gibt es auch einen Starter von CXF: http://cxf.apache.org/docs/springboot.html

  • 27. September 2016 von Thomas Arand

    Ein sehr schönes Tutorial! Genau was ich gesucht habe!
    Ein paar Anmerkungen – ein bisschen Zeit ist ja vergangen:
    – Ich habe problemlos auf spring-boot 1.4.1 umgestellt. Allerdings sollte man dann org.springframework.boot.web.servlet.ServletRegistrationBean benutzen (die in den Sourcen importierte Version ist inzwischen deprecated)
    – Ebenso problemlos ist cxf.version 3.1.7
    – Ich bin ein Fan von „so wenig Konfiguration in pom-files wie möglich“. Daher habe ich in der Konfiguration für jaxws-maven-plugin die Anweisung weggelassen. Dann kann man sich das build-helper-plugin komplett sparen.

    • 27. September 2016 von Thomas Arand

      Oops, hier hat der Editor was vergessen! Am Ende muss es heißen
      Daher habe ich in der Konfiguration für jaxws-maven-plugin die Anweisung weggelassen

      • 27. September 2016 von Thomas Arand

        Aha, der Fehler ist reproduzierbar, wenn man die kleiner-größer Zeichen verwendet :-). Also nochmal, ohne diese Zeichen:
        Daher habe ich in der Konfiguration für jaxws-maven-plugin die sourceDestDir-Anweisung weggelassen

    • Jonas Hecht

      27. September 2016 von Jonas Hecht

      Vielen Dank 🙂

      Das stimmt, man kann problemlos auf Spring Boot 1.4.x wechseln – außer, man will meinem Vorschlag folgen, das Logging per logstash-logback-encoder in einen Elastic-Stack zu pumpen (siehe Teil 4 der Artikelserie: https://blog.codecentric.de/2016/07/spring-boot-apache-cxf-logging-monitoring-logback-elasticsearch-logstash-kibana/). Dann muss man aktuell zusätzlich in der pom die logback Version von Spring Boot (aktuell 1.1.7) auf 1.1.6 downgraden – sonst läuft man in aktuell nur in logback 1.1.8 gelöste Probleme, das aktuell leider noch nicht released ist.

      Interessant ist bei dem Umstieg noch das Thema Testing, dass die Boot Entwickler nochmals verschlankt haben.

    • Jonas Hecht

      12. Oktober 2016 von Jonas Hecht

      Für Fans von „so wenig Konfiguration in pom-Files wie möglich“ gibt es jetzt auch das https://github.com/codecentric/cxf-spring-boot-starter-maven-plugin. Dann sind es wirklich nur noch folgende Zeilen:

      <groupId>de.codecentric</groupId>
      <artifactId>cxf-spring-boot-starter-maven-plugin</artifactId>
      <version>1.0.7.RELEASE</version>
      <executions>
          <execution>
              <goals>
                  <goal>generate</goal>
              </goals>
          </execution>
      </executions>
  • Andreas Ebbert-Karroum

    27. September 2016 von Andreas Ebbert-Karroum

    „Leider gibt es keine frei verfügbaren & vergleichbaren Webservices“

    Ich würde z.B. behaupten das die WSDL und das XML Schema der Order Management API (und anderer API aus der Telekommunikation) hinreichend komplex sind 🙂

    https://jcp.org/en/jsr/detail?id=264

    • Jonas Hecht

      28. September 2016 von Jonas Hecht

      Hi Andreas, danke für den Hinweis. Der Service sieht auf den ersten Blick wirklich mächtig aus! Den muss ich eigentlich mal direkt mit den Frameworks testen, oder hast Du das schon gemacht 😉

      Hey, den Namen unter dem JSR kenne ich doch 😀

  • Hi Jonas! Vielen Dank für das Tutorial. Leider hatte ich bereits im ersten Schritt ein Problem:

    Im ersten Schritt werden die folgenden beiden Importe in der SimpleBootCxfConfiguration.java bei mir nicht gefunden:

    import de.codecentric.namespace.weatherservice.Weather;
    import de.codecentric.namespace.weatherservice.WeatherService;

    Was mache ich falsch bzw. wie sollte das funktionieren?

    Vielen Dank

    • Ooops, es handelt sich um die SimpleBootCxfConfiguration aus Step 10, nicht Step 1…

      • Jonas Hecht

        3. November 2016 von Jonas Hecht

        Vielen Dank 🙂 Immer wieder schön zu hören, wenn es hilft. Ich hoffe, es funktioniert jetzt?!?

  • Mit welcher Version von CXF sollte das Projekt gebaut werden können? Mit Version 3.1.6 bekomme ich bei Step1:

    java.lang.ClassNotFoundException: org.apache.cxf.bus.spring.SpringBus

  • OK, die java.lang.ClassNotFoundException: org.apache.cxf.bus.spring.SpringBus Geschichte trat auf weil gar keine Maven Dependency auf jaxrs vorhanden war…

    • Jonas Hecht

      3. November 2016 von Jonas Hecht

      Also auf JAX-RS zielt die Artikel-Serie nicht ab 😉 Sondern auf JAX-WS, also SOAP… Deshalb werden in der pom im tutorial step1 auch cxf-rt-frontend-jaxws und cxf-rt-transports-http als dependency eingebunden, die über transitive Abhängigkeiten dann cxf-core nachladen, dass die entsprechende Klasse org.apache.cxf.bus.spring.SpringBus enthält:

      <!-- Apache CXF -->
      <dependency>
      	<groupId>org.apache.cxf</groupId>
      	<artifactId>cxf-rt-frontend-jaxws</artifactId>
      	<version>${cxf.version}</version>
      </dependency>
      <dependency>
      	<groupId>org.apache.cxf</groupId>
      	<artifactId>cxf-rt-transports-http</artifactId>
      	<version>${cxf.version}</version>
      </dependency>

      Dass dies zufällig auch mit dem Hinzufügen von cxf-rt-frontend-jaxrs funktioniert, liegt an der ebenfalls transitiven Abhängigkeit dessen von cxf-core. Aber die pom in step1 enthält wirklich alles, was man benötigt, um die Schritte im Tutorial nachzuvollziehen. Ich fürchte, es handelt sicher eher um lokale Maven Probleme…

  • Ja, es ist 1:1 das Projekt ausm GitHub… Nachdem

    org.apache.cxf
    cxf-bundle-jaxrs
    2.7.5

    in die pom.xml von Step 1 hinzugefügt wurde, kann zumindest Schritt 1 jetzt gebaut werden bei mir…

    • Auch in Schritt zwei kann ich erst bauen, nachdem die Dependency auf jaxrs in die zugehörige pom eingefügt wurde. Auffällig ist, dass bei beiden Steps Compiler-Warnungen auftreten:

      [WARNING] error reading …\.m2\repository\org\apache\cxf\cxf-core\3.1.6\cxf-core-3.1.6.jar; invalid CEN header (bad signature)
      [WARNING] error reading …\.m2\repository\org\apache\cxf\cxf-rt-databinding-jaxb\3.1.6\cxf-rt-databinding-jaxb-3.1.6.jar; invalid LOC header (bad signature)
      [WARNING] error reading …\.m2\repository\com\sun\xml\bind\jaxb-core\2.2.11\jaxb-core-2.2.11.jar; invalid LOC header (bad signature)
      [WARNING] error reading …\.m2\repository\org\apache\cxf\cxf-rt-ws-policy\3.1.6\cxf-rt-ws-policy-3.1.6.jar; invalid LOC header (bad signature)
      [WARNING] error reading …\.m2\repository\org\apache\neethi\neethi\3.0.3\neethi-3.0.3.jar; invalid LOC header (bad signature)
      [WARNING] error reading …\.m2\repository\wsdl4j\wsdl4j\1.6.3\wsdl4j-1.6.3.jar; invalid LOC header (bad signature)

      • Jonas Hecht

        3. November 2016 von Jonas Hecht

        Das hört sich nach einer korrupten lokalen Maven-Installation an – wie auch eine kurze google-Recherche vermuten lässt. Wie gesagt enthalten die Projekte alles in ihren poms, was man benötigt um sauber zu bauen…

        • Vielen Dank! Jetzt funktioniert bereits einiges mehr als vorher 😉 Z.B. lässt sich jetzt Step 10 erfolgreich bauen, jedoch treten dort am cxf interceptor unerwartete Elemente auf, wie z.B.

          org.apache.cxf.interceptor.Fault: Unexpected wrapper element notRelevantHere found. Expected {http://www.codecentric.de/namespace/weatherservice/general}GetCityForecastByZIP.

          oder

          org.apache.cxf.binding.soap.SoapFault: Error reading XMLStreamReader: Unexpected character ’s‘ (code 115) in prolog; expected ‚<'
          at [row,col {unknown-source}]: [2,1]

          • OK, da wird vorher schon die Connection refused, weil der SOAP Service noch nicht läuft… Den muss ich natürlich erstmal starten…

            Super Tutorial, vielen lieben Dank dafür (und fürs Antworten)!

  • OK, ich kann die oben beschriebenen Warnings in „Schritt 1“ allesamt los werden, indem ich die apache cfx dependencies (inklusive der neu hinzugefügten Dependency auf jaxrs) vor die springframework.boot dependencies schiebe (in der pom zu „Step1“).

    An den Spring Boot Dependencies befindet sich keine explizite Versionsnummer? Mit welcher Version von Spring Boot verläuft der build denn erfolgreich?

    • Dass es überhaupt bei mir funktioniert, ist bei mir auch nur mit cxf Version 3.1.6 (und nicht 3.1.7 oder 3.1.8) gegeben…

      • Bei Step 2 genau das gleiche… Bei Step 3 führt dasselbe Vorgehen jedoch nicht zum build ohne Error und Warning, stattdessen fliegt eine Exception mit folgendem root cause:

        java.lang.NoSuchMethodError: org.apache.cxf.common.util.ClassHelper.getRealClass(Lorg/apache/cxf/Bus;Ljava/lang/Object;)Ljava/lang/Class;

        • Wobei es in Step 1 mit jaxrs 2.7.18 ohne Warnings funktioniert, während in Step 2 die Warnings bei der Verwendung von jaxrs 2.7.5 verschwinden – bei jaxrs 2.7.18 jedoch weiter vorhanden sind…

          • Also ich habe nochmal versucht, einfach nur den Repo-Teil von Step 1 isoliert zu verwenden (pom.xml von Step 1 genauso wie sie auf GitHub liegt) und mit Maven in der Kommandozeile zu bauen (mvn clean package). Dann tritt bereits das oben genannte Fehlerszenario auf: NoClassDefFoundError beim SpringBus, mit der Folge „Failed to introspect annotated methods“ in der SimpleBootCxfApplication nebst der oben genannten bad signature Warnings.

      • Jonas Hecht

        3. November 2016 von Jonas Hecht

        Welche Fehler sind das im Detail mit der 3.1.7 und der 3.1.8? Mir ist da bisher noch nichts aufgefallen bzw. in einem Kurztest laufen die Beispiele mit 3.1.6, 3.1.7 oder 3.1.8… Das wäre auch seltsam wenn sich hier in den Minor-Versionen z.B. CXF API-Umbauten eingeschlichen hätten.

    • Jonas Hecht

      3. November 2016 von Jonas Hecht

      Es sollte eigentlich egal sein, an welcher Stelle in der pom im Abschnitt dependencies die jeweilige Abhängigkeit auftaucht – die Reihenfolge ist hier nicht entscheidend.

      Die Versionsnummer von Spring Boot wird über den parent definiert, in dem Fall noch die 1.3.3.RELEASE („noch“ ist hier relativ zu sehen – die Steps sollten auch mit der aktuellen 1.4.1.RELEASE wunderbar laufen):

      <parent>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-starter-parent</artifactId>
      	<version>1.3.3.RELEASE</version>
      	<relativePath/> <!-- lookup parent from repository -->
      </parent>
  • Wenn ich versuche, Step 10 ausm Tutorial mit Maven clean package zu bauen wird scheinbar die neethi dependency nicht gefunden. Wenn ich sie manuell hinzufüge, bekomme ich einen Error mit der root cause „java.lang.IncompatibleClassChangeError: Implementing class“ am ClassLoader.defineClass

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.