Spring Boot & Apache CXF – Von 0 auf SOAP mit dem cxf-spring-boot-starter

Keine Kommentare

Sie haben bisher keinen einzigen Artikel dieser Blogserie gelesen? Gut so! Denn das Beste kommt erst jetzt. Wir verheiraten Spring Boot und Apache CXF in einem eigenen spring-boot-starter. So haben wir unsere SOAP-Endpoints noch schneller am Start und nutzen trotzdem alle bisher vorgestellten Features voll aus!

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

Wir haben in den vorangegangenen Blogartikeln eine Menge über die Arbeit mit Spring Boot und Apache CXF gelernt. Es bleibt eigentlich nur ein Problem: Wir fangen bei jedem unserer SOAP Endpoints von Neuem an, alle Schritte jedes einzelnen Blogartikels aufs Neue nachzuvollziehen. Bei aller Aktualität der Technologien geht uns das aber irgendwann gegen den Strich: Wir machen Dinge doppelt und dreifach. Doch glücklicherweise haben sich die Spring-Boot-Entwickler auch für diesen Fall etwas einfallen lassen: Es ist möglich, sich eigene Spring Boot Starter zu bauen, die alles Nötige für unseren speziellen Use Case mitbringen.

Der offizielle Apache CXF spring-boot-starter reicht uns nicht…

Das haben mittlerweile auch die Apache-CXF-Entwickler erkannt und bieten einen spring boot starter an (danke nochmal für den Hinweis Stéphane Nicoll 🙂 ). Schaut man sich diesen spring-boot-starter genauer an, so wird schnell klar, dass er sich natürlich ausschließlich auf Apache CXF konzentriert. Er nimmt uns dabei insgesamt die folgende Arbeit ab: Er initialisiert CXF. Das ist alles. Wie im ersten Artikel dieser Serie beschrieben, ist das letztlich folgender Code, den wir uns sparen können, wenn wir den Starter einsetzen:

@Bean
public ServletRegistrationBean dispatcherServlet() {
    return new ServletRegistrationBean(new CXFServlet(), "/soap-api/*");
}
@Bean(name=Bus.DEFAULT_BUS_ID)
public SpringBus springBus() {      
    return new SpringBus();
}

Im Kontext der CXF-Entwickler ist diese Fokussierung auch völlig ok! Aber es hilft uns in unseren Enterprise-Umfeldern wenig. Denn mit allen anderen Punkten stehen wir wieder allein da.

Also ein eigener spring-boot-starter?!!

Genau deshalb haben wir uns entschieden, einen eigenen spring-boot-starter für Apache CXF sowie notwendige Technologien „drumherum“ zu entwickeln und auf GitHub verfügbar zu machen…

Aber halt! Dürfen spring-boot-starter nicht nur durch die Spring-Entwickler bereitgestellt werden? Sind sie nicht deren exklusiver Weg, Funktionalität mit Spring Boot verfügbar zu machen? Zum Glück nicht! Neben der riesigen Anzahl von Startern, die Spring Boot von Haus aus mitbringt, kann jeder Entwickler seinen eigenen Starter bauen. Einige davon haben es sogar in die Community-Auswahl der Spring-Boot-Entwickler geschafft.

Wie man einen eigenen spring-boot-starter baut, beschreiben die Spring-Boot-Entwickler auf docs.spring.io und einer meiner Kollegen hat alle notwendigen Schritte zusammengetragen. Der Aufbau eines eigenen spring-boot-starters birgt die Möglichkeit, richtig in die Tiefen von Spring Boot abzutauchen und ein tieferes Verständnis für das Framework zu entwickeln. Gleichzeitig werden technische Frameworks nicht mehrfach entwickelt und können in allen Projekten mit den gleichen Anforderungen benutzt werden. Damit widerspricht ein spring-boot-starter auch nicht den Gedanken hinter der Microservices-Bewegung, sondern fördert diese sogar: Der Austausch von technischen Bibliotheken, am besten sogar über github.com, wird ausdrücklich empfohlen.

cxf-spring-boot-starter

Doch nun zum Eigentlichen: Ähnlich dem schon länger verfügbaren spring-boot-starter-batch-web haben wir den cxf-spring-boot-starter auf GitHub verfügbar gemacht. Er nimmt uns viele Dinge ab, die wir ohne ihn zu Fuß machen müssten. Hier ein paar davon:

  • Hochziehen aller notwendigen Apache-CXF-Komponenten (natürlich mit 100% Java-Konfiguration 😉 )
  • Extremes Vereinfachen der Logging-Konfiguration
  • Extraktion der ein- und ausgehenden SOAP-XML-Nachrichten für einen Elastic-Stack – inkl. eigener Custom Fields & Korrelation aller Log-Events eines Requests
  • Anbieten eines Builders für eigene Custom SOAP Faults, die bei XML-Schemavalidierung auf Basis eigener XSDs zurückgeliefert werden sollen
  • Umfangreiche Unterstützung bei der Erstellung von Unit-, Integrations- und Systemstests sowie beim Testen von invaliden XML-Anfragen

Außerdem wird die im Detail umständliche Konfiguration des jaxws-maven-plugin vollständig gekapselt und die Generierung aller Java-Klassen aus der WSDL wird zum Kinderspiel. Hierfür kommt eine zweite Komponente zum Einsatz: das cxf-spring-boot-starter-maven-plugin. Es sucht neben der Generierung selbständig im resource-Folder nach der WSDL und sorgt dafür, dass die Klassen im Classpath landen. Zusätzlich konfiguriert es das jaxws-maven-plugin so, dass keine absoluten Pfade in die @WebServiceClient-annotierten Klassen generiert werden. So können diese uns keinen Ärger mehr machen, wenn unser CI-Server unseren Code auscheckt und die Pfade nicht auflösen kann.

Hört sich gut an! Wie fange ich an?

Damit die Vorteile des cxf-spring-boot-starter greifbar werden, schlage ich folgende Vorgehensweise vor: Wir ziehen ein Beispielprojekt mit allen Features komplett von 0 hoch und gehen dabei jedes Thema aus jedem bisherigen Artikel durch – natürlich in deutlich schnellerer Form. 😉 Wie immer gibt es in unserem Tutorial Repository ein Beispielprojekt, in dem sich alle Schritte nachvollziehen lassen.

Also los! Für die schnelle Projektanlage nutzen wir wie im ersten Artikel den Spring Initializr. Hier lassen wir unser Projekt per Generate Project erzeugen. Die generierte POM ist vom spring-boot-starter-parent abgeleitet und hängt von spring-boot-starter-test ab. Am Ende folgt noch das Build-Plugin spring-boot-maven-plugin.

Um unseren cxf-spring-boot-starter einzubinden, müssen wir nur die folgende Dependency in die POM einbinden (aktuell ist 1.0.7.RELEASE):

<dependencies>
    <dependency>
        <groupId>de.codecentric</groupId>
        <artifactId>cxf-spring-boot-starter</artifactId>
        <version>1.0.7.RELEASE</version>
    </dependency>
</dependencies>

Danach binden wir noch das Build-Plugin cxf-spring-boot-starter-maven-plugin in die Build Section unserer POM ein (aktuell ist auch hier die 1.0.7.RELEASE):

<build>
    <plugins>
        <plugin>
            <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>
        </plugin>
    </plugins>
</build>

Durch einen Bug in logback 1.1.7, der sich auf die Extraction der ein- und ausgehenden SOAP-XML-Nachrichten für einen Elastic-Stack auswirkt (da wir hierfür den logstash-logback-encoder einsetzen), müssen wir aktuell noch ein Downgrade von logback auf die 1.1.6 machen – das gilt natürlich nur, solange logback 1.1.8 noch nicht released ist und dieses Spring Boot nicht mitliefert:

 <properties>
    <logback.version>1.1.6</logback.version>
</properties>

Durchstarten

Nun legen wir unsere WSDL inkl. aller importierten XSDs unter src/main/resources (oder einem beliebigen Ordner darin) ab. In unserem Beispielprojekt liegen die Files wie gewohnt unter src/main/resources/service-api-definition. Wir müssen nicht einmal konfigurieren, wo wir unsere WSDL abgelegt haben 🙂 Wir können einfach die Generierung aller notwendigen Java-Klassen per mvn generate-sources veranlassen oder das Projekt einfach in unsere Lieblings-IDE importieren, die das im Hintergrund für uns übernimmt.

Für den letzten Schritt brauchen wir außerdem wieder eine das SEI implementierende Klasse WeatherServiceEndpoint, die wir ebenfalls wie gewohnt erstellen – später setzen wir hier wieder mit unserer fachlichen Implementierung auf. Danach legen wir eine @Configuration-annotierte Klasse an, in der wir unseren Endpoint hochziehen:

@Configuration
public class WebServiceConfiguration {
 
    @Autowired private SpringBus springBus;
 
    @Bean
    public WeatherService weatherService() {
        return new WeatherServiceEndpoint();
    }
 
    @Bean
    public Endpoint endpoint() {
        EndpointImpl endpoint = new EndpointImpl(springBus, weatherService());
        endpoint.setServiceName(weatherClient().getServiceName());
        endpoint.setWsdlLocation(weatherClient().getWSDLDocumentLocation().toString());
        endpoint.publish("/WeatherSoapService_1.0");
        return endpoint;
    }
 
    @Bean
    public Weather weatherClient() {
        return new Weather();
    }
}

Die Besonderheit des cxf-spring-boot-starters ist hier, dass wir Apache CXF gar nicht initialisieren müssen. Den für die Endpoint-Initialisierung benötigten org.apache.cxf.bus.spring.SpringBus können wir uns einfach autowiren lassen.

Und das war auch schon alles. 🙂 Wir können unsere SimpleBootCxfApplication per „RUN“ in unserer IDE starten oder per mvn spring-boot:run auf der Kommandozeile zum Leben erwecken. Ein kurzer Blick auf http://localhost:8080/soap-api bestätigt uns, dass Apache CXF hochgefahren ist und unser WebService registriert ist. Aufrufe per SoapUI funktionieren wunderbar.

SOAP Webservices testen

Nachdem wir den ersten Artikel damit schon abgedeckt haben, widmen wir uns dem Testen unserer SOAP-Services. Dabei lassen wir die Unit- & Integrationstests hier links liegen, denn die Auswirkungen auf diese beiden Typen durch den cxf-spring-boot-starter halten sich in Grenzen. Interessant wird es bei den Single System Integration Tests, denn hier müssen wir einen JAX-WS Client zur Verfügung haben, der unseren lokalen Server aufrufen kann – und dafür natürlich dessen URL benötigt. Die URL, auf der unsere WebServices zur Apache CXF publiziert werden, wird nun aber standardmäßig durch den cxf-spring-boot-starter festgelegt. Wir müssen also wissen, wie wir da rankommen.

Dazu nehmen wir uns den WeatherServiceXmlFileSystemTest aus dem zweiten Blogartikel. Er wird über die SimpleBootCxfSystemTestConfiguration konfiguriert – und hier setzen wir an. Wir autowiren uns die de.codecentric.cxf.configuration.CxfAutoConfiguration und greifen einfach per cxfAutoConfiguration.getBaseUrl() die benötigte URL ab. Dann sieht die Konfiguration unseres JAX-WS-Clients so aus:

@Autowired private CxfAutoConfiguration cxfAutoConfiguration;
 
@Bean
public WeatherService weatherServiceSystemTestClient() {
    JaxWsProxyFactoryBean jaxWsProxyFactory = new JaxWsProxyFactoryBean();
    jaxWsProxyFactory.setServiceClass(WeatherService.class);
    jaxWsProxyFactory.setAddress("http://localhost:8090" + cxfAutoConfiguration.getBaseUrl() + SimpleBootCxfConfiguration.SERVICE_URL);
    return (WeatherService) jaxWsProxyFactory.create();
}

Um die Basis-URL unserer CXF-Endpoints anzupassen, reicht es übrigens, die application.properties um eine property soap.service.base.url zu erweitern und ihr einen entsprechenden Wert zu vergeben. Genauso einfach lässt sich übrigens der Titel der generierten Webseite anpassen, auf der CXF die verfügbaren Services zeigt – das geht per cxf.servicelist.title. Beides lässt sich auch in der application.properties unseres Beispielprojekts nachvollziehen.

Unser Testfall WeatherServiceXmlFileSystemTest ändert sich daneben nur leicht. Der cxf-spring-boot-starter bringt die praktische Utility-Klasse de.codecentric.cxf.common.XmlUtils, die uns die Arbeit mit dem Marshalling von XML Files in JAX-B Objekte innerhalb unserer Tests abnimmt:

@RunWith(SpringRunner.class)
@SpringBootTest(
        classes=SimpleBootCxfSystemTestApplication.class,
        webEnvironment= SpringBootTest.WebEnvironment.DEFINED_PORT,
        properties = {"server.port=8090"}
)
public class WeatherServiceXmlFileSystemTest {
 
    @Autowired private WeatherService weatherServiceSystemTestClient;
 
    @Value(value="classpath:requests/GetCityForecastByZIPTest.xml")
    private Resource getCityForecastByZIPTestXml;
 
    @Test
    public void getCityForecastByZIP() throws WeatherException, IOException, BootStarterCxfException {
        // Given
        GetCityForecastByZIP getCityForecastByZIP = XmlUtils.readSoapMessageFromStreamAndUnmarshallBody2Object(getCityForecastByZIPTestXml.getInputStream(), GetCityForecastByZIP.class);
 
        // When
        ForecastReturn forecastReturn = weatherServiceSystemTestClient.getCityForecastByZIP(getCityForecastByZIP.getForecastRequest());
 
        // Then
        assertNotNull(forecastReturn);
        assertEquals(true, forecastReturn.isSuccess());
        ...
    }
}

Okay, es gibt natürlich eine weitere Änderung. Die bezieht sich allerdings auf die neuen Testfeatures von Spring Boot 1.4.x, die in der Klasse org.springframework.boot.test.context.SpringBootTest zusammengefasst wurden. Diese ersetzt alle vorher notwendigen Testannotations – wie @WebIntegrationTest, @SpringApplicationConfiguration, @ContextConfiguration usw. (siehe dazu auch den spring.io blogpost). Außerdem verkürzt der SpringRunner die lästige Tipperei mit dem SpringJUnit4ClassRunner.

Und das war es dann auch schon zum Thema „Testen von SOAP-WebServices“.

XML-Validierung und Custom SOAP Faults

Das Fazit des dritten Blogposts lässt nicht unbedingt ein gutes Haar an den vielen Schritten, die notwendig sind, um auf XML-Validierungsfehler in einer XML-Schema-konformen Weise zu reagieren. Da diese Anforderung gerade im Enterprise-Bereich aber häufiger vorkommt (siehe BiPro), vereinfacht uns der cxf-spring-boot-starter das Leben hier erheblich.

Wir können im Grunde alle Schritte aus dem dritten Artikel getrost wieder vergessen und implementieren stattdessen das Interface de.codecentric.cxf.xmlvalidation.CustomFaultBuilder. Das ist auch schon alles.

Im Detail heißt das: Wir überschreiben zwei Methoden – createCustomFaultMessage(FaultType faultType) bietet uns dabei die Möglichkeit, die Fault-Message in unserem Soap Fault zu beeinflussen. Durch den übergebenen de.codecentric.cxf.common.FaultType wissen wir auch, ob es sich um einen XML-Schema-Validierungsfehler oder grundsätzlich inkorrektes XML handelt. Durch das Design des cxf-spring-boot-starter können wir hier sogar auf Fehler reagieren, die gar nicht durch fehlerhafte XMLs bedingt sind und dadurch wirklich immer Schema-konforme Fehlerantworten liefern.
Mithilfe der Methode createCustomFaultDetail(String originalFaultMessage, FaultType faultType) bauen wir uns unsere XML-Schema-konforme Fehlernachricht zusammen. Beides sehen wir in der Klasse WeatherFaultBuilder unseres Beispielprojekts:

@Component
public class WeatherFaultBuilder implements CustomFaultBuilder {
 
	private de.codecentric.namespace.weatherservice.exception.ObjectFactory objectFactoryDatatypes = new de.codecentric.namespace.weatherservice.exception.ObjectFactory();
 
	@Override
	public String createCustomFaultMessage(FaultType faultType) {
		if(FaultType.SCHEME_VALIDATION_ERROR.equals(faultType))
			return CustomIds.NON_XML_COMPLIANT.getMessage();
		else if(FaultType.SYNTACTICALLY_INCORRECT_XML_ERROR.equals(faultType))
			return CustomIds.COMPLETE_USELESS_XML.getMessage();
		else
			return CustomIds.SOMETHING_ELSE_WENT_TERRIBLY_WRONG.getMessage();
	}
 
	@Override
	public WeatherException createCustomFaultDetail(String originalFaultMessage, FaultType faultType) {
		// Build SOAP-Fault detail <datatypes:WeatherException>
		WeatherException weatherException = objectFactoryDatatypes.createWeatherException();
		weatherException.setBigBusinessErrorCausingMoneyLoss(true);
		setIdBasedUponFaultContent(faultType, weatherException);
		weatherException.setExceptionDetails(originalFaultMessage);
		weatherException.setUuid("ExtremeRandomNumber");
		return weatherException;
	}
...
}

In der Methode createCustomFaultDetail(String originalFaultMessage, FaultType faultType) sollten wir darauf achten, die korrekte Exception zurückzuliefern. Hier kommt es bei manchen Spezifikationen zu Doppelungen, wie auch bei den BiPro-WebServices. Alles, was nun noch zu tun bleibt, ist die @Component-annotierte Klasse in unserer Spring-Konfiguration als Bean zu definieren. Und: das war es dann auch schon (wieder).

Natürlich trauen wir hier dem Autor nicht – und wollen einen Testfall sehen. Das kann ja wohl nicht so einfach gewesen sein. 🙂 Und auch für diesen Fall bringt der cxf-spring-boot-starter etwas mit: den de.codecentric.cxf.soaprawclient.SoapRawClient. Mit seiner Hilfe können wir nicht XML-Schema-konformes XML gegen unseren Endpoint schicken – damit wir überhaupt die XML-Validierungsfehler provozieren können, auf die wir ja Schema-konform antworten wollen.

Dazu nutzen wir den WeatherServiceXmlErrorSystemTest aus dem dritten Artikel – einen Single System Integration Test, der die gewünschten Validierungsfehler provoziert und prüft, ob unser Endpoint ebenfalls Schema-konform antwortet. Wir erweitern ihn nur an wenigen Stellen. Neben den obligatorischen Spring-Boot-1.4.x-Änderungen tauschen wir in unserem Testfall WeatherServiceXmlErrorSystemTest unsere selbst implementierten Konstanten gegen die im cxf-spring-boot-starter mitgelieferten in der Klasse de.codecentric.cxf.common.FaultType aus und prüfen auf den in unserem implementierten CustomFaultBuilder festgelegten Fehlertext:

assertEquals(WeatherFaultBuilder.CUSTOM_ERROR_MSG, soapRawResponse.getFaultstringValue());

Die Konfiguration unterscheidet sich nur durch die CxfAutoConfiguration, die wir für die URL benötigen:

@Autowired private CxfAutoConfiguration cxfAutoConfiguration;
 
@Bean
public SoapRawClient soapRawClient() throws BootStarterCxfException {
    return new SoapRawClient(buildUrl(), WeatherService.class);
}
 
private String buildUrl() {
    // return something like http://localhost:8084/soap-api/WeatherSoapService
    return "http://localhost:8087"
            + cxfAutoConfiguration.getBaseUrl()
            + SimpleBootCxfConfiguration.SERVICE_URL;
}

Führen wir unseren WeatherServiceXmlErrorSystemTest nun aus, gehen alle Lampen auf grün. Außerdem können wir per SoapUI oder Boomerang SOAP & REST Client die XML-Schema-konformen Fehlernachrichten bestaunen. In jedem Fall sind wir schnellstmöglich mit unseren eigenen Custom SOAP Faults am Start.

Logging & Monitoring mit Logback, Elasticsearch, Logstash & Kibana

Und schon sind wir beim letzten Artikel der Serie angelangt. Gerade das Thema Logging kann durchaus in Arbeit ausarten – und auch hier untersützt uns der cxf-spring-boot-starter massiv. Mit seiner Hilfe läuft alles Logging standardmäßig über slf4j und logback. Zum Loggen der SOAP-Nachrichten müssen wir nur die Property soap.messages.logging in unseren application.properties auf true setzen. Schon sehen wir die ein- und ausgehenden Nachrichten in unserem Logfile oder in der Console.

Beim Loggen in einen Elastic-Stack nimmt uns der cxf-spring-boot-starter ebenfalls die meiste Arbeit ab. Die einzigen Voraussetzungen sind die Property soap.messages.extract=true in application.properties und das Vorhandensein einer logback-spring.xml, die genauso wie im letzten Artikel beschrieben aussehen kann:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml"/>
    <logger name="org.springframework" level="WARN"/>
    <logger name="de.jonashackt.tutorial" level="DEBUG"/>
    <logger name="org.apache.cxf" level="INFO"/>
 
    <!-- Logstash-Configuration -->
    <appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <destination>192.168.99.100:5000</destination>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <customFields>{"service_name":"WeatherService 1.0"}</customFields>
            <fieldNames>
                <message>log_msg</message>
            </fieldNames>
        </encoder>
        <keepAliveDuration>5 minutes</keepAliveDuration>
    </appender>
 
  <root level="INFO">
        <appender-ref ref="logstash" />
  </root>
</configuration>

Hat man einen Elastic-Stack am Laufen (dafür gibt es ebenfalls Tipps im letzten Artikel), war es das auch schon wieder. Zusätzlich ist sogar noch mehr als nur reines Logging aktiv. Z.B. landen SOAP-Messages automatisch in den eigenen Custom Fields soap-message-inbound und soap-message-outbound, die sich viel besser auswerten lassen. Diese sind über eine Enumeration in de.codecentric.cxf.logging.ElasticsearchField definiert.

Außerdem werden alle zu einem SOAP-Request gehörenden Log-Events automatisch korreliert, und der cxf-spring-boot-starter versucht den Methodennamen des SOAP-Services zu ermitteln (was momentan allerdings auf die WSDL Specification 1.1 beschränkt ist). Diese landet automatisch im Custom Field soap-method-name. Damit hat man aus dem Stegreif praktisch alles an Bord, was man fürs Logging von SOAP-Services braucht.

Von 0 auf SOAP… Fertig!

Nun haben wir alle Voraussetzungen, noch schneller SOAP Endpoints zu entwickeln – und trotzdem vom Start weg alle notwendigen Features verfügbar zu haben, die man im Enterprise-Umfeld gebrauchen kann. Alles in vier Blogartikeln Besprochene haben wir an Bord. Liest man alle vorangegangenen Artikel, wird klar, was das heißt.

Und falls doch mal eine Funktion fehlen sollte – kein Problem: Der cxf-spring-boot-starter freut sich immer über Mitarbeit. Hier zeigt sich auch wieder die Power von Open Source: Man profitiert im eigenen Projekt und kann gleichzeitig über eigene Contributions wieder etwas zurückgeben.

Wir wünschen viel Spaß mit dem cxf-spring-boot-starter und freuen uns über Feedback und Pull Requests! 🙂

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

Kommentieren

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