Creating contract-first web services with Spring WS

This article explains how to implement and test SOAP web services with Spring WS project. This example uses JAXB2 for (un)marshalling. To develop the service, I’ll use the contract-first approach, which consists in defining the service contract first, and based on this contract implement the service.

The article is divided into the following sections:

2   Explaining the application
3   Implementing the service
3.1   Creating the contract
3.2   Generating Java classes
3.3   Implementing the SOAP endpoint
3.4   Configuring the application
4   Testing the service
5   Additional information
5.1   Implementing the client
5.2   How it works internally

 

1 Explaining the application

The example application processes orders. We’ve got a front controller (messageDispatcher servlet) that will handle order requests, invoke the service to process the order and return a result.

application using web services with Spring WS

You can get the source code at my Github repository.

2 Implementing the service
3.1 Creating the contract

Since we will use the contract-first approach, the simplest way to create the contract is by first defining sample xml documents and from that, we will generate the contract using a tool. Below are the sample xml documents:

client-request.xml

<clientDataRequest xmlns="http://www.xpadro.spring.samples.com/orders"
    clientId="A-123"
    productId="C5FH"
    quantity="5" />
view raw

 

client-response.xml

<clientDataResponse xmlns="http://www.xpadro.spring.samples.com/orders" 
    confirmationId="7890B"
    orderDate="2013-09-22"
    amount="15.50" />

 

In order to create the schema, we can use Trang, which is an open source tool that will allow us to generate the xsd schema from the xml documents. I’ve included this library into the project build path (you can get this jar from Trang web site) and I’ve created an Ant task for executing the conversion:

generate-schema.xml

<project name="Ant-Generate-Schema-With-Trang" default="generate" basedir=".">
    <property name="src.dir" location="src" />
    <property name="trang.dir" location="lib" />
    <property name="source.dir" location="${src.dir}/main/webapp/WEB-INF/schemas/samples" />
    <property name="schema.dir" location="${src.dir}/main/webapp/WEB-INF/schemas/xsd" />
    
    <target name="generate" description="generates order schema">
        <delete dir="${schema.dir}" />
        <mkdir dir="${schema.dir}" />
        
        <java jar="${trang.dir}/trang.jar" fork="true">
            <arg value="${source.dir}/client-request.xml" />
            <arg value="${schema.dir}/client-request.xsd" />
        </java>
        
        <java jar="${trang.dir}/trang.jar" fork="true">
            <arg value="${source.dir}/client-response.xml" />
            <arg value="${schema.dir}/client-response.xsd" />
        </java>
    </target>
</project>

 

Once the Ant task is executed, it will generate the schemas. Since schemas have been automatically generated, it is possible that we need to make some modifications to adapt it to our needs. Let’s take a look:

 

client-request.xsd

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" 
    elementFormDefault="qualified" 
    targetNamespace="http://www.xpadro.spring.samples.com/orders" 
    xmlns:orders="http://www.xpadro.spring.samples.com/orders">
    
    <xs:element name="clientDataRequest">
        <xs:complexType>
            <xs:attribute name="clientId" use="required" type="xs:NCName"/>
            <xs:attribute name="productId" use="required" type="xs:NCName"/>
            <xs:attribute name="quantity" use="required" type="xs:integer"/>
        </xs:complexType>
    </xs:element>
</xs:schema>

 

client-response.xsd

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" 
    elementFormDefault="qualified" 
    targetNamespace="http://www.xpadro.spring.samples.com/orders" 
    xmlns:orders="http://www.xpadro.spring.samples.com/orders">
    
    <xs:element name="clientDataResponse">
        <xs:complexType>
            <xs:attribute name="amount" use="required" type="xs:decimal"/>
            <xs:attribute name="confirmationId" use="required" type="xs:NMTOKEN"/>
            <xs:attribute name="orderDate" use="required" type="xs:NMTOKEN"/>
        </xs:complexType>
    </xs:element>
</xs:schema>

 

We can add different validations to these schemas, but in this example I’ll just modify several types like clientId, productId and confirmationId (xs:string) and orderDate (xs:date). The mapping of XML data types to Java types is done by JAXB. You can check which are the mappings provided at Oracle JavaEE tutorial.

To finish with the schema, we will copy the response element into the request schema. I’ve created a third schema with both response and request:

 

client-service.xsd

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
    elementFormDefault="qualified" targetNamespace="http://www.xpadro.spring.samples.com/orders"
    xmlns:orders="http://www.xpadro.spring.samples.com/orders">
    
    <xs:element name="clientDataRequest">
        <xs:complexType>
            <xs:attribute name="clientId" use="required" type="xs:string" />
            <xs:attribute name="productId" use="required" type="xs:string" />
            <xs:attribute name="quantity" use="required" type="xs:integer" />
        </xs:complexType>
    </xs:element>
    
    <xs:element name="clientDataResponse">
        <xs:complexType>
            <xs:attribute name="amount" use="required" type="xs:decimal" />
            <xs:attribute name="confirmationId" use="required" type="xs:string" />
            <xs:attribute name="orderDate" use="required" type="xs:date" />
        </xs:complexType>
    </xs:element>
</xs:schema>

 

The last step would consist in writing the contract, generally expressed as a WSDL file. If you don’t want to create it by hand, the Spring-ws project provides us with a way to generate this file from an XSD schema. We will use this second approach as you will see in the configuring the application section.

2.2   Generating Java classes

We will use JAXB2 to generate request and response objects. The XJC compiler from JAXB will be responsible of converting these objects from the XSD schema that we generated before. It will be executed as an Ant task:

<project name="Ant-Generate-Classes-With-JAXB2" default="generate" basedir=".">
    <property name="src.dir" location="src" />
    <property name="java.dir" location="src/main/java" />
    <property name="schema.dir" location="${src.dir}/main/webapp/WEB-INF/schemas/xsd" />
    
    <target name="generate">
        <exec executable="xjc">
            <arg line=" -d ${java.dir} -p xpadro.spring.ws.types ${schema.dir}/client-service.xsd" />
        </exec>
    </target>
</project>

This task will create Java classes in the xpadro.spring.ws.types package (you may need to refresh the project).

created java types package structure

2.3   Implementing the SOAP endpoint

The endpoint receives the unmarshalled message payload and uses this data to invoke the order service. It will then return the service response, which will be marshalled by the endpoint adapter:

@Endpoint
public class OrderEndpoint {
    @Autowired
    private OrderService orderService;
    
    @PayloadRoot(localPart="clientDataRequest", namespace="http://www.xpadro.spring.samples.com/orders")
    public @ResponsePayload ClientDataResponse order(@RequestPayload ClientDataRequest orderData) {
        OrderConfirmation confirmation = 
            orderService.order(orderData.getClientId(), orderData.getProductId(), orderData.getQuantity().intValue());
        
        ClientDataResponse response = new ClientDataResponse();
        response.setConfirmationId(confirmation.getConfirmationId());
        BigDecimal amount = new BigDecimal(Float.toString(confirmation.getAmount()));
        response.setAmount(amount);
        response.setOrderDate(convertDate(confirmation.getOrderDate()));
        
        return response;
    }
    
    //date conversion
}

 

Here’s a brief description of the annotations used by the endpoint:

@Endpoint: Registers the class as a component. In this way, the class will be detected by component scan.

@PayloadRoot: Registers the endpoint method as a handler for a request. This annotation will define what type of request message can be handled by the method. In our example, it will receive messages where its payload root element has the same namespace as defined in the XSD schema we created, and its local name is the one defined for the request (clientDataRequest).

@RequestPayload: Indicates the payload of the request message to be passed as a parameter to the method.

@ResponsePayload, indicates that the return value is used as the payload of the response message.

2.4   Configuring the application

web.xml

Application configuration (like datasource, transactionManager…)

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:xpadro/spring/ws/config/root-config.xml</param-value>
</context-param>

 

Loads the application context:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

 

This is the servlet that will act as a Front Controller to handle all SOAP calls. Its function is to derive incoming XML messages to endpoints, much like the DispatcherServlet of Spring MVC:

<servlet>
    <servlet-name>orders</servlet-name>
    <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:xpadro/spring/ws/config/servlet-config.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>orders</servlet-name>
    <url-pattern>/orders/*</url-pattern>
</servlet-mapping>

 

servlet-config.xml

This configuration contains web service infrastructure beans.

<!-- Detects @Endpoint since it is a specialization of @Component -->
<context:component-scan base-package="xpadro.spring.ws"/>

<!-- detects @PayloadRoot -->
<ws:annotation-driven/>

<ws:dynamic-wsdl id="orderDefinition" portTypeName="Orders" locationUri="http://localhost:8081/spring-ws">
    <ws:xsd location="/WEB-INF/schemas/xsd/client-service.xsd"/>
</ws:dynamic-wsdl>

 

In the dynamic wsdl, it doesn’t matter what value you put in the locationUri attribute because it will be handled by the MessageDispatcherServlet. Hence, the wsdl will be available at:

http://localhost:8081/spring-ws/orders/whatever/orderDefinition.wsdl

3 Testing the service

The following example creates a mock client which will access the web service:

@ContextConfiguration("classpath:xpadro/spring/ws/test/config/test-server-config.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class TestWebService {
    @Autowired
    ApplicationContext context;
    
    private MockWebServiceClient mockClient;
    
    @Test
    public void testValidOrderRequest() {
        Source requestPayload = new StringSource(
            "<clientDataRequest xmlns='http://www.xpadro.spring.samples.com/orders' " +
            "clientId='123' productId='XA-55' quantity='5'/>");
        
        Source responsePayload = new StringSource(
            "<clientDataResponse xmlns='http://www.xpadro.spring.samples.com/orders' " +
            "amount='55.99' confirmationId='GHKG34L' orderDate='2013-10-26+02:00'/>");
        
        RequestCreator creator = RequestCreators.withPayload(requestPayload);
        mockClient = MockWebServiceClient.createClient(context);
        mockClient.sendRequest(creator).andExpect(ResponseMatchers.payload(responsePayload));
    }
    
    @Test
    public void testInvalidOrderRequest() {
        Source requestPayload = new StringSource(
            "<clientDataRequest xmlns='http://www.xpadro.spring.samples.com/orders' " +
            "clientId='456' productId='XA-55' quantity='5'/>");
        
        Source responsePayload = new StringSource(
            "<SOAP-ENV:Fault xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/'>" +
            "<faultcode>SOAP-ENV:Server</faultcode><faultstring xml:lang='en'>Client [456] not found</faultstring></SOAP-ENV:Fault>");
        
        RequestCreator creator = RequestCreators.withPayload(requestPayload);
        mockClient = MockWebServiceClient.createClient(context);
        mockClient.sendRequest(creator).andExpect(ResponseMatchers.payload(responsePayload));
    }
}

 

The configuration file used on this test is pretty simple, just contains scanning of the service components:

<context:component-scan base-package="xpadro.spring.ws"/>
<ws:annotation-driven/>

 

4 Additional information

4.1 Implementing a client

To facilitate the client to access the web service, Spring provides us with the WebServiceTemplate class. This class contains methods for sending and receiving messages and it also uses converters to (un)marshal objects.

I’ve created a test that acts as a client of the service:

@ContextConfiguration("classpath:xpadro/spring/ws/test/config/client-config.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class TestClient {
    @Autowired 
    WebServiceTemplate wsTemplate;
    
    @Test
    public void invokeOrderService() throws Exception {
        ClientDataRequest request = new ClientDataRequest();
        request.setClientId("123");
        request.setProductId("XA-55");
        request.setQuantity(new BigInteger("5", 10));
        
        ClientDataResponse response = (ClientDataResponse) wsTemplate.marshalSendAndReceive(request);
        
        assertNotNull(response);
        assertEquals(new BigDecimal("55.99"), response.getAmount());
        assertEquals("GHKG34L", response.getConfirmationId());
    }
}

 

The configuration test file contains WebServiceTemplate configuration:

<oxm:jaxb2-marshaller id="marshaller" contextPath="xpadro.spring.ws.types"/>

<bean class="org.springframework.ws.client.core.WebServiceTemplate">
    <property name="marshaller" ref="marshaller" />
    <property name="unmarshaller" ref="marshaller" />
    <property name="defaultUri" value="http://localhost:8081/spring-ws/orders" /> 
</bean>

 

Just remember to start the server with the deployed web service application before executing this test.

4.2 How it works internally

If you just want to implement a web service, the article finished in the previous section. For those curious about how this really works, I will try to explain how a request is mapped to the endpoint, just a little more low-level than explained until this point.

When a request arrives to the MessageDispatcher, it relies on two components:

  1. It asks the EndpointMapping which is the appropriate endpoint.
  2. With the information received from the mapping it uses an endpoint adapter to invoke the endpoint. The adapter also support argument resolvers and return type handlers.

 

Endpoint mapping

MessageDispatcher contains a list of endpoint mappings, each of them, containing a map of previously registered method endpoints. In our case, the JAXB mapping PayloadRootAnnotationMethodEndpointMapping has registered all methods annotated with @PayloadRoot. If the qualified name of the payload of the message resolves as a registered method, it will be returned to the MessageDispatcher. If we didn’t annotate our method it would fail to process the request.

endpoint mapping diagram

Endpoint adapter

MessageDispatcher will then ask each of its endpoint adapters if it supports the current request. In our case, the adapter checks if the following conditions are both true:

If an adapter is returned, it will then invoke the endpoint, unmarshalling the parameter before passing it to the method. When the method returns a response, the adapter will marshal it.

The following diagram is a much reduced version of this step in order to keep it simple:

endpoint adapter diagram

Conclusion

We’ve seen an introduction on how to implement a simple web service and then test it. If you are interested, you can also take a look at how to test the client-side with MockWebServiceServer.