Follow my blog with Bloglovin

Saturday, December 31, 2011

Spring Web Services 2.0 step-by-step explained end-to-end

Spring Web Services 2.0 Step by Step Publishing Web Service and client for the web service.

Here, we will crate a web service using contract first approach. In contract first approach we first finalize the contact (WSDL, message types for request/response), so the interface is clear and is exact what is required and less likely to change and implementation detail (Java classes and interface) at last. For more on contract first approach http://static.springsource.org/spring-ws/site/reference/html/why-contract-first.html.

Envirenment:
Eclipse 3.6 JEE
JDK 1.6
Spring WS 2.0.3
Spring framework 3.1
Apache Tomact 6.0.26

Step #1 Download Spring Web Service release 2.0.3 and its dependencies Spring framework 3 higher from
www.springsource.org/download. This also requires Java 5 or higher.

Unzip both downloaded files. You can find library jars in following locations:
<Unzip_Dir>\spring-ws-2.0.3.RELEASE\dist\ and in modules directory of spring web service distribution and in <Unzip_Dir>\spring-framework-3.1.0.M1\dist directory of spring framework distribution. Exact location may change depending on distribution but can be easily located. These jars need to be copied in project we will create in next step and also while deploying in server.

Step #2 In eclipse create a dynamic web project "SpringWS" with all default provided by eclipse.

Step #3 Finalize the contract (interface and message exchange structure).

We do not have to create a WSDL, this will be generated by Spring using the XSD schema and configuration (will be discussed later).

For creating the schema for request and response consider a usecase in which we will send a complex object containing some order detail in request and service will return price and other information in response.

The XSD schema will look like this:

<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.springwssample.org/types" xmlns:tns="http://www.springwssample.org/types" elementFormDefault="qualified">
<element name="orderRequest">
  <complexType>
    <sequence>
      <element name="item" type="string"/>
      <element name="quantity" type="integer"/>
      <element name="city" type="string"/>
      <element name="country" type="string"/>
    </sequence>
  </complexType>
</element>

<element name="orderResponse">
  <complexType>
    <sequence>
      <element name="item" type="string"/>
      <element name="quantity" type="string"/>
      <element name="city" type="string"/>
      <element name="country" type="string"/>
      <element name="price" type="double"/>
      <element name="isDeliver" type="boolean"/>
    </sequence>
  </complexType>
</element>
</schema>

Step #4 Create an endpoint to process requests and provide response. This is the implementation for the web service.

Create a Java class "OrderEndpoint" in the project created earlier in package "springws.usecase". No need to extend any class or implement any interface.

Annotate the class with @Endpoint annotation. This will make the class able to process XML messages and will be available for Spring component scanning.

Create a method "processOrder" to process order request. Annotate it with @PayloadRoot to tell Spring that this method can handle XML requests of type specified by parameters of the annotation.

@PayloadRoot(localPart = "echoRequest", namespace = "http://www.springwssample.org/types")
@ResponsePayload
public Element processOrder(@RequestPayload Element request){

}

In the method body write the processing need. This may require database access or call to some other services or some validation and may throw an exception. For simplicity we will make dummy implementation. Here is the complete class:

package springws.usecase;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

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;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

@Endpoint
public class OrderEndpoint {

    private static final String NAMESPACE = "http://www.springwssample.org/types";
   
    @PayloadRoot(localPart = "orderRequest", namespace = "http://www.springwssample.org/types")
    @ResponsePayload
    public Element processOrder(@RequestPayload Element request) throws ParserConfigurationException {
        Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
        Element response = document.createElementNS(NAMESPACE, "orderResponse");
        response.appendChild(addElementWithValue(document, "item", "Item1"));
        response.appendChild(addElementWithValue(document, "quantity", "2"));
        response.appendChild(addElementWithValue(document, "city", "Ahmedabad"));
        response.appendChild(addElementWithValue(document, "country", "India"));
        response.appendChild(addElementWithValue(document, "price", "200.00"));
        response.appendChild(addElementWithValue(document, "isDeliver", "true"));
        return response;
    }
   
    private Element addElementWithValue(Document document, String element, String value){
        Element child = document.createElementNS(NAMESPACE, element);
        child.appendChild(document.createTextNode(value));
        return child;
    }
}

Step #5 AddMessageDispatcherServlet mapping in web.xml and define all incomming request mapping to this  servlet. Make this entry in web.xml as:

<display-name>My Service</display-name>
    <servlet>
        <servlet-name>spring-ws</servlet-name>
        <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
        <init-param>
            <param-name>transformWsdlLocations</param-name>
            <param-value>true</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>spring-ws</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

Step #6 Create Spring WS specific configuration for this servlet which contains dynamic WSDL generation configuration, interceptors, Spring WS specific beans etc. Create a spring-ws-servlet.xml (<servlet name>-servlet.xml) file as:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:sws="http://www.springframework.org/schema/web-services"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
       http://www.springframework.org/schema/web-services http://www.springframework.org/schema/web-services/web-services-2.0.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <description>
        This web application context contains Spring-WS beans. The beans defined in this context are automatically
        detected by Spring-WS, similar to the way Controllers are picked up in Spring Web MVC.
    </description>

    <context:component-scan base-package="springws.usecase"/>

    <sws:annotation-driven />

    <sws:interceptors>
        <bean class="org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor">
            <description>
                This interceptor validates both incoming and outgoing message contents according to the 'echo.xsd' XML
                Schema file.
            </description>
            <property name="schema" value="/WEB-INF/service.xsd"/>
            <property name="validateRequest" value="true"/>
            <property name="validateResponse" value="true"/>
        </bean>
        <bean class="org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor">
            <description>
                This interceptor logs the message payload.
            </description>
        </bean>
    </sws:interceptors>

    <sws:dynamic-wsdl id="order" portTypeName="Order" locationUri="orderservices">
        <sws:xsd location="/WEB-INF/service.xsd"/>
    </sws:dynamic-wsdl>
</beans>


The dynamic WSDL configuration:

    <sws:dynamic-wsdl id="order" portTypeName="Echo" locationUri="orderservices">
        <sws:xsd location="/WEB-INF/service.xsd"/>
    </sws:dynamic-wsdl>

 will publish the WSDL at http://<host>:<port>/<web app context>/orderservices/order.wsdl.
To enable this to work on different app context "transformWsdlLocations" init param is added to the servlet.


 Step #7 Build project and deploy in tomcat server.
The project structure in eclipse is like this:

An automated build script to create .war can be created to build project or for simplicity just create a directory "classes" under "WEB-INF" directory and add compiled class file OrderEndpoint.class into it. The class file can be found in build directory. Copy the content of "build/classes" directory into classes folder.

Then create a .war file from the contents of "WebContent" directory or just copy this directory into Tomcat's "webapps" directory.

You need to put following jars in WEB-INF/lib directory :

Additionally put commons logging and log4j jars.

Start tomcat and browse http://localhost:8080/WebContent/orderservices/order.wsdl to see the WSDL published.

Step #8 Create a Web Service Client for order service using WebServiceTemplate.

You can create a new project or can use the project created earlier. For simplicity I am using same project.
Create a class Client.java in springws.usecase package and put the following code in it:

package springws.usecase;


import java.io.StringReader;

import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.springframework.ws.client.core.WebServiceTemplate;

public class Client {



//sample request XML
    private static String MESSAGE =
        "<orderRequest xmlns=\"http://www.springwssample.org/types\"><item>Spring Flowers</item><quantity>2</quantity><city>Ahmedabad</city><country>India</country></orderRequest>";

    public static void main(String[] args){
        StreamSource source = new StreamSource(new StringReader(MESSAGE));
        StreamResult result = new StreamResult(System.out);

//WebServiceTemplate provides the functionality for sending and receiving webservice messages.
        WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
        webServiceTemplate.sendSourceAndReceiveToResult("http://localhost:8080/WebContent/orderservices",
                source, result);

    }
}

Run this client and following message will appear on console:

<?xml version="1.0" encoding="UTF-8"?><orderResponse xmlns="http://www.springwssample.org/types"><item>Item1</item><quantity>2</quantity><city>Ahmedabad</city><country>India</country><price>200.00</price><isDeliver>true</isDeliver></orderResponse>


In this example client has hard-coded messages, but if in an application where it is created dynamically we may need to process message before sending. For this we can use WebServiceTemplate#sendSourceAndReceiveToResult method with WebServiceMessageCallback as argument. This class has one method
public void doWithMessage(WebServiceMessage arg0)    throws IOException, TransformerException
that need to be implemented and gives handle to the WebSeviceMessage before sending.


See the response and request, as we are sending and receiving raw XML we can choose our own marshalling/unmarshalling technology like JAXB, XStream, JDOM, DOM4J or Axiom. In the future posts I will explain how to use Axiom to create message and handle response received. As Axiom is based on pull parsing so it is efficient for handling large XML messages. This is developed along with Axis2.

Troubleshooting:

 org.springframework.ws.client.WebServiceTransportException: Not Found [404]
    at org.springframework.ws.client.core.WebServiceTemplate.handleError(WebServiceTemplate.java:663)
    at org.springframework.ws.client.core.WebServiceTemplate.doSendAndReceive(WebServiceTemplate.java:587)
    at org.springframework.ws.client.core.WebServiceTemplate.sendAndReceive(WebServiceTemplate.java:537)
    at org.springframework.ws.client.core.WebServiceTemplate.doSendAndReceive(WebServiceTemplate.java:492)
    at org.springframework.ws.client.core.WebServiceTemplate.sendSourceAndReceiveToResult(WebServiceTemplate.java:436)
    at org.springframework.ws.client.core.WebServiceTemplate.sendSourceAndReceiveToResult(WebServiceTemplate.java:421)    

This error may come due to mismatching namespaces decleration or localpart (element name) in WSDL (derived from XSD schema) and in Endpoint class or request sent from client. Check that the payload root method in endpoint for localpart and namespaceuri used these should be element name for request message in schema and namespace of the schema. Also, ensure request sent from client is in same namespace.

org.springframework.ws.soap.client.SoapFaultClientException: Validation error
    at org.springframework.ws.soap.client.core.SoapFaultMessageResolver.resolveFault(SoapFaultMessageResolver.java:37)
    at org.springframework.ws.client.core.WebServiceTemplate.handleFault(WebServiceTemplate.java:774)
    at org.springframework.ws.client.core.WebServiceTemplate.doSendAndReceive(WebServiceTemplate.java:600)
    at org.springframework.ws.client.core.WebServiceTemplate.sendAndReceive(WebServiceTemplate.java:537)
    at org.springframework.ws.client.core.WebServiceTemplate.doSendAndReceive(WebServiceTemplate.java:492)
    at org.springframework.ws.client.core.WebServiceTemplate.sendSourceAndReceiveToResult(WebServiceTemplate.java:436)
    at org.springframework.ws.client.core.WebServiceTemplate.sendSourceAndReceiveToResult(WebServiceTemplate.java:421)

This error is due to request sent or response sent message is not valid XML according to schema. This validation is done by interceptors registered in spring-ws-servlet.xml.


Thats it! we have created a web service and a sample client for it. For any issues with this post contact me at sylentprayer@gmail.com or leave a comment.

Refrences:
Spring WS reference guide.

Get code as eclipse project from here.

14 comments:

  1. Yeah, Its very helpful for the beginners. Really appreciable work, will get in touch with u for further details. Thanks

    ReplyDelete
  2. Hi,

    I deployed the application through Jetty Webserver using the Client.java and getting the below error. Please help me with the correct direction.

    I have followed all the steps mentioned in this post. Please resond to my mail id smshameem@gmail.com

    Thanks,
    Syed

    ReplyDelete
  3. log4j:WARN No appenders could be found for logger (org.springframework.ws.soap.saaj.SaajSoapMessageFactory).
    log4j:WARN Please initialize the log4j system properly.
    Exception in thread "main" org.springframework.ws.client.WebServiceTransportException: Not Found [404]
    at org.springframework.ws.client.core.WebServiceTemplate.handleError(WebServiceTemplate.java:663)
    at org.springframework.ws.client.core.WebServiceTemplate.doSendAndReceive(WebServiceTemplate.java:587)
    at org.springframework.ws.client.core.WebServiceTemplate.sendAndReceive(WebServiceTemplate.java:537)
    at org.springframework.ws.client.core.WebServiceTemplate.doSendAndReceive(WebServiceTemplate.java:492)
    at org.springframework.ws.client.core.WebServiceTemplate.sendSourceAndReceiveToResult(WebServiceTemplate.java:436)
    at org.springframework.ws.client.core.WebServiceTemplate.sendSourceAndReceiveToResult(WebServiceTemplate.java:421)
    at springws.usecase.Client.main(Client.java:25)

    ReplyDelete
  4. Hi,

    I deployed the application through Jetty Webserver using the Client.java and getting the below error. Please help me with the correct direction.

    I have followed all the steps mentioned in this post. Please resond to my mail id smshameem@gmail.com

    Thanks,
    Syed

    log4j:WARN No appenders could be found for logger (org.springframework.ws.soap.saaj.SaajSoapMessageFactory).
    log4j:WARN Please initialize the log4j system properly.
    Exception in thread "main" org.springframework.ws.client.WebServiceTransportException: Not Found [404]
    at org.springframework.ws.client.core.WebServiceTemplate.handleError(WebServiceTemplate.java:663)
    at org.springframework.ws.client.core.WebServiceTemplate.doSendAndReceive(WebServiceTemplate.java:587)
    at org.springframework.ws.client.core.WebServiceTemplate.sendAndReceive(WebServiceTemplate.java:537)
    at org.springframework.ws.client.core.WebServiceTemplate.doSendAndReceive(WebServiceTemplate.java:492)
    at org.springframework.ws.client.core.WebServiceTemplate.sendSourceAndReceiveToResult(WebServiceTemplate.java:436)
    at org.springframework.ws.client.core.WebServiceTemplate.sendSourceAndReceiveToResult(WebServiceTemplate.java:421)
    at springws.usecase.Client.main(Client.java:25)

    ReplyDelete
  5. Hi Syed,

    It seems problem with namespaces as described in troubleshooting. I will examine files and update soon.

    Thanks,

    ReplyDelete
  6. Hi,

    Initially the Prblem is due to the below.

    I found that in WebContent/classes folder classes [OrderEndpoint.class, Client.class] are located in org.springws package and in spring-ws-servlet.xml component scan is enabled for springws.usecase package. So on server startup these classes will not be scanned and hence will not be available.

    You can change the package structure to springws.usecase in classes folder (requires changes in package declaration of OrderEndpoint.java and rebuild) or change the package for component scan. For this change in spring-ws-servlet.xml.

    After changing the Package Structure and rebuild it, When i deploy the application through Tomcat webapps, the WSDL is Published without any error.

    But still i was unable to deploy it through Eclipse. Later i found that the deployment through Tomcat and Deployment through Eclipse differs.

    When we copy the WebContent folder into Tomcat's webapps folder, it deploys the web application with the context root 'WebContent'. Eclipse Deploys the Web application with context root as name of the project. So in this case it is SpringWS.

    After changing the WebContent to SpringWS in the URL, i was able to view the published WSDL. This was the problem and thanks for the help provided by Lalit.

    Thanks

    Syed

    ReplyDelete
  7. So Finally i was able to deploy this spring Web
    services through tomcat and eclipse successfully..

    Lalit,

    I appreciate your step by step help in this regard.

    ReplyDelete
  8. Hey, thanks, your post helped me!

    ReplyDelete
  9. below line it showing the error in client.java file. webServiceTemplate.sendSourceAndReceiveToResult("http://localhost:8080/WebContent/orderservices",source,result);

    Any one can help me please ?

    ReplyDelete
  10. Can you please post the stack trace.

    Also try downloading the source and check.

    ReplyDelete
  11. Cool article! It was very helpful for understanding

    ReplyDelete
  12. I just can't explain how helpful has been this example for me. I just want to add two things that might be useful for other readers:
    1. When running this service in JBoss, in test URL WebContent must be replaced with SpringWS.
    2. When testing from SOAPUI, locationUri located inside spring-ws-servlet must be changed to real value, which is http://localhost:8080/SpringWS/orderservices

    Thanks again!

    ReplyDelete
    Replies
    1. Agreed!!

      Ideally, we should always refer to web app by its name and package it in war by its name. So every where it should be SpringWS.

      As eclipse create a folder name "WebContent" by default, I used it and did not bother to raname, package, war etc.

      Even if we launch this from eclipse it will deploy this with name SpringWS.

      I will make necessary changes to make it consistent for any deployment container or client.

      Thanks for providing feedback for improvement.
      Lalit

      Delete
    2. Just wanted to add that I'm running JBoss from eclipse, using "Run as server".

      Delete

Popular Posts