How to create SOAP 馃Ъ馃 services with Spring Boot 馃崈 + Apache CXF
One of the standards most extended for create services before REST was SOAP, the main difference is instead of using JSON as a data interchange element uses XML.
But the main advantage of SOAP services in the past was not only to define a way to transmit the information, it also included rules related to the structure of the messages, I mean things like WSDL as a standard, which is a way to establish a contract for any consumer of your web service.
As today the equivalent for WSDL on REST services is Swagger / OpenAPI as a definition standard, so personally I think right now there's not much sense of making SOAP services instead of REST main reason because JSON is lightweight compared to XML and currently in the REST world there's alternatives to the things SOAP has, but is important to know about SOAP because atleast during my career I had to deal with them, still existing so many services working with SOAP and probably in some point you're going to need to consume them, so the main objective of this article is open the way for this article where I talk about how to consume SOAP services.
Let's get started.
For getting started, I'm going to start making a new Spring Boot project using the wonderful site Spring Initialzr which allow us to select quickly all the starters we need.
In this specific case I'm only going to need:
- Spring Web starter
But I'm going to also select the Spring DevTools
just for have a better development experience.
After downloading the .zip and open the project, I'm going to add also another dependency to the pom.xml
file.
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxws</artifactId>
<version>4.0.2</version>
</dependency>
As a recommendation put the version as a variable in <properties>
tag inside pom.xml and use ${cxf.version}
Creating our first operation.
Comparing them to the REST services in SOAP we have only a single endpoint with multiple operations.
For example, you have a /Hello
endpoint, then when you consume the SOAP service, you're always going to make a POST request to /Hello
and depending on the body send, it will execute a different operation. (for some of you this probably reminds you of GraphQL)
For create my first operation I've created a class and then added a simple method:
package dev.mpesteban.soapservice;
import jakarta.jws.WebMethod;
import jakarta.jws.WebService;
@WebService
public class MySoapService {
@WebMethod
public String sayHello(String name) {
return "Hello, " + name + "!";
}
}
Next step is create the required beans for make it work correctly with Spring Boot, for that I created another class with the @Configuration
annotation:
package dev.mpesteban.soapservice;
import jakarta.xml.ws.Endpoint;
import org.apache.cxf.Bus;
import org.apache.cxf.bus.spring.SpringBus;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.transport.servlet.CXFServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WebServiceConfig {
@Bean
public ServletRegistrationBean<CXFServlet> cxfServlet() {
return new ServletRegistrationBean<>(new CXFServlet(), "/ws/*");
}
@Bean(name = Bus.DEFAULT_BUS_ID)
public SpringBus springBus() {
return new SpringBus();
}
@Bean
public Endpoint endpoint() {
EndpointImpl endpoint = new EndpointImpl(springBus(), new MySoapService());
endpoint.publish("/Hello");
return endpoint;
}
}
And that's all we've our service ready to be used.
Let's try it!
For try if our operation was created and works fine, we run the project normally as we do with any Spring Boot project.
The first thing we can see as a good signal for know if it's working fine is some of the log messages, indicating the service was registered:
2023-08-08T11:33:35.763+02:00 INFO 34656 --- [ restartedMain] o.a.c.w.s.f.ReflectionServiceFactoryBean : Creating Service {http://soapservice.mpesteban.dev/}MySoapServiceService from class dev.mpesteban.soapservice.MySoapService
2023-08-08T11:33:36.124+02:00 INFO 34656 --- [ restartedMain] org.apache.cxf.endpoint.ServerImpl : Setting the server's publish address to be /Hello
This is a good signal but if we open a browser and go to: http://localhost:8080/ws/Hello?wsdl
we should be able to see the WSDL definition
<wsdl:definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema"
聽 聽 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://soapservice.mpesteban.dev/"
聽 聽 xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
聽 聽 xmlns:ns1="http://schemas.xmlsoap.org/soap/http" name="MySoapServiceService"
聽 聽 targetNamespace="http://soapservice.mpesteban.dev/">
聽 聽 <wsdl:types>
聽 聽 聽 聽 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
聽 聽 聽 聽 聽 聽 xmlns:tns="http://soapservice.mpesteban.dev/" elementFormDefault="unqualified"
聽 聽 聽 聽 聽 聽 targetNamespace="http://soapservice.mpesteban.dev/" version="1.0">
聽 聽 聽 聽 聽 聽 <xs:element name="sayHello" type="tns:sayHello" />
聽 聽 聽 聽 聽 聽 <xs:element name="sayHelloResponse" type="tns:sayHelloResponse" />
聽 聽 聽 聽 聽 聽 <xs:complexType name="sayHello">
聽 聽 聽 聽 聽 聽 聽 聽 <xs:sequence>
聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 <xs:element minOccurs="0" name="arg0" type="xs:string" />
聽 聽 聽 聽 聽 聽 聽 聽 </xs:sequence>
聽 聽 聽 聽 聽 聽 </xs:complexType>
聽 聽 聽 聽 聽 聽 <xs:complexType name="sayHelloResponse">
聽 聽 聽 聽 聽 聽 聽 聽 <xs:sequence>
聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 <xs:element minOccurs="0" name="return" type="xs:string" />
聽 聽 聽 聽 聽 聽 聽 聽 </xs:sequence>
聽 聽 聽 聽 聽 聽 </xs:complexType>
聽 聽 聽 聽 </xs:schema>
聽 聽 </wsdl:types>
聽 聽 <wsdl:message name="sayHelloResponse">
聽 聽 聽 聽 <wsdl:part element="tns:sayHelloResponse" name="parameters"> </wsdl:part>
聽 聽 </wsdl:message>
聽 聽 <wsdl:message name="sayHello">
聽 聽 聽 聽 <wsdl:part element="tns:sayHello" name="parameters"> </wsdl:part>
聽 聽 </wsdl:message>
聽 聽 <wsdl:portType name="MySoapService">
聽 聽 聽 聽 <wsdl:operation name="sayHello">
聽 聽 聽 聽 聽 聽 <wsdl:input message="tns:sayHello" name="sayHello"> </wsdl:input>
聽 聽 聽 聽 聽 聽 <wsdl:output message="tns:sayHelloResponse" name="sayHelloResponse"> </wsdl:output>
聽 聽 聽 聽 </wsdl:operation>
聽 聽 </wsdl:portType>
聽 聽 <wsdl:binding name="MySoapServiceServiceSoapBinding" type="tns:MySoapService">
聽 聽 聽 聽 <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" />
聽 聽 聽 聽 <wsdl:operation name="sayHello">
聽 聽 聽 聽 聽 聽 <soap:operation soapAction="" style="document" />
聽 聽 聽 聽 聽 聽 <wsdl:input name="sayHello">
聽 聽 聽 聽 聽 聽 聽 聽 <soap:body use="literal" />
聽 聽 聽 聽 聽 聽 </wsdl:input>
聽 聽 聽 聽 聽 聽 <wsdl:output name="sayHelloResponse">
聽 聽 聽 聽 聽 聽 聽 聽 <soap:body use="literal" />
聽 聽 聽 聽 聽 聽 </wsdl:output>
聽 聽 聽 聽 </wsdl:operation>
聽 聽 </wsdl:binding>
聽 聽 <wsdl:service name="MySoapServiceService">
聽 聽 聽 聽 <wsdl:port binding="tns:MySoapServiceServiceSoapBinding" name="MySoapServicePort">
聽 聽 聽 聽 聽 聽 <soap:address location="http://localhost:8080/ws/Hello" />
聽 聽 聽 聽 </wsdl:port>
聽 聽 </wsdl:service>
</wsdl:definitions>
If that's the case that means everything goes like a charm, for finally finish of check everything, I downloaded SOAP UI, for me a essential tool when I work with SOAP services, for summarize what is this tool is basically "Postman but for SOAP services", Postman currently as the date of writing of this article supports already importing WSDL definition but I had some problems it not generates fine the example content request that's why for now I prefer still using SOAP UI.
For run request against our SOAP service with SOAP UI:
From File > New SOAP Project
:
- Project Name: you're free to choose the name you prefer.
- Initial WSDL: URL of WSDL
http://localhost:8080/ws/Hello?wsdl
This is going to create a new SOAP project which allow us to to request against our operations.
Let's do the request:
We can see the response is fine!
Working with more complex data structures
The example shown before is super simple but what if we want to recieve a object with multiple attributes, how can we do that?
In this case we have the JAXB annotations, let's look to an example:
For this example I'm going to create a Car
class, and a new operation in my service which return a list of Car
based on the brand introduced by parameters in the request.
package dev.mpesteban.soapservice;
import jakarta.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Car {
private String brand;
private String model;
private Integer horsepower;
public Car(String brand, String model, Integer horsepower) {
this.brand = brand;
this.model = model;
this.horsepower = horsepower;
}
public Car() {}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public Integer getHorsepower() {
return horsepower;
}
public void setHorsepower(Integer horsepower) {
this.horsepower = horsepower;
}
}
Is important the Car
class has a empty constructor, is something required by JAXB to work fine.
Our new operation inside the MySoapService
class is going to look like this:
@WebMethod
public List<Car> getCarsByBrand(String brandName) {
final List<Car> cars = List.of(
new Car("Ford", "Mustang", 300),
new Car("Kia", "Ceed", 140),
new Car("Honda", "Civic", 300),
new Car("Honda", "Jazz", 90)
);
return cars.stream().filter((e) -> e.getBrand().equalsIgnoreCase(brandName)).collect(Collectors.toList());
}
We do a request to try our new operation and everything works!
Making the addition of new endpoints simpler
As you will have observed at the beginning, each time we want to create a new "Endpoint", it involves creating a new Bean, indicating the class that contain operations associated with that endpoint, this means that there may be some point in which the configuration class grows a lot, in a repetitive task such as the creation of endpoints.
For solve this, we're going to create our custom annotation which allow us at the start of the application get all the classes which contains this annotation, instance them and create a Endpoint bean.
This is our custom annotation:
package dev.mpesteban.soapservice;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface WebServiceEndpoint {
String value();
}
With our custom annotation created, let's write all the stuff needed for get the classes and create the beans, first I'm going to modify the class WebServiceConfig
and make it to implement the BeanFactoryPostProcessor
interface which proportionates the method postProcessBeanFactory
The postProcessBeanFactory
method is called during the startup of the Spring context, after all bean definitions have been loaded but before all actual bean instances have been created. This provides the opportunity to influence how the beans will be configured and instantiated before the application itself uses them, in our specific case it will allow to define beans dynamically.
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
try {
generateServiceBeans(beanFactory);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
generateServiceBeans
method has the main things we're going to use.
public void generateServiceBeans(
final ConfigurableListableBeanFactory beanFactory
) throws ClassNotFoundException {
List<Class<?>> clazzList = findAllWebServiceEndpointClasses(this.getClass().getPackageName());
log.info("Detected " + clazzList.size() + " SOAP WebServices interfaces");
Bus bus = beanFactory.getBean(Bus.class);
for (Class<?> clazz : clazzList) {
WebServiceEndpoint wsEndpoint = clazz.getAnnotation(WebServiceEndpoint.class);
beanFactory.registerSingleton("endpoint" + clazz.getSimpleName(), generateEndpointBean(bus, clazz, wsEndpoint));
}
}
As we can see there's a method called findAllWebServiceEndpointClasses
this method contains all the stuff for find the classes with our custom annotation, it recieves the package name, so in this case I get the package name on the class but this is something you to change to fit to you.
private List<Class<?>> findAllWebServiceEndpointClasses(String packageName) throws ClassNotFoundException {
final List<Class<?>> result = new LinkedList<>();
final ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(
false);
provider.addIncludeFilter(new AnnotationTypeFilter(WebServiceEndpoint.class));
for (BeanDefinition beanDefinition : provider
.findCandidateComponents(packageName)) {
result.add(Class.forName(beanDefinition.getBeanClassName()));
}
return result;
}
This method returns a list with all the reference of the classes which contains that annotation, to make it work correctly we should apply our custom annotation @WebServiceEndpoint
to the class MySoapService
this is the final result for this class:
package dev.mpesteban.soapservice;
import jakarta.jws.WebMethod;
import jakarta.jws.WebService;
import java.util.List;
import java.util.stream.Collectors;
@WebServiceEndpoint("/Hello")
@WebService
public class MySoapService {
@WebMethod
public String sayHello(String name) {
return "Hello, " + name + "!";
}
@WebMethod
public List<Car> getCarsByBrand(String brandName) {
final List<Car> cars = List.of(
new Car("Ford", "Mustang", 300),
new Car("Kia", "Ceed", 140),
new Car("Honda", "Civic", 300),
new Car("Honda", "Jazz", 90)
);
return cars.stream().filter((e) -> e.getBrand().equalsIgnoreCase(brandName)).collect(Collectors.toList());
}
}
Going back to the method generateServiceBeans
we can see the next thing it does it loop over all the list and for every element calls the method generateEndpointBean
and register the bean using beanFactory
.
for (Class<?> clazz : clazzList) {
WebServiceEndpoint wsEndpoint = clazz.getAnnotation(WebServiceEndpoint.class);
beanFactory.registerSingleton("endpoint" + clazz.getSimpleName(), generateEndpointBean(bus, clazz, wsEndpoint));
}
private <T> T generateEndpointBean(
final Bus bus,
Class<T> clazz,
final WebServiceEndpoint endpointConfig
) {
JaxWsServerFactoryBean endpointBean = new JaxWsServerFactoryBean();
endpointBean.setAddress(endpointConfig.value());
endpointBean.setBus(bus);
endpointBean.setServiceClass(clazz);
return (T) endpointBean.create();
}
Using the JaxWsServerFactoryBean
class it allows to create a instance of the class ready to be used as a Bean.
With all of this any class with our custom annotation is going to be considered as a new endpoint and avoids to be defining manually a bean for every endpoint, in summary no more boilerplate code for create endpoints.
And that's all for a solid base for a SOAP project.
驴Why Apache CXF instead of Spring WS?
This is a question probably the most advanced users had in mind since the beginning of the article.
The main reason is because the objective of this articule is start the SOAP service as fast as posible, Spring WS forces you to start defining your XSD schema (do the API contract-first) but Apache CXF allows you to do a code-first aproach and then it creates by itself all the definition stuff needed.