wikipedia

Support Wikipedia

Wednesday, July 14, 2010

SOAP vs REST in Java land

SOAP and REST are the two most popular web service technologies in use today. REST has become the more preferred one, since it deals directly with URIs and can handle requests in plain text directly over HTTP. Testing it with a browser or tools like curl is super easy.

Let's go through some examples here. Since our focus is on the Java landscape.All the examples are in the Java environment.
The development environment I used is eclipse 3.5, Ubuntu 10.04 LTS the Lucid Lynx, JDK 1.16.20

With Java annotations kicking off in big style with Java 5. Now the coolest way to develop a web service is by using POJOs and some sprinkling of annotations here and there.

Let's start with a simple Java class (POJO) and make a web service out of it!


Let's take the same Java class (CalcWSImpl.java and Calculator.java are pretty much identical except for the annotations) and setup a web service using SOAP as well as REST.

Developing a SOAP based web service

There are many nice tools available today bundled with the JRE like wsimport, wsgen to generate web service client source code and wrapper classes to build a web service.

We start with a simple class Calculator.java. see the listing below.







This class already has the following JAX-WS annotations...

@WebService - to identify itself as an endpoint class.

@SOAPBinding(style=SOAPBinding.Style.RPC) – style for messages used in a webservice.

@WebMethod – to expose the method as a webservice operation.

For a complete list of all the JAX-WS annotations refer to JAX-WS annotations
Just with these annotations , we are ready to expose this class through a web service.

Take a look at TestCalc.java, which launches a web service.

package org.webservice.server;

import javax.xml.ws.Endpoint;

public class TestCalc {

        public static void main(String[] args) {
            Calculator calcWS = new Calculator();
            
            Endpoint.publish("http://localhost:8085/calc", calcWS);
        }

}

Run it and you have a running webservice at http://localhost:8085/calc.
The wsdl is available at http://localhost:8085/calc?wsdl

This is what the generated .wsdl looks like.

<?xml version="1.0" encoding="UTF-8"?><!-- Published by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is JAX-WS RI 2.1.6 in JDK 6. --><!-- Generated by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is JAX-WS RI 2.1.6 in JDK 6. --><definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://server.webservice.org/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://server.webservice.org/" name="CalcWS">
<types></types>
<message name="div">
<part name="arg0" type="xsd:int"></part>
<part name="arg1" type="xsd:int"></part>
</message>
<message name="divResponse">
<part name="return" type="xsd:float"></part>
</message>
<message name="add">
<part name="arg0" type="xsd:int"></part>
<part name="arg1" type="xsd:int"></part>
</message>
<message name="addResponse">
<part name="return" type="xsd:int"></part>
</message>
<message name="list"></message>
<message name="listResponse">
<part name="return" type="xsd:int"></part>
</message>
<portType name="Calc">
<operation name="div" parameterOrder="arg0 arg1">
<input message="tns:div"></input>
<output message="tns:divResponse"></output>
</operation>
<operation name="add" parameterOrder="arg0 arg1">
<input message="tns:add"></input>
<output message="tns:addResponse"></output>
</operation>
<operation name="list">
<input message="tns:list"></input>
<output message="tns:listResponse"></output>
</operation>
</portType>
<binding name="CalcWSPortBinding" type="tns:Calc">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="rpc"></soap:binding>
<operation name="div">
<soap:operation soapAction=""></soap:operation>
<input>
<soap:body use="literal" namespace="http://server.webservice.org/"></soap:body>
</input>
<output>
<soap:body use="literal" namespace="http://server.webservice.org/"></soap:body>
</output>
</operation>
<operation name="add">
<soap:operation soapAction=""></soap:operation>
<input>
<soap:body use="literal" namespace="http://server.webservice.org/"></soap:body>
</input>
<output>
<soap:body use="literal" namespace="http://server.webservice.org/"></soap:body>
</output>
</operation>
<operation name="list">
<soap:operation soapAction=""></soap:operation>
<input>
<soap:body use="literal" namespace="http://server.webservice.org/"></soap:body>
</input>
<output>
<soap:body use="literal" namespace="http://server.webservice.org/"></soap:body>
</output>
</operation>
</binding>
<service name="CalcWS">
<port name="CalcWSPort" binding="tns:CalcWSPortBinding">
<soap:address location="http://localhost:8085/calc"></soap:address>
</port>
</service>
</definitions> 




Now we are ready to test the newly created web service. Either you can go use the free SOAP clients available on the Internet like soap client or tools like wsimport (that comes with JDK) to generate wrapper classes to test a web service endpoint.

Now using the live web service and wsimport tool, let's generate some client classes. Run the following command from the root folder of the project.
wsimport -d bin -s test http://localhost:8085/calc?wsdl

The last step creates an interface Calc.....


package org.webservice.server;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;


/**
 * This class was generated by the JAX-WS RI.
 * JAX-WS RI 2.1.6 in JDK 6
 * Generated source version: 2.1
 * 
 */
@WebService(name = "Calc", targetNamespace = "http://server.webservice.org/")
@SOAPBinding(style = SOAPBinding.Style.RPC)
public interface Calc {


    /**
     * 
     * @param arg1
     * @param arg0
     * @return
     *     returns float
     */
    @WebMethod
    @WebResult(partName = "return")
    public float div(
        @WebParam(name = "arg0", partName = "arg0")
        int arg0,
        @WebParam(name = "arg1", partName = "arg1")
        int arg1);

    /**
     * 
     * @param arg1
     * @param arg0
     * @return
     *     returns int
     */
    @WebMethod
    @WebResult(partName = "return")
    public int add(
        @WebParam(name = "arg0", partName = "arg0")
        int arg0,
        @WebParam(name = "arg1", partName = "arg1")
        int arg1);

    /**
     * 
     * @return
     *     returns int
     */
    @WebMethod
    @WebResult(partName = "return")
    public int list();

}
 

.....and a service class CalcWS


package org.webservice.server;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.logging.Logger;
import javax.xml.namespace.QName;
import javax.xml.ws.Service;
import javax.xml.ws.WebEndpoint;
import javax.xml.ws.WebServiceClient;
import javax.xml.ws.WebServiceFeature;


/**
 * This class was generated by the JAX-WS RI.
 * JAX-WS RI 2.1.6 in JDK 6
 * Generated source version: 2.1
 * 
 */
@WebServiceClient(name = "CalcWS", targetNamespace = "http://server.webservice.org/", wsdlLocation = "http://localhost:8085/calc?wsdl")
public class CalcWS
    extends Service
{

    private final static URL CALCWS_WSDL_LOCATION;
    private final static Logger logger = Logger.getLogger(org.webservice.server.CalcWS.class.getName());

    static {
        URL url = null;
        try {
            URL baseUrl;
            baseUrl = org.webservice.server.CalcWS.class.getResource(".");
            url = new URL(baseUrl, "http://localhost:8085/calc?wsdl");
        } catch (MalformedURLException e) {
            logger.warning("Failed to create URL for the wsdl Location: 'http://localhost:8085/calc?wsdl', retrying as a local file");
            logger.warning(e.getMessage());
        }
        CALCWS_WSDL_LOCATION = url;
    }

    public CalcWS(URL wsdlLocation, QName serviceName) {
        super(wsdlLocation, serviceName);
    }

    public CalcWS() {
        super(CALCWS_WSDL_LOCATION, new QName("http://server.webservice.org/", "CalcWS"));
    }

    /**
     * 
     * @return
     *     returns Calc
     */
    @WebEndpoint(name = "CalcWSPort")
    public Calc getCalcWSPort() {
        return super.getPort(new QName("http://server.webservice.org/", "CalcWSPort"), Calc.class);
    }

    /**
     * 
     * @param features
     *     A list of {@link javax.xml.ws.WebServiceFeature} to configure on the proxy.  Supported features not in the features parameter will have their default values.
     * @return
     *     returns Calc
     */
    @WebEndpoint(name = "CalcWSPort")
    public Calc getCalcWSPort(WebServiceFeature... features) {
        return super.getPort(new QName("http://server.webservice.org/", "CalcWSPort"), Calc.class, features);
    }

}
 

Now let's run the test client CalcWSClient

package org.webservice.server;

import javax.xml.ws.WebServiceRef;


public class CalcWSClient {
      @WebServiceRef(wsdlLocation="http://localhost:8085/calc?wsdl")
      static CalcWS service = new CalcWS();

      public static void main(String[] args) {
        try {
            CalcWSClient client = new CalcWSClient();
          client.doTest(args);
        } catch(Exception e) {
          e.printStackTrace();
        }
      }

      public void doTest(String[] args) {
        try {
          System.out.println("Retrieving the port from the following service: " + service);
          Calc port = service.getCalcWSPort();
          System.out.println("Invoking the add operation on the port.");

          int response = port.add(1, 3);
          System.out.println(response);
        } catch(Exception e) {
          e.printStackTrace();
        }
      }
} 

The picture below shows the results on eclipse console.

 

 

 

 

 

 

 

 

 

 

 

Developing a RESTful web service


Using JAX-RS the API for REST based web service. There are a couple frameworks out there. The popular ones are Jersey and Apache CXF. This example was tested using Jersey 1.2 (you can download from jersey), the Sun's open source implementation.
Just with few annotations and some associated classes, Jersey lets you expose POJOs as web services. It also provides support for JSON.
You can find all the literature and code at jersey docs.

All the annotations are defined in jsr311-api.jar.
Here is a nice article about implementing REST in java. So in short RESTful architecture is about exposing resources and it's operations.
A resource class is a Java class with JAX-RS annotations to identify itself as a web resource.
The Root resource class used here is CalcWSImpl.java. It has three resource methods, add, div and list.
The code listing is shown below.

package org.rest.examples;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;

import org.json.JSONException;
import org.json.JSONObject;

//The Java class will be hosted at the URI path "/calc"
@Path("/calc/")
public class CalcWSImpl {
    // The Java method will process HTTP GET requests
    @GET 
    @Path("add/{input1}/{input2}")
    // The Java method will produce content identified by the MIME Media
    // type "text/plain"
    @Produces("text/plain")
    public String add(@PathParam("input1") int a, @PathParam("input2") int b) {
        return String.valueOf(a+b);
    }
    
    @GET
    @Path("list/")
    @Produces("application/json")
    public String list() throws JSONException {
        JSONObject list = new JSONObject();
        list.put("mercedes", "20");
        list.put("porsche", "25");
        list.put("audi", "32");
        list.put("lexus", "35");
        return list.toString();
    }
    
    // The Java method will process HTTP GET requests
    @GET 
    @Path("div/{input1}/{input2}")
    // The Java method will produce content identified by the MIME Media
    // type "text/plain"
    @Produces("text/plain")
    public String div (@PathParam("input1") int a, @PathParam("input2") int b) {
        return String.valueOf(a/b);
    }
}


Some brief explanations of the annotations used...
@Path the URI path for a class or method. This is path you add to the base URL for the webservice to access the resource. For example http://localhost:9998/calc/list
@GET method will process HTTP GET methods
@Produces MIME media type of objects returned by a method
@PathParam parameters in the URI path like in the following URI http://localhost:9998/calc/add/6/7/
6 and 7 are parameters as you can see from the @Path annotation add/{input1}/{input2} for the add method.
To find all the JSR-311 annotations refer to JAX-RS annotations
Now to get going we would need the following jars
asm-3.1.jar
jersey-bundle-1.2.jar
jsr311-api-1.11.jar
json.jar
You can download them from here.


We are ready to launch the webservice. Let's look at Main.java 

package org.rest.examples;

import com.sun.net.httpserver.HttpServer;
import com.sun.jersey.api.container.httpserver.HttpServerFactory;
import java.io.IOException;

public class Main {
    
    public static void main(String[] args) throws IOException {
        HttpServer server = HttpServerFactory.create("http://localhost:9998/");
        server.start();
        
        System.out.println("Server running");
        System.out.println("Visit: http://localhost:9998/list");
        System.out.println("Hit return to stop...");
        System.in.read();
        System.out.println("Stopping server");   
        server.stop(0);
        System.out.println("Server stopped");
    }
    
    
} 
Run Main.java from eclipse as a java application. You have a running web service.

Let's try some examples here using curl.
~$ curl http://localhost:9998/calc/add/6/7/
13
 ~$

~$ curl http://localhost:9998/calc/list
{"lexus":"35","audi":"32","porsche":"25","mercedes":"20"}
~$ 
 
Deploying...
Let's deploy to a servlet container other than Tomcat :-) , Jetty for instance.
here is what the web.xml should look like.

 

<?xml
version="1.0"
encoding="UTF-8"?>



<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>JerseyTest</servlet-name>
<servlet-class>
com.sun.jersey.spi.container.servlet.ServletContainer
</servlet-class>
<init-param>
<param-name>com.sun.jersey.config.property.packages</param-name>
<param-value>org.rest.examples</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>JerseyTest</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
With jersey configured as a servlet.
All we are doing here is providing the package name (“ org.rest.examples”) which has resource classes.
We need another class to instantiate Jetty! Yes we are going to start an embedded instance programmatically. Here is RunServlet.java

package org.rest.examples;

import org.mortbay.jetty.Server;
import org.mortbay.jetty.webapp.WebAppContext;

public class RunServlet {

    public static void main(String[] args) throws Exception {
        Server server = new Server(8081);
        
        WebAppContext context = new WebAppContext();
        context.setParentLoaderPriority(true);
        context.setDescriptor("./src/web/WEB-INF/web.xml");
        context.setResourceBase("./src/web");
        
        server.setHandler(context);
        
        server.start();
        server.join();
        
    }

} 

Need the following Jetty jars to be in the classpath.
jetty-6.1.18.jar
jetty-util-6.1.1.18.jar
servlet-api-2.5-20081211.jar

On running RunServlet, see the console output below. The JerseyTest servlet scans the package org.rest.examples and finds HelloWorld and CalcWSImpl resource classes.


Now let's see how it works.

All you need is just a few jars and resource classes and you are done creating a RESTful web service! Couldn't be easier than this.

Conclusions:
As you can see, with frameworks like Jersey writing/deploying/testing REST based web services is much simpler and straight forward than it's SOAP equivalent. But of course SOAP has many more features that you may or may not need. So depending on your internal computing environment and external client needs you can choose SOAP or put everything to REST!

6 comments:

  1. Actually, with annotations, they both look pretty easy. I would say the choice should be based on your requirements, but as a rule of thumb:
    * If your services are dominated by verbs (e.g. add, process, approve), SOAP is more natural
    * If your services are dominated by nouns (e.g. person, car, account) REST is more natural

    ReplyDelete
  2. Hi, I've followed you example for the REST service with Jetty and when running the HTTPServer, and doing a request, curl hangs waiting for a response. Any idea?
    Thanks. Great post.

    ReplyDelete
  3. Sorry, I missed a line.

    ReplyDelete
  4. Do you have any idea how to use the Java SE built-in HTTP server to serve both SOAP and REST from the same port?
    E.g. SOAP at http://blah:80/service/ and REST at http://blah:80/resources/

    ReplyDelete
  5. I will give it a try. As long as the URLs are different for SOAP and REST endpoints, I don't think there should be any problems.

    ReplyDelete
  6. Thanks for the post. Question: How would you deploy this to Jetty without creating an embedded instance, i.e., using .../bin/jetty.sh start ?

    ReplyDelete