Sunday, September 17, 2006

JAX-WS 2.0

This is a walkthrough of my first experience with the Java API for XML Web Services 2.0 (JAX-WS 2.0).

My objective was to create a web service with a single method that returns the groups to which a member belongs. JAX-WS gives you two basic ways to build a web service:

  1. Write a WSDL and use JAX-WS to generate a Java interface for you to implement.
  2. Write a set of Java classes and use annotations to control how JAX-WS maps your code to a web service.

I opted for the second approach. It was appealing to think that I could just write some code and turn it into a web service simply by adding some annotation.

I expected my web service to receive requests that look like this:

<?xml version="1.0" ?>
  <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
                    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
                    xmlns:ns1="http://xocoatl.blogspot.com/schema/1/">
    <soapenv:Body>
      <ns1:MemberGroups>
        <MemberID>2</MemberID>
      </ns1:MemberGroups>
    </soapenv:Body>
</soapenv:Envelope>

I expected my web service to send responses that look like this:

<?xml version="1.0" ?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
                  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
                  xmlns:ns1="http://xocoatl.blogspot.com/schema/1/">
  <soapenv:Body>
    <ns1:MemberGroupsResponse>
      <MemberGroup>
        <ID>1</ID>
        <name>Orcs</name>
      </MemberGroup>
      <MemberGroup>
        <ID>2</ID>
        <name>Elves</name>
      </MemberGroup>
    </ns1:MemberGroupsResponse>
  </soapenv:Body>
</soapenv:Envelope>

To complicate matters slightly, I wanted to run the web service in Resin, rather than Tomcat or the Sun Java Application Server.

My development environment includes:

  1. A Windows XP laptop
  2. cygwin and Xemacs
  3. JDK 1.5
  4. ant

I do not use an IDE, so I needed to be able to code my web service in emacs and deploy it from the command line.

Getting Started

I downloaded the JAX-WS 2.0 reference implementation and expanded it.

The samples directory contains a number of good examples. I was particularly interested the fromjava-* examples, including supplychain.

The documentation does not explain how to run the fromjava samples from the command line. Here is what I figured out:

  1. Set JAXWS_HOME to the JAX-WS installation directory.
  2. Go to the samples/fromjava directory.
  3. Build and run the web service using the server-j2se task:
    $ ant server-j2se
    Buildfile: build.xml
    Trying to override old definition of task apt
    
    setup:
    
    server-j2se:
    
    clean:
       [delete] Deleting directory C:\Docs\jaxws-ri\samples\fromjava\build
    
    setup:
        [mkdir] Created dir: C:\Docs\jaxws-ri\samples\fromjava\build
        [mkdir] Created dir: C:\Docs\jaxws-ri\samples\fromjava\build\classes
        [mkdir] Created dir: C:\Docs\jaxws-ri\samples\fromjava\build\war
    
    build-server-java:
          [apt] warning: Annotation types without processors: [javax.xml.bind.annotation.XmlRootElement, javax.xml.bind.annotation.XmlAccessorType, javax.xml.bind.annotation.XmlType, javax.xml.bind.annotation.XmlElement]
          [apt] 1 warning
         [echo] Starting endpoint... To stop: ant server-j2se-stop 
    
    BUILD SUCCESSFUL
    Total time: 6 seconds
    $ 

    The service runs fine despite the annotation warning, but it exits immediately. To work around this I simply put the main thread to sleep for a few minutes after starting the server:

    public class AddWebservice {
        
        public static void main (String[] args) throws Exception {
            Endpoint endpoint = Endpoint.publish (
                "http://localhost:8080/jaxws-fromjava/addnumbers",
                new AddNumbersImpl ());
    
            // Stops the endpoint if it receives request http://localhost:9090/stop
            new EndpointStopper(9090, endpoint);
    
     Thread.sleep(60000); // Addded to keep the JVM from exiting
        }
    }
  4. Start another shell to run the client.
  5. First test the server by requesting the WSDL:
    $ wget http://localhost:8080/jaxws-fromjava/addnumbers?wsdl
  6. Now build and run the client:
    $ ant client run

    The client is built by requesting the WSDL from the server and then generating a corresponding set of JavaBean classes for working with requests and responses.

Once the sample seemed to be running, I used the tcpmon tool from the Apache Axis project to intercept and read the actual SOAP messages between server and client:

  1. Download and run the tcpmon:
    $ java -cp axis.jar org.apache.axis.utils.tcpmon 8081 localhost 8080
  2. Go to the samples/fromjava/etc directory and change the port from 8080 to 8081 in build.properties, custom-client.xml and custom-schema.xml.
  3. Make sure the server is still running and then run the client again.

Implementing the Service

The supplychain sample seemed closest to what I wanted, so I copied that project as the basis for my experiment.

I was not able to get the SOAP messages to come out the way I wanted by modifying the code and annotations from the supplychain sample.

Finally I created a new project and imported the WSDL for the Amazon Simple Queue Service, since it has messages with the desired structure.

By studying the code generated from the Amazon SQS WSDL I was ultimately able to get what I wanted with only a few lines of code:

package member.server;

import java.math.BigInteger;

import java.util.ArrayList;
import java.util.List;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebParam.Mode;
import javax.jws.WebService;
import javax.xml.ws.Holder;
import javax.xml.ws.RequestWrapper;
import javax.xml.ws.ResponseWrapper;

@WebService(name="MemberService", 
     targetNamespace = "http://xocoatl.blogspot.com/schema/1/")
public class MemberService {

  private static final long INVALID_ID = 1L;

  @WebMethod(operationName = "MemberGroups", action = "MemberGroups")
  public void getMembers(@WebParam(name = "MemberID")
    BigInteger memberID,
    @WebParam(name = "MemberGroup",
       mode = Mode.OUT)
    Holder<List<MemberGroup>> memberGroups) 
    throws InvalidMemberIDException {

    if (memberID.equals(BigInteger.valueOf(INVALID_ID))) {
      throw new InvalidMemberIDException("Invalid member ID");
    }

    memberGroups.value = new ArrayList();
    
    memberGroups.value.add(new MemberGroup(1, "Steelers"));
    memberGroups.value.add(new MemberGroup(1, "49ers"));
  }
}

MemberGroup is a simple unadorned JavaBean:

package member.server;

public class MemberGroup {
  
  private String name;
  private Integer id;

  public MemberGroup() {}

  public MemberGroup(Integer id, String name) {
    setID(id);
    setName(name);
  }
    
  public String getName() { 
    return name; 
  }

  public void setName(String name) { 
    this.name = name; 
  }
  
  public Integer getID() { 
    return id; 
  }

  public void setID(Integer id) { 
    this.id = id; 
  }
    
}

Note that the service method returns void and has an OUT parameter.

Deploying to Resin

For the final step, I generated the war file containing the service implementation:

$ ant clean build-server-java create-war

I copied the resulting war file to a fresh Resin instance with an empty webapps directory, and modified the server configuration to run my service as the root application:

<host id="" root-directory=".">
  <web-app id="/" document-directory="webapps/jaxws-member"/>
</host>

After a series of NoClassDefFoundError exceptions, I determined that the following jar files must be copied to the server lib directory:

  • jaxws-api.jar
  • jaxws-rt.jar
  • jsr173-api.jar
  • jsr181-api.jar
  • jsr250-api.jar
  • resolver.jar
  • saaj-api.jar
  • saaj-impl.jar
  • jaxb-api.jar
  • jaxb-impl.jar

Once that was sorted out, the web service ran fine.

1 comment:

Unknown said...

Hi,

This article really helps!! I successfully deploy the "fromjava" example on Resin. However I got one error while sending a request from client:

run:
[java] Invoking addNumbers(10, 20)
[java] Exception in thread "main" XML reader error: javax.xml.stream.XMLStreamException: ParseError at [row,col]:[1,1]
[java] Message: Premature end of file.
................

Have any idea about this??

Thank you,
Alex