SOAP Web Services With Apache CXF and Spring Boot
In this post, we build a SOAP Web Service from scratch using Apache CXF and Spring Boot.
Join the DZone community and get the full member experience.
Join For FreeThis post is based on one I wrote a few years ago about building contract first web services with Apache CXF and Spring. The previous post didn't use Spring Boot and most of the Spring and CXF configuration was via XML. This post moves things forward a bit by using the latest version of CXF and Spring Boot.
Sample App
We're going to build a simple Spring Boot app that exposes SOAP web service using Apache CXF. The service will have a single operation that takes an account number and returns bank account details. If you're impatient and want to jump ahead you can grab the full source code from GitHub.
Defining the Data Model
When building web services I always take a contract first approach. That means defining the service contract as a WSDL before writing the service implementation. We'll begin that process by creating an XSD file with the types that the Account Service will use. The first type we'll create is Account
— the diagram below shows an XSD snippet for Account
as well as a visual representation taken from XML Spy. Note: you don't need XML Spy to define XSDs but a visual editor can be very handy if you're designing complex domain models.
Next, we'll define a request type for the service that encapsulates service parameters. In this case, we have only one parameter, accountNumber
, but its good practice to define a request type as its a nice way of wrapping multiple parameters and keeps the interface clean.
Finally, we'll define the response type AccountDetailsResponse
which is a simple wrapper for Account
we created earlier. Again, we could have simply returned Account
but I think it's generally a good idea to use wrappers for the response type as it means that we can easily add other data to the response wrapper in the future.
The full schema definition is shown below.
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://com/blog/samples/webservices/accountservice" xmlns:account="http://webservices.samples.blog.com" targetNamespace="http://com/blog/samples/webservices/accountservice" elementFormDefault="qualified">
<xsd:complexType name="Account">
<xsd:sequence>
<xsd:element name="AccountNumber" type="xsd:string"/>
<xsd:element name="AccountName" type="xsd:string"/>
<xsd:element name="AccountBalance" type="xsd:double"/>
<xsd:element name="AccountStatus" type="EnumAccountStatus"/>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="EnumAccountStatus">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="Active"/>
<xsd:enumeration value="Inactive"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:element name="AccountDetailsRequest">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="accountNumber" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="AccountDetailsResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="AccountDetails" type="Account"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
Defining the Service WSDL
Now that we've defined the service types via XSD, its time to create a WSDL to define the public facing service contract. A WSDL is an XML document that describes a SOAP Web Service and how clients can interact with it. The Account Service WSDL is defined as follows.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap12/"
xmlns:tns="http://www.briansjavablog.com/Accounts/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="Accounts"
targetNamespace="http://www.briansjavablog.com/Accounts/"
xmlns:accounts="http://com/blog/samples/webservices/accountservice">
<wsdl:types>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:import namespace="http://com/blog/samples/webservices/accountservice"
schemaLocation="../schema/AccountsService.xsd">
</xsd:import>
</xsd:schema>
</wsdl:types>
<wsdl:message name="AccountDetailsRequest">
<wsdl:part element="accounts:AccountDetailsRequest" name="parameters" />
</wsdl:message>
<wsdl:message name="AccountDetailsResponse">
<wsdl:part element="accounts:AccountDetailsResponse" name="parameters" />
</wsdl:message>
<wsdl:portType name="Accounts">
<wsdl:operation name="GetAccountDetails">
<wsdl:input message="tns:AccountDetailsRequest" />
<wsdl:output message="tns:AccountDetailsResponse" />
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="AccountsServiceSoapBinding" type="tns:Accounts">
<soap:binding style="document"
transport="http://schemas.xmlsoap.org/soap/http" />
<wsdl:operation name="GetAccountDetails">
<soap:operation
soapAction="http://www.briansjavablog.com/Accounts/GetAccountDetails" />
<wsdl:input>
<soap:body use="literal" />
</wsdl:input>
<wsdl:output>
<soap:body use="literal" />
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="AccountsService">
<wsdl:port binding="tns:AccountsServiceSoapBinding" name="AccountsPort">
<soap:address
location="http://localhost:8080/apache-cfx-demo/services/accounts" />
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
The WSDL contains 5 key pieces of information:
- Types - <wsdl:types> defines the domain model used by the service. The model is defined via XSD and can be included inline, in the WSDL or imported from a separate XSD. Line 9 above imports the XSD file we created earlier.
- Message - <wsdl:message> defines the request and response messages used by the service. The nested <wsdl:part> section defines the domain types of the request and response messages.
- PortType - <wsdl:portType> defines the service operations, parameters, and response types exposed to clients.
- Binding - <wsdl:binding> defines the protocol and data format.
- The binding type attribute refers to the portType defined earlier in the WSDL.
- The soap binding style can be either RPC or document.
- The transport attribute indicates that the service will be exposed over HTTP. Other options (less common) include JMS and SMTP.
- The operation element defines each operation that we exposed through the portType.
- Binding - <wsdl:binding> defines the protocol and data format.
- Service - <wsdl:service> defines the exposed service using the
portType
and binding we defined above.
Generating the Service Interface and Domain Model
At this point, we have a WSDL that defines a contract for the service we're going to build. The next step is to use the WSDL to generate Java classes for the domain model and service interface. We're going to use the CXF codegen plugin to run a WSDL2Java job as part of the build. The codegen plugin is defined in the project POM as follows.
<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<sourceRoot>src/generated/java</sourceRoot>
<wsdlOptions>
<wsdlOption>
<wsdl>${basedir}/src/main/resources/wsdl/Accounts.wsdl</wsdl>
</wsdlOption>
</wsdlOptions>
</configuration>
<goals>
<goal>wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>
The sourceRoot
element tells the plugin to put the generated java classes in a new source directory src/generated/java. This separates the generate classes from classes we'll write ourselves. The wsdl
element points to the WSDL file we created earlier.
To run the code generation open a command window and run mvn generate-sources
.
Refresh your IDE workspace and you'll see two new packages. By default, the package names are based on the namespaces in the WSDL. The contents of both packages are described below.
- com.blog.demo.webservices.accountservice - contains the four domain objects,
Account
,AccountDetailsRequest
,AccountDetailsResponse
andEnumAccountStatus
. These are the core types we defined in the XSD earlier and will form the main building blocks of the service. - com.briansdevblog.accounts - contains the Service Endpoint Interface
AccountService
. This interface is a Java representation of the service operation defined in the WSDL.AccountService_Service.
is a web service client and can be used to invoke the service.
Service Endpoint Interface
The generated Service Endpoint Interface AccountService
contains a method for each operation defined in the WSDL. As you'd expect, the method parameter and return type are those defined in the XSD and referenced in the WSDL.
@WebService(targetNamespace = "http://www.briansdevblog.com/Accounts/", name = "AccountService")
@XmlSeeAlso({com.blog.demo.webservices.accountservice.ObjectFactory.class})
@SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE)
public interface AccountService {
@WebMethod(operationName = "GetAccountDetails", action = "http://www.briansjavablog.com/Accounts/GetAccountDetails")
@WebResult(name = "AccountDetailsResponse", targetNamespace = "http://com/blog/demo/webservices/accountservice", partName = "parameters")
public com.blog.demo.webservices.accountservice.AccountDetailsResponse getAccountDetails(
@WebParam(partName = "parameters", name = "AccountDetailsRequest", targetNamespace = "http://com/blog/demo/webservices/accountservice")
com.blog.demo.webservices.accountservice.AccountDetailsRequest parameters
);
}
- @WebService - Marks the class as defining a Web Service interface from a WSDL. The namespace should match the namespace defined in the WSDL and the name should match the WSDL Port Type.
- @XmlSeeAlso - Lets JAXB know what other classes need to be registered with the JAXB context for serialization and deserialization.
- @SoapBinding - Describes mapping from web service operations to the SOAP protocol.
- @WebMethod - Maps a service operation to a Java method. The operation name references the operation defined in the WSDL and the target namespace uses the namespace associated with the WSDL operation.
- @WebResult - Maps a service operation response message to a Java return type. The name refers to the response message name defined in the WSDL. The target namespace uses the namespace associated with the WSDL message and the
partName
refers towsdl:part
name in the WSDL. - @WebParam - Maps a service operation request message to a Java parameter type. The name refers to the request message name defined in the WSDL. The target namespace uses the namespace associated with the WSDL message and the
partName
refers towsdl:part
name in the WSDL.
Writing the Service Endpoint
Next, we're going to create the service endpoint by implementing the AccountService
interface.
@Service
public class AccountServiceEndpoint implements AccountService {
@Override
public AccountDetailsResponse getAccountDetails(AccountDetailsRequest parameters) {
ObjectFactory factory = new ObjectFactory();
AccountDetailsResponse response = factory.createAccountDetailsResponse();
Account account = factory.createAccount();
account.setAccountNumber("12345");
account.setAccountStatus(EnumAccountStatus.ACTIVE);
account.setAccountName("Joe Bloggs");
account.setAccountBalance(3400);
response.setAccountDetails(account);
return response;
}
}
The endpoint implements the getAccountDetails
method on the AccountService
interface. To keep things simple the method body returns some hard-coded Account
data wrapped in a AccountDetailsResponse
.
Configuring the Service With Spring
In the past, we would have configured the service endpoint via XML, but now that we're using Spring Boot we can move all our configuration to Java. The XML config was okay, but it's hard to beat configuring Spring with Java. The ApplicationConfig
class below contains all the configuration required to run the service. I'll describe each bean below.
@Configuration
public class ApplicationConfig {
@Bean
public ServletRegistrationBean<CXFServlet> dispatcherServlet() {
return new ServletRegistrationBean<CXFServlet>(new CXFServlet(), "/soap-api/*");
}
@Bean
@Primary
public DispatcherServletPath dispatcherServletPathProvider() {
return () -> "";
}
@Bean(name=Bus.DEFAULT_BUS_ID)
public SpringBus springBus(LoggingFeature loggingFeature) {
SpringBus cxfBus = new SpringBus();
cxfBus.getFeatures().add(loggingFeature);
return cxfBus;
}
@Bean
public LoggingFeature loggingFeature() {
LoggingFeature loggingFeature = new LoggingFeature();
loggingFeature.setPrettyLogging(true);
return loggingFeature;
}
@Bean
public Endpoint endpoint(Bus bus, AccountServiceEndpoint accountServiceEndpoint) {
EndpointImpl endpoint = new EndpointImpl(bus, accountServiceEndpoint);
endpoint.publish("/service/accounts");
return endpoint;
}
}
ServletRegistrationBean<CXFServlet>
- registers the CXF dispatcher servlet to handle incoming HTTP requests to /soap-api/*. The dispatcher servlet essentially routes requests to an endpoint for processing.SpringBus
- is a Spring flavoured CXFBus
. ABus
is a core CXF extension point that allows you to add interceptors to any CXF client or endpoint that uses the bus. In the example above we add the injectedLoggingFetaure
to enable logging.LoggingFeature
- AFeature
is something that adds some functionality to a CXF client or server. In this instance theLoggingFeature
performs logging of the inbound and outbound SOAP payload. I've enabled pretty logging to make the SOAP messages a little more readable.Endpoint
- exposes a HTTP endpoint to process incoming SOAP requests. The publish method tells CXF to publish the endpoint at /service/accounts. This path will be appended to the /soap-api/* pattern used to configure theCXFServlet
earlier. This means the endpoint is exposed at CONTEXT_ROOT/soap-api/service/accounts.
Writing an Integration Test
Next, we're going to write an integration test to make sure everything is working as expected. The test will do the following:
- Use Jetty to stand up and an instance of the endpoint at http://localhost:8080/services/accounts.
- Create a web service client/SOAP proxy to handle serialization of request and deserialization of response.
- Create an
AccountDetailsRequest
, send it to the SOAP endpoint and check the contents of theAccountDetailsResponse
- Tear down the test endpoint.
Related tutorial: How to Integrate Cucumber for Spring Boot Integration Tests
You'll notice that the test doesn't use ApplicationConfig
we defined earlier but instead uses the following test specific config.
@Configuration
@ComponentScan("com.blog.demo.service")
public class TestConfig {
private static final String SERVICE_URL = "http://localhost:8080/services/accounts";
@Bean("accountServiceClient")
public AccountService accountServiceClient() {
JaxWsProxyFactoryBean jaxWsProxyFactoryBean = new JaxWsProxyFactoryBean();
jaxWsProxyFactoryBean.setServiceClass(AccountService.class);
jaxWsProxyFactoryBean.setAddress(SERVICE_URL);
return (AccountService) jaxWsProxyFactoryBean.create();
}
@Bean(name=Bus.DEFAULT_BUS_ID)
public SpringBus springBus(LoggingFeature loggingFeature) {
SpringBus bus = new SpringBus();
bus.getFeatures().add(loggingFeature);
return bus;
}
@Bean
public LoggingFeature loggingFeature() {
LoggingFeature loggingFeature = new LoggingFeature();
loggingFeature.setPrettyLogging(true);
return loggingFeature;
}
@Bean
public Endpoint endpoint(Bus bus, LoggingFeature loggingFeature, AccountServiceEndpoint accountServiceEndpoint) {
EndpointImpl endpoint = new EndpointImpl(bus, accountServiceEndpoint);
endpoint.publish(SERVICE_URL);
return endpoint;
}
The accountServiceClient
method uses the JaxWsProxyFactoryBean
to create a web service client for the AccountService
Service Endpoint Interface. The client is configured to call the endpoint at http://localhost:8080/services/accounts. In order to stand up a test instance of the endpoint, we also configure a SpringBus
, LoggingFeature
, and Endpoint
similar to the way we did in ApplicationConfig
.
The AccountServiceEndpointTest
below uses the injected accountServiceClient
from TestConfig
.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class AccountServiceEndpointTest {
@Autowired
@Qualifier("accountServiceClient")
private AccountService accountsServiceClient;
private AccountDetailsRequest accountDetailsRequest;
@Before
public void setUp() throws Exception {
ObjectFactory objectFactory = new ObjectFactory();
accountDetailsRequest = objectFactory.createAccountDetailsRequest();
accountDetailsRequest.setAccountNumber("12345");
}
@Test
public void testGetAccountDetails() throws Exception {
AccountDetailsResponse response = accountsServiceClient.getAccountDetails(accountDetailsRequest);
assertTrue(response.getAccountDetails()!= null);
assertTrue(response.getAccountDetails().getAccountNumber().equals("12345"));
assertTrue(response.getAccountDetails().getAccountName().equals("Joe Bloggs"));
assertTrue(response.getAccountDetails().getAccountBalance() == 3400);
assertTrue(response.getAccountDetails().getAccountStatus().equals(EnumAccountStatus.ACTIVE));
}
}
Running the Test
You can run the integration test in your IDE or fire up the command line and run mvn test
. You'll see Jetty start on port 8080 and the SOAP request/response payloads logged when the endpoint is called. Learn how to check which process is using port 8080.
Running as a Stand Alone Service
At this point, we've run the integration test and everything is behaving as expected. The only thing left to do is to fire up the service with Spring Boot. On the command line run mvn spring-boot:run
. The service should start on port 8090 as shown below.
If you browse to http://localhost:8090/soap-api you'll see the standard CXF service page with the Account Service listed and a link to the WSDL.
You can now test the service using any standard HTTP client. Below I use Postman to send a POST request to http://localhost:8090/soap-api/service/accounts. The SOAP response is displayed as expected.
Read our related tutorial on how to get an access token from Keycloak with Postman.
Wrapping Up
In this post, we built a SOAP Web Service from scratch using Apache CXF and Spring Boot. We began by defining the data model and WSDL (contract first) and then moved on to implement the service endpoint. We looked at how a CXF endpoint is configured in Spring Boot and also put together a simple integration test. The full source code for this post is available on GitHub.
Opinions expressed by DZone contributors are their own.
Comments