DEV Community

Joaquín Ponte
Joaquín Ponte

Posted on

SOAP Microservices with Spring Boot, Part 2 using Spring Webservices (spring-ws)

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

GitHub logo 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:

Optional tools

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:
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
portType diagram

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

WSDL Generation

Testing the application with SoapUI and the endpoint:
http://localhost:8081/soap/service/EmployeeService
WSDL Generation

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!

Discussion (0)