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 cxf–rt–frontend–jaxws und cxf–rt-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:
1@SpringBootApplication
2public class SimpleBootCxfApplication {
3
4 public static void main(String[] args) {
5 SpringApplication.run(SimpleBootCxfApplication.class, args);
6 }
7
8 @Bean
9 public ServletRegistrationBean dispatcherServlet() {
10 return new ServletRegistrationBean(new CXFServlet(), "/soap-api/*");
11 }
12
13 @Bean(name=Bus.DEFAULT_BUS_ID)
14 public SpringBus springBus() {
15 return new SpringBus();
16 }
17}
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:
1<wsdl:service name="Weather"> 2 <wsdl:port name="WeatherService" binding="weather:WeatherService"> 3 <soap:address location="http://localhost:8095/soap-api/WeatherSoapService_1.0"/> 4 </wsdl:port> 5</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:
1<wsdl:operation name=“GetWeatherInformation“>…</wsdl:operation> 2<wsdl:operation name=“GetCityForecastByZIP“>…</wsdl:operation> 3<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 :
1<plugin> 2 <groupId>org.codehaus.mojo</groupId> 3 <artifactId>jaxws-maven-plugin</artifactId> 4 <version>2.4.1</version> 5 <configuration>...</configuration> 6</plugin>
Hier ist vor allem das -Tag interessant:
1<configuration> 2 <wsdlUrls> 3 <wsdlUrl>src/main/resources/service-api-definition/Weather1.0.wsdl</wsdlUrl> 4 </wsdlUrls> 5 <sourceDestDir>target/generated-sources/wsdlimport/Weather1.0</sourceDestDir> 6 <vmArgs> 7 <vmArg>-Djavax.xml.accessExternalSchema=all</vmArg> 8 </vmArgs> 9</configuration>
Das definiert den Ablageort unserer WSDL, , 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 ü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
1mvn 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):
1@Bean
2public WeatherService weatherService() {
3 return new WeatherServiceEndpoint();
4}
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:
1@Bean 2public Endpoint endpoint() { 3 EndpointImpl endpoint = new EndpointImpl(springBus(), weatherService()); 4 endpoint.publish("/WeatherSoapService_1.0"); 5 endpoint.setWsdlLocation("Weather1.0.wsdl"); 6 return endpoint; 7}
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.
Weitere Beiträge
von Jonas Hecht
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Jonas Hecht
Senior Solution Architect
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.