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:
- Write a WSDL and use JAX-WS to generate a Java interface for you
to implement.
- 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:
- A Windows XP laptop
- cygwin and Xemacs
- JDK 1.5
- 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:
- Set JAXWS_HOME to the JAX-WS installation directory.
- Go to the
samples/fromjava
directory.
- 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
}
}
- Start another shell to run the client.
- First test the server by requesting the WSDL:
$ wget http://localhost:8080/jaxws-fromjava/addnumbers?wsdl
- 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:
- Download and run the
tcpmon
:
$ java -cp axis.jar org.apache.axis.utils.tcpmon 8081 localhost 8080
- 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
.
- 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.