Enterprise-ready production-ready SOAP-Webservices powered by Spring Boot & Apache CXF
!! This document applies to the next version under development.
Features include:
- Generating all necessary Java-Classes using JAX-B from your WSDL/XSDs (using the complementing Maven plugin cxf-spring-boot-starter-maven-plugin
- Booting up Apache CXF within Spring Context with 100% pure Java-Configuration
- Complete automation of Endpoint initialization - no need to configure Apache CXF Endpoints, that´s all done for you automatically based upon the WSDL and the generated Java-Classes (bringing up a nice Spring Boot 1.4.x Failure Message if you missed something :) )
- Customize SOAP service URL and the title of the CXF generated Service site
- Configures CXF to use slf4j and serve Logging-Interceptors, to log only the SOAP-Messages onto console
- Extract the SoapMessages for processing in ELK-Stack, like docker-elk
- Tailor your own custom SOAP faults, that comply with the exceptions defined inside your XML schema
- SOAP Testing-Framework: With XmlUtils to easy your work with JAX-B class handling & a SOAP Raw Client to Test malformed XML against your Endpoints
Documentation
There´s also an blog post describing this project: Spring Boot & Apache CXF – SOAP on steroids fueled by cxf-spring-boot-starter
Initial Setup
- Create a Spring Boot maven project. Use the spring-boot-starter-parent as a parent and the spring-boot-maven-plugin as a build plugin (you could speed that up, if you use the Spring Initializr).
- Then append cxf-spring-boot-starter as dependency and the cxf-spring-boot-starter-maven-plugin as build-plugin (see the example cxf-boot-simple):
<dependencies>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>cxf-spring-boot-starter</artifactId>
<version>1.1.4.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>de.codecentric</groupId>
<artifactId>cxf-spring-boot-starter-maven-plugin</artifactId>
<version>1.1.5.RELEASE</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
- place your .wsdl-File (and all the imported XSDs) into a folder somewhere under src/main/resources (see cxf-spring-boot-starter-maven-plugin for details)
- run mvn generate-sources to generate all necessary Java-Classes from your WSDL/XSD
- Implement the javax.jws.WebService annotated Interface (your generated Service Endpoint Interface (SEI) ) - it is the starting point for your development and is needed to autoconfigure your Endpoints. See the WeatherServiceEndpoint class inside the cxf-boot-simple project.
- That´s it
Additional Configuration Options
Customize URL, where your SOAP services are published
- create a application.properties and set the BaseURL of your Webservices via soap.service.base.url=/yourUrlHere
Customize title of generated CXF-site
- place a cxf.servicelist.title=Your custom title here in application.properties
SOAP-Message-Logging
Activate SOAP-Message-Logging just via Property soap.messages.logging=true in application.properties (no more configuration on the Endpoint needed)
SOAP-Messages will be logged only and printed onto STDOUT/Console for fast analysis in development.
Extract the SoapMessages for processing in ELK-Stack
The cxf-spring-boot-starter brings some nice features, you can use with an ELK-Stack to monitor your SOAP-Service-Calls:
- Extract SOAP-Service-Method for Loganalysis (based on WSDL 1.1 spec, 1.2 not supported for now - because this is read from the HTTP-Header field SoapAction, which isn´ mandatory in 1.2 any more)
- Dead simple Calltime-Logging
- Correlate all Log-Messages (Selfmade + ApacheCXFs SOAP-Messages) within the Scope of one Service-Consumer`s Call in Kibana via logback´s MDC, placed in a Servlet-Filter
HowTo use
- Activate via Property soap.messages.extract=true in application.properties
- Add a logback-spring.xml file to src/main/resources (otherwise the feature will not be activated) and configure the logstash-logback-encoder (which is delivered with this spring-boot-starter), like:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<logger name="org.springframework" level="WARN"/>
<!-- more logging config here -->
<!-- Logstash-Configuration -->
<!-- For details see https://github.com/logstash/logstash-logback-encoder/tree/logstash-logback-encoder-4.5 -->
<appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<!-- You may want to configure a default instance, this could be done like
<destination>${LOGANALYSIS_HOST:-192.168.99.100}:5000</destination> as discribed here:
http://logback.qos.ch/manual/configuration.html#defaultValuesForVariables
Set the SystemProperty with
env LOGANALYSIS_HOST={{ loganalysis.host }}
e.g. in a service.upstart.conf.j2, when using Ansible and deploying to Ubuntu -->
<destination>192.168.99.100:5000</destination>
<!-- encoder is required -->
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeCallerData>true</includeCallerData>
<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>
- Now some fields will become available in your kibana dashboard (in other words in your elasticsearch index), e.g. soap-message-inbound contains the Inbound Message
- see all of them here ElasticsearchField.java
- Additionally Spring Cloud Sleuth will provide detailed tracing information of your services. Sleuth will populate the Logback MDC automatically with the tracing information. You can for example retrieve the Trace-Id of the current call via
MDC.get("X-B3-TraceId")
. - The default is to use the ELK stack for log analysis. With further configuration you can even extend the tracing infrastructure to use more tailored tracing tools like Zipkin.
Custom SOAP faults for XML Schema Validation Errors
The standard behavior of Apache CXF with XML validation errors (non schema compliant XML or incorrect XML itself) is to return a SOAP fault including the corresponding exception in CXF:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>soap:Server</faultcode>
<faultstring>wrong number of arguments while invoking public de.codecentric.namespace.weatherservice.general.ForecastRequest de.codecentric.cxf.WeatherServiceEndpoint.getCityForecastByZIP(de.codecentric.namespace.weatherservice.general.ForecastRequest) throws net.bipro.namespace.BiproException with params null.</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>
Many SOAP based standards demand a custom SOAP-Fault, that should be delivered in case of XML validation errors. To Implement that behavior, you have to:
- Implement the Interface CustomFaultBuilder as Spring @Component
- Override Method createCustomFaultMessage(FaultType faultContent) an give back appropriate Messages you want to see in faultstring: soap:FaultYOUR CUSTOM MESSAGE HERE
- Override Method createCustomFaultDetail(String originalFaultMessage, FaultType faultContent) and return the JAX-B generated Object, that represents your WebService´ Fault-Details (be really careful to take the right one!!, often the term 'Exception' is used twice... - e.g. with the BiPro-Services)
- Configure your Implementation as @Bean - only then, XML Schema Validation will be activated
Testing SOAP web services
Create a WebService Client
- If you instantiate a JaxWsProxyFactoryBean, you need to set an Adress containing your configured (or the standard) soap.service.base.url. To get the correct path, just autowire the CxfAutoConfiguration like:
@Autowired
private CxfAutoConfiguration cxfAutoConfiguration;
and obtain the base.url and the serviceUrlEnding (this one is derived from the wsdl:service name attribute of your WSDL) by calling
cxfAutoConfiguration.getBaseAndServiceEndingUrl()
Integrate real XML test files into your Unit-, Integration- or SingleSystemIntegrationTests
As described in this blogpost the best gut feeling one could get while writing SOAP Tests, is the usage of real XML test files. To easily marshall these into your Java classes with JAX-B, this starter brings a utility class de.codecentric.cxf.common.XmlUtils with lots of useful methods like readSoapMessageFromStreamAndUnmarshallBody2Object(java.io.InputStream fileStream, Class jaxbClass). Then you could do things inside your testcases like:
@Value(value="classpath:requests/GetCityForecastByZIPTest.xml")
private org.springframework.core.io.Resource GetCityForecastByZIPTestXml;
@Test
public void getCityForecastByZIP() throws WeatherException, BootStarterCxfException, IOException {
GetCityForecastByZIP getCityForecastByZIP = XmlUtils.readSoapMessageFromStreamAndUnmarshallBody2Object(GetCityForecastByZIPTestXml.getInputStream(), GetCityForecastByZIP.class);
...
}
SOAP Raw client
Enables automatic testing of malformed XML Requests (e.g. for Testing your Custom SOAP faults) with the de.codecentric.cxf.soaprawclient.SoapRawClient. To use it in your Testcases, initialize the SoapRawClient inside a @Configuration annotated Class like this:
@Bean
public SoapRawClient soapRawClient() throws BootStarterCxfException {
return new SoapRawClient(buildUrl(), YourServiceInterface.class);
}
public String buildUrl() {
// return something like http://localhost:8084/soap-api/WeatherSoapService
return "http://localhost:8084" + cxfAutoConfiguration.getBaseAndServiceEndingUrl();
}
@Autowired
private CxfAutoConfiguration cxfAutoConfiguration;
Running Client-only mode
If you´d like to run Apache CXF only to call other SOAP web services but don´t want to provide one for yourself, than booting up a complete server is a bit to much for you. Therefore you´re also able to deactivate the Complete automation of Endpoint initialization feature, which only makes sense if you have an Endpoint to fire up. You can deactivate it with the following propery in your application.propteries:
endpoint.autoinit=false
Concepts
Complete automation of Endpoint initialization
100% contract first approach
Taking into account a 100% contract first development approach there shouldn´t be a single reason, why one has to manually configure Endpoints in Apache CXF - because pretty much every piece of information that is necessary to configure them should be available through the WSDL. Since the start of this spring-boot-starter project, this was a thought that didn´t let me go.
To understand, how the complete automation of Endpoint initialization is implemented in the cxf-spring-boot-starter, let´s first have a look on how the initialization works without the help of the starter. To instantiate & publish a org.apache.cxf.jaxws.EndpointImpl
, we need the SEI implementing class and the generated WebServiceClient annotated class. In a non-automated way to use Apache CXF to fire up JAX-WS endpoints, this is done with code like this:
@Bean
public WeatherService weatherService() {
return new WeatherServiceEndpoint();
}
@Bean
public Endpoint endpoint() {
EndpointImpl endpoint = new EndpointImpl(springBus(), weatherService());
endpoint.setServiceName(weather().getServiceName());
endpoint.setWsdlLocation(weather().getWSDLDocumentLocation().toString());
endpoint.publish(serviceUrlEnding());
return endpoint;
}
@Bean
public Weather weather() {
// Needed for correct ServiceName & WSDLLocation to publish contract first incl. original WSDL
return new Weather();
}
The easier parts are the SpringBus, which we already have instantiated in our CxfAutoConfiguration, and the serviceUrlEnding, which is constructed from the configurable base url and the WSDL tag´s service name
content. To instantiate the EndpointImpl, set the service name and the WSDL location correctly, we need the SEI implementing class (which you have to write yourself, because it´s the starting point for your implementation) and the generated WebServiceClient annotated class.
Scanning...
Because a spring-boot-starter is a generic thing everybody can use just via including it in the pom, these two classes are not fixed - they are always generated or derived from generated classes. Therefore we have to search for them - according to some things we know. The search is done with the help of Spring´s ClassPathScanningCandidateComponentProvider (instead of using the really nice fast-classpath-scanner, which didn´t work well in this use case).
Either scanning framework you use, self written or library - any of them will be much faster, if you have the package names of the searched classes. In some scenarios -escpecially with the ClassPathScanningCandidateComponentProvider used here - you have to know the packages, otherwise scanning will fail (because it tries to double-scan the package org.springframework itself). So to search for the WebServiceClient annotated class and the SEI itself (which we need to scan for the SEI implementation, which is only characterized due to the fact of implementing the SEI), we need to somehow know their package beforehand.
Here cxf-spring-boot-starter-maven-plugin comes to our rescue. With the new 1.0.8´s feature Extract the targetNamespace from the WSDL, generate the SEI and WebServiceClient annotated classes´ package names from it & write it together with the project´s package name into a cxf-spring-boot-maven.properties the package names are extracted into a cxf-spring-boot-maven.properties file inside your project.buildpath while a
mvn generate-sources` is ran. The package name of the WebServiceClient annotated class and the SEI are derived from the WSDL:
To get this 100% right, we need to use the same mechanism as the jaxws-maven-plugin, which itself uses WSimportTool of the JAXWS-RI implementation, to obtain the package-Name from the WSDL file, where the classes are generated to. The WSDL´s targetNamespace is used to generate the package name. If you have targetNamespace="http://www.codecentric.de/namespace/weatherservice/" for example, your package will be de.codecentric.namespace.weatherservice. One can find the code used to generate the package name in the WSDLModeler at line 2312 (This algorithm is specified in the JAXB spec. So we rely onto it):
String wsdlUri = document.getDefinitions().getTargetNamespaceURI();
return XJC.getDefaultPackageName(wsdlUri);
The package name of the SEI implementing class is a bit more of a guesswork, because this class could literally reside everywhere. BUT: If you start a project to use a spring-boot-starter, the 99,9% case will be to start with a Maven pom - and even faster through the usage of the Spring initializr. It should be safe to rely on that and just guess the package name from your project´s pom. This will in 99,9% of all cases contain your SEI implementing class, which is you´re entry point to develop a SOAP web service with this starter and CXF.
Auto initialize the Endpoint!
Now having the package names of every needed class residing in the cxf-spring-boot-maven.properties file after a run of mvn generate-sources
, using Spring´s ClassPathScanningCandidateComponentProvider to scan for the WebServiceClient annotated class is easy - just adding a new AnnotationTypeFilter and voila we´ve got the class. Obtaining the class of an interface which has some annotation isn´t possible with the Spring scanner at first sight (and therefore we had a long experimenting phase with the fast-classpath-scanner). But looking a bit deeper, this is also possible - just via a really small hack :) , which only means to override the ClassPathScanningCandidateComponentProvider:
ClassPathScanningCandidateComponentProvider scanningProvider = new ClassPathScanningCandidateComponentProvider(false) {
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return true;
}
};
Now we´re able to scan for the SEI. And with that and adding the AssignableTypeFilter we also get the needed SEI implementing class.
Having all the three necessary classes at hand, we can easiely and automatically fire up a org.apache.cxf.jaxws.EndpointImpl
!
If you start your Spring Boot application and everything went fine, then you should see some of those log messages inside your console:
[...] INFO 83684 --- [ost-startStop-1] d.c.c.a.WebServiceAutoDetector : Found WebServiceClient class: 'de.codecentric.namespace.weatherservice.Weather'
[...] INFO 83684 --- [ restartedMain] d.c.c.a.WebServiceAutoDetector : Found Service Endpoint Interface (SEI): 'de.codecentric.namespace.weatherservice.WeatherService'
[...] INFO 83684 --- [ restartedMain] d.c.c.a.WebServiceAutoDetector : Found SEI implementing class: 'de.codecentric.soap.endpoint.WeatherServiceEndpoint'
Deactivate autoinitialization
Although it should be a great feature to be able to work 100% contract first, there might be situations, where one wants to deactivate it. E.g. while running in client-only mode.
Because there is (& sadly will be) no @ConditionalOnMissingProperty in Spring Boot, we need to use a workaround:
@Bean
@ConditionalOnProperty(name = "endpoint.autoinit", matchIfMissing = true)
public Endpoint endpoint() throws BootStarterCxfException ...
To get the desired deactivation flag nevertheless, we need to use the @ConditionalOnProperty in an interesting way :) With the usage of matchIfMissing = true
and name = "endpoint.autoinit"
the autoinitialization feature is activated in situations, where the property is missing or is set to true
. Only, if endpoint.autoinit=false
the feature is disabled (which is quite ok in our use-case).
Setting the URL of the endpoint
You can manually specify the url of the Service Endpoint using the spring property: soap.service.publishedEndpointUrl
. This can be handy if your application is behind a reverse proxy and the resulting WSDLs don't reflect that.
Known limitations
Using devtools with mvn spring-boot:run
If you want to use the well known Spring Boot Developer Tools (devtools) - no problem. As long as you don´t want to use mvn spring-boot:run
. Because of the devtools make usage of the 2 separate classloaders the scanned, found and instantiated classes aren´t valid inside the other classloader and you could get into trouble. This is only in combination with the Complete automation of Endpoint initialization feature and the starting method mvn spring-boot:run
. All the other starting mechanisms of Spring Boot will work as expected (java -jar service.jar
, Starting inside the IDE via Run as...
or in mvn test
).
Contribution
If you want to know more or even contribute to this Spring Boot Starter, maybe you need some information like: