This is the second part of the tutorial creating SOAP microservices with Spring Boot. On this occasion, we will use the same artifacts from the previous article Part 1, the project structure, the controllers, and the WSDL/XSD files.
Using the same WSDL and components will help you understand the key differences between Apache CXF and Spring WS for creating SOAP webservices, and those will be discussed in another publication.
If you want to skip the introduction and go directly to the code, then you can find it in my GitHub repository ws-employee-soapspringws
jpontdia / ws-employee-soapspringws
SOAP microservice with Spring Boot and Spring Web Services (Spring-WS)
SOAP Microservices with Spring Boot and Spring web services (spring-ws)
A docker container created with Spring Boot exposing a SOAP endpoint for a legacy client
The tech stack for this POC is:
- Spring Boot 2.3.4
- Java 15
- Spring Webservices (spring-ws)
- REST Assured 4.3
- Docker
Software requirements
Workstation must be properly configured with next tools:
- Java 15 JDK - https://jdk.java.net/15/
- Maven - https://maven.apache.org/download.cgi
Optional tools
- Docker - https://www.docker.com/products/docker-desktop
- Docker hub account with token access enabled
WSDL and Domain Model
In our example, we are going to work in a fictitious Employee SOAP service with 2 operations:
- GetEmployeeById
- GetEmployeeByName
For the demo, I separated the XSD from the WSDL. In a real scenario, this will be the most followed pattern but expect to have more than one XSD in different folders. The employee.xsd has the full domain model for the service, next diagram shows the main response sent back to the client
…
As we discussed in the first part, we follow the contract-first approach, which means we have a previously defined WSDL/XSD, and we want to implement it with spring-ws. Once the endpoint is created, the framework can create a dynamic WSDL, but we don't want that because we want to preserve the original. So we are going to expose the original as a static WSDL. The complete explanation about the WSDL and XSD schema is in section: Get the WSDL for the service
of the Part 1
Setting up the project
The project uses maven, and the properties and dependencies of the pom.xml are these:
<properties>
<!-- Override BOM property in Spring Boot for Rest Assured and Groovy-->
<!-- With rest-assured 4.3.X upgrade the Groovy from 2.5.7 to 3.0.2 -->
<rest-assured.version>4.3.1</rest-assured.version>
<groovy.version>3.0.2</groovy.version>
<!-- Other properties-->
<java.version>15</java.version>
<springboot.version>2.3.4.RELEASE</springboot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Generating the Java classes from XSD
In spring-ws, the Java classes are only generated from the XSD files; the WSDL is not used for Java generation. The employee.xsd is used for this purpose.
We will use the maven plugin: jaxb2-maven-plugin
for java generation, the classes are saved in the next directory:
<source-code>/target/generated-sources/jaxb
Next is the configuration of the plugin in the pom.xml
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<version>2.5.0</version>
<executions>
<execution>
<id>xjc</id>
<goals>
<goal>xjc</goal>
</goals>
</execution>
</executions>
<configuration>
<sources>
<source>${project.basedir}/src/main/resources/wsdl/employee.xsd</source>
</sources>
</configuration>
</plugin>
Generate the java classes by running in the command window in the project root:
mvn compile
Next image shows the generated classes:
Configure Spring Webservices (spring-ws)
Add the annotation @EnableWs
to the configuration class
@Configuration
@EnableWs
public class ApplicationConfiguration {
Configure the path for the exposed webservice; in our example, it is /soap/service/.
@Bean
public ServletRegistrationBean<MessageDispatcherServlet> messageDispatcherServlet(ApplicationContext applicationContext) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
servlet.setTransformWsdlLocations(true);
return new ServletRegistrationBean<>(servlet, "/soap/service/*");
}
Expose the static WSDL and XSD. We configure the service endpoint as: /soap/service/EmployeeService
. We get the WSDL from the service with http://localhost:8081/soap/service/EmployeeService.wsdl
@Bean(name = "EmployeeService")
public Wsdl11Definition defaultWsdl11Definition() {
SimpleWsdl11Definition wsdl11Definition = new SimpleWsdl11Definition();
wsdl11Definition.setWsdl(new ClassPathResource("/wsdl/EmployeeServices.wsdl"));
return wsdl11Definition;
}
@Bean
public XsdSchema employee() {
return new SimpleXsdSchema(new ClassPathResource("/wsdl/employee.xsd"));
}
The XsdSchema definition is required because the WSDL has an imported XSD. The name of the bean must be the name of the XSD file.
Important
If you have more than one XSD, then all of them must be described with XsdShema. Another approach is using the class org.springframework.xml.xsd.commons.CommonsXsdSchemaCollection. If for any reason the XSD files were placed in a relative path in a parent location, then you should consider serving them as static resources, and placing them inside /static directory in your classpath.
<wsdl:types>
<xsd:schema>
<xsd:import namespace="http://www.jpworks.com/employee"
schemaLocation="../employee.xsd"/>
</xsd:schema>
</wsdl:types>
Implementing the service
We need to implement the portType section of the WSDL file manually
Every operation is implemented using the @PayloadRoot
and @ResponsePayload
annotations. The @PayloadRoot
has two elements, the namespace, and the localPart. For the operation GetEmployeeById
the localPart is EmployeeByIdRequest
and this value comes from the next section:
<wsdl:operation name="GetEmployeeById">
<wsdl:input message="tns:EmployeeByIdRequest"/>
And the namespace value comes from the reference tns:
, we find this value in the WSDL definitions section:
<wsdl:definitions
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:tns="http://www.jpworks.com/employee"
The java method is a direct implementation of the operation using the generated classes from the XSD:
@PayloadRoot(namespace = NAMESPACE_URI, localPart = "EmployeeByIdRequest")
@ResponsePayload
public EmployeeResponse getEmployeeById(@RequestPayload EmployeeByIdRequest parameters) {
The complete implementation with the fake backend service is here:
package com.jpworks.datajdbc.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.jpworks.employee.*;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;
@Slf4j
@Endpoint
@RequiredArgsConstructor
public class EmployeeEndpoint{
private final BackendService backendService;
private static final String NAMESPACE_URI = "http://www.jpworks.com/employee";
@PayloadRoot(namespace = NAMESPACE_URI, localPart = "EmployeeByIdRequest")
@ResponsePayload
public EmployeeResponse getEmployeeById(@RequestPayload EmployeeByIdRequest parameters) {
EmployeeResponse employeeResponse = new EmployeeResponse();
try{
employeeResponse.setEmployee(backendService.getEmployeeById(parameters.getId()));
}
catch (Exception e){
log.error("Error while setting values for employee object", e);
}
return employeeResponse;
}
@PayloadRoot(namespace = NAMESPACE_URI, localPart = "EmployeeByNameRequest")
@ResponsePayload
public EmployeesResponse getEmployeesByName(@RequestPayload EmployeeByNameRequest parameters) {
EmployeesResponse employeesResponse = new EmployeesResponse();
try{
employeesResponse.getEmployee().addAll(backendService.getEmployeesByName(parameters.getFirstname(), parameters.getLastname()));
}
catch (Exception e){
log.error("Error while setting values for employee object", e);
}
return employeesResponse;
}
}
Running the application
In a command window, on the root project run:
mvn spring-boot:run
The log in the console:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.4.RELEASE)
2020-10-13 15:03:35.154 INFO 157100 --- [ main] com.jpworks.datajdbc.MainApplication : Starting MainApplication on LNAR-PC0NTFTQ with PID 157100 (C:\workspace\dev\datajdbc\ws-employee-soapspringws\target\classes started by jponte in C:\workspace\dev\datajdbc\ws-employee-soapspringws)
2020-10-13 15:03:35.156 DEBUG 157100 --- [ main] com.jpworks.datajdbc.MainApplication : Running with Spring Boot v2.3.4.RELEASE, Spring v5.2.9.RELEASE
2020-10-13 15:03:35.157 INFO 157100 --- [ main] com.jpworks.datajdbc.MainApplication : The following profiles are active: local
2020-10-13 15:03:35.642 INFO 157100 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.ws.config.annotation.DelegatingWsConfiguration' of type [org.springframework.ws.config.annotation.DelegatingWsConfiguration$$EnhancerBySpringCGLIB$$1d99be26] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2020-10-13 15:03:35.689 INFO 157100 --- [ main] .w.s.a.s.AnnotationActionEndpointMapping : Supporting [WS-Addressing August 2004, WS-Addressing 1.0]
2020-10-13 15:03:35.988 INFO 157100 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8081 (http)
2020-10-13 15:03:35.997 INFO 157100 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-10-13 15:03:35.998 INFO 157100 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.38]
2020-10-13 15:03:36.097 INFO 157100 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-10-13 15:03:36.097 INFO 157100 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 902 ms
2020-10-13 15:03:36.268 INFO 157100 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-10-13 15:03:36.405 INFO 157100 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
2020-10-13 15:03:36.414 INFO 157100 --- [ main] com.jpworks.datajdbc.MainApplication : Started MainApplication in 1.627 seconds (JVM running for 2.19)
To get the wsdl of the service, write in the browser:
http://localhost:8081/soap/service/EmployeeService.wsdl
Testing the application with SoapUI and the endpoint:
http://localhost:8081/soap/service/EmployeeService
Conclusion
This tutorial explained how to create a SOAP microservice with a contract-first approach, using Spring Boot and Spring Webservices (spring-ws)
If you have any questions, feel free to ask in the comments; thanks for reading!
Top comments (0)