|
©1994-2003 Kevin Boone |
| Home Section index K-Zone home | Software development |
|
What this tutorial is aboutThis tutorial describes how to create a very simple XML-RPC Web Service, to be hosted using Sun's `Java Web Services Developer Pack' (JWSDP). This will allow one Java class to call methods on another over a network. In effect we will be doing remote method invocation (RMI) or remote procedure calls (RPC) using using HTTP as the carrier and XML as the data format. (Although there are technical differences between RMI and RPC, they aren't important here. I will use the term `RMI' as it is more familiar to Java developers.) Why would you want to do this? One of the substantial problems that exists with RMI in the wide-area (e.g., business-to-business) arena is that it can be difficult to find a platform-independent way to allow distributed method calls to be made without compromising security. As an RMI carrier, Corba's IIOP protocol has broad industry support, but it can be difficult (sometimes impossible) to use it in configurations that require it to pass a firewall. In addition, few vendors support the `common secure interoperability' extensions that allow it to be encrypted and authenticated in a vendor-neutral way.Using XML with HTTP as the carrier goes some way to solving the problems inherent in other RMI schemes. XML has almost universal support in the computing industry, and generation and parsing libraries exist for use in most programming languages for most platforms. HTTP is well-understood, tolerated by firewalls, and has a well-established integration with SSL (secure sockets layer) encryption. Thus XML-RPC is a vendor-independent, platform-independent, language-independent way of doing remote method invocation. You should appreciate that some of the underlying technology in XML-RPC is exceptionally complex. The benefit of developer tools like Sun's JWSDP is that they conceal this complexity, allowing the developer to use standard programming techniques. We shall see that, using JWSDP, the code in both the calling class and the target class is almost identical to that which would be used if they were two classes interacting locally (although, to be fair, I should point out that I have chosen a very simple example that emphasises this similarity). Scope of this tutorialThis tutorial demonstrates the following features of XML-RPC Web Service development:
Readership and prerequisitesThis article is written for reasonably experienced developers, who may already have some experience of distributed programming. In particular, I assume that the reader has a good working knowledge of the `Ant' build tool, the Java language, command-line development tools (javac, etc), and is not entirely baffled by XML.
Contacting the authorI am happy to receive comments on, and questions about, the contents of the article. Please address these to kb@kevinboone.com. However, please don't contact me about:
Abbreviations and terminologyIn what follows, I will use the following abbreviations; please replace them where necessary with whatever is appropriate on your system.
BackgroundWe will be writing two classes that communicate using XML documents carried over HTTP. Of course, we don't want to manipulate the HTTP protocol or the XML in our code, which means we need these three sets of classes as well as the ones we will write ourselves:
The Web server supplied with the JWSDP is Tomcat 4; all the other supporting classes will be generated using a command-line tool called xrpcc supplied with
the JWSDP.
Step 1: install the JWSDPThe Sun JWSDP contains the Tomcat Web server, a pile of supporting classes to do the XML operations, and tools to generate the network-level plumbing. It also contains the Ant build tool. In principle, JWSDP can be added to an existing Tomcat installation, or to the J2EE Reference Implementation. However, such topics are beyond the scope of this article. I will assume that you are working with a new and complete installation of JWSDP. It can be downloaded from the JavaSoft Web site. You may need to upgrade your basic Java installation; JWSDP works only with JVMs compatible with Sun JDK 1.3 or later. You should be able to get a recent JDK at the same place as you got the JWSDP itself. The download is a self-extracting installer; it should only be necessary to run it. On Solaris you will almost certainly need some OS patches as well; the installer will indicate which ones are necessary. On my Solaris 8 (October 2000 release) system I needed a patch for the Motif GUI.The installer will prompt for a username and password for the `manager' application; this will be required later to deploy the Web Service. I have assumed that the username and password are both `manager' for the sake of simplicity. By default the Tomcat Web server that gets installed with JWSDP listens for connections on port 8080; if you have other services on this port (e.g., another installation of Tomcat), change the port number in JWSDP_DIR/conf/server.xml,
but remember to change it in the build configuration file as described below.
The `bin' directory created by the installer contains a script or batch file to run Ant. This script sets the class search path to be appropriate for the build files that are likely to be required for Web Services. In particular, it enables the Tomcat hot deployment tasks. If you already have Ant, you may need to modify its classpath to get the build files in this example to work; alternatively, use the version of Ant that comes with the JWSDP by running it using its full path on the command line. Step 2: get the source code for this tutorialSpace precludes a full listing of all the source files and configuration files in this article; if you want to try it for yourself you will need to get the source code package and unpack it into any handy directory.The files and directories included in the source package are as follows:
Step 3: set up the build environmentEditbuild.properties to reflect your working environment.
build.properties.
Step 4: create and compile the interfaceThe interface forms the bridge between the Web Service and its clients. Creating a Web Service interface is very similar to creating a standard Java RMI interface: it must extendjava.rmi.Remote, and all methods must be declared to
throw RemoteException.
In this simple example, our service is defined by the interface Interest,
in package com.web_tomorrow.xmlrpctut. It exposes one method,
getCompoundInterest. The full source is listed below; this needs to
be in the directory SOURCE_ROOT/src/shared/com/web_tomorrow/xmlrpctut.
package com.web_tomorrow.xmlrpctut;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
Service interface for the `Interest' Web Service
(c)2002 Kevin Boone/Web-Tomorrow
*/
public interface Interest extends Remote
{
public double getCompoundInterest
(double principal, double interest, int terms)
throws RemoteException;
}
The interface is straightforward to compile, as it is plain, standard Java; there is
no XML or RPC stuff in it. So, using command-line tools, and assuming that the
current directory is SOURCE_ROOT:
javac -d build/shared/ src/shared/com/web_tomorrow/xmlrpctut/Interest.javaThis creates the compiled class in build/shared/.... Alternatively, using
Ant, do:
ant compile-sharedwhich should have the same effect. Step 5: create and compile the implementationThe implementation class provides the `flesh' for the `bones' of the interface created in step 4. Again, it is standard Java; there is no XML or RPC stuff in it. The source code should be inSOURCE_ROOT/src/server/com/web_tomorrow/xmlrpctut/InterestImpl.java.
Note that although this class is in a different directory from the interface generated
in step 4, it is in the same Java package. Some developers like to specify different
packages for the interfaces and implementations, but I don't like this. In any event,
if it is in the same package, you can't call this class `Interest' as well, even though
its source code is in a different directory to the interface.
package com.web_tomorrow.xmlrpctut;
/**
Implementation class for the Interest Web Service; must implement
the service interface
(c)2002 Kevin Boone/Web-Tomorrow
*/
public class InterestImpl implements Interest
{
public double getCompoundInterest(double principal,
double interest, int terms)
{
double total = principal;
for (int i = 0; i < terms; i++)
total = total * (1 + interest/100.0);
return total - principal;
}
}
To compile this class, be aware that it refers to the interface Interest, which
we compiled in step 4. The classpath seen by the Java compiler must allow this interface
to be found. So:
javac -d build/server/ -classpath build/shared/ src/server/com/web_tomorrow/xmlrpctut/InterestImpl.java(Please note that I have written this command on two separate lines so as not to upset your Web browser; in practise, of course, it is one long command.) This command creates the compiled class in build/shared/....
Alternatively, do
ant compile-serverwhich also compiles the interface if necessary. So we now have the interface and the implementation compiled; what we need now is to create the server-side plumbing for XML-RPC. Step 5: create server-side plumbingTo create the plumbing (technically, the skeletons and Web Services configuration file), we use thexrpcc that comes with the JWSDP. This takes as its input
a configuration file in XML. A suitable file is listed below:
<?xml version="1.0" encoding="UTF-8"?>
<configuration
xmlns="http://java.sun.com/jax-rpc-ri/xrpcc-config">
<rmi name="InterestService"
targetNamespace="http://hello.org/wsdl"
typeNamespace="http://hello.org/types">
<service name="InterestService"
packageName="com.web_tomorrow.xmlrpctut">
<interface name="com.web_tomorrow.xmlrpctut.Interest"
servantName="com.web_tomorrow.xmlrpctut.InterestImpl"/>
</service>
</rmi>
</configuration>
This file is specific to the Sun JWSDP, and this job may be done differently in different
products; what is important is that the interface definition is product-independent.
In the XML file we specify the name of the service, and the Java package and classes that will
form the interface and implementation.
In this example, the configuration file is just called config.xml, so
run xrpcc like this:
xrpcc.sh -server -d build/server/ -classpath build/shared/ config.xmlOn Windows systems, you need xrpcc.bat rather than xrpcc.sh.
In any case, you can use Ant instead:
ant xrpcc-serverThe `-d' switch to xrpcc tells the tool where to put its output files.
These consist of compiled classes, and configuration files. In this
case we have specified build/server, to align with the compiled implementation
classes. In the end, we will be packaging all these classes into a WAR file for deployment,
so it's convenient if they're in the same directory. xrpcc also
generates two configuration files. InterestService.wsdl is a
`Web Services description language' (WSDL) file. This is a standard format for describing
Web Services in some sort of Services registry. We won't be using this in the current
example. In any event, you would need to modify it to indicate the true URL of your
Service (as it would be seen from the Internet).
The other file, InterestService_Config.properties, is going to be used;
the supporting classes on the Tomcat Web server will read this to determine which classes
provide the implementation of the Service. Later this is going to have to be
moved to a different directory, but we'll deal with this when the time comes.
Step 6: package the server side classes, plumbing and configsThe Tomcat Web server expects to receive applications in `WAR' format, as defined in the J2EE specifications. A WAR file is nothing more than a standard Java JAR archive with a name that ends in `.war', but it has a particular directory structure. For our Web Service we will need the WAR to look like this:
WEB-INF (directory)
web.xml (file)
InterestService_Config.properties (file)
classes (directory)
... implementation, interface, and plumbing classes ...
That is, all the compiled classes must go in subdirectories of WEB-INF/classes.
In principle, the .properties file does not need to go in WEB-INF,
as its location is specified in web.xml (see below), but it's as good a
place as any. Note that this file structure is a J2EE matter, not a Web Services matter.
If your hosting server is not a J2EE-compliant Web server, the deployment process
may be different; it may not even be in the form of WAR files. In addition, if you were
creating your services using, say, a graphical development tool, the entire packaging
and deployment process may be invisible.
Anyway, working with command-line tools we have three jobs to do: create the web.xml file, copy the classes and config files to the
right directories, package into a WAR file.
Step 6a: create web.xmlThe format of this file is a Tomcat/J2EE matter, and a complete description is beyond the scope of this article. The important sections are as follows.
<servlet>
<servlet-name>JAXRPCEndpoint</servlet-name>
<servlet-class>com.sun.xml.rpc.server.http.JAXRPCServlet</servlet-class>
<init-param>
<param-name>configuration.file</param-name>
<param-value>/WEB-INF/InterestService_Config.properties</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>JAXRPCEndpoint</servlet-name>
<url-pattern>/jaxrpc/*</url-pattern>
</servlet-mapping>
The servlet section defines the name of a servlet (JAXRPCEndpoint) and
a class that implements it. The name is arbitrary, and the class is provided with the
JWSDP, so we don't need to know much about it. However, the configuration.file
parameter is important: this references the configuration file that was created by
xrpcc earlier, and this in turn tells the servlet which of your classes
to invoke when a request is received from a client. The servlet-mapping section
tells Tomcat which URL will be specified by clients for this service. Again, this is
arbitrary, but must agree with the URL that the client will specify (we'll see how to
tell the client about this later).
Step 6b: copy files and directoriesWe need to create the directorybuild/WEB-INF and copy in the config
files and the classes in directories build/server and build/shared
that were created in previous steps. This is an operating-system issue, and I will assume
that you know how to copy files on your system. Alternatively, use Ant:
ant setup-web-inf Step 6c: package the WAR fileFinally, we need to get all the server-side stuff into a WAR file. At the command-line, use thejar utility:
jar cf build/InterestService.war -C build WEB-INFAlternatively, use Ant: ant packagewhich does all the preceding steps as well if necessary. Step 7: deploy the serviceThis is where Ant comes in really useful; we can define Ant tasks that reference the Tomcat hot deployment facilities. The alternative way to deploy is to copy the WAR file from step 6c intoJWSDP_DIR/webapps and re-start the
Tomcat server. I will assume that you will want to use the Ant method, which is
much faster. First, make sure the server is running.
Change to the directory JWSDP_DIR, and execute
./bin/startup.sh(startup.bat for Windows systems). To test whether Tomcat is running, point your Web browser at http://localhost:8080/and you should get an welcome page. If not, you need to fix the problem before continuing; everything else is predicated on a working Tomcat. If Tomcat is running, you should be able to deploy by doing: ant installThe `install' task carries out all the preceding steps as well, if necessary. Note that if you have already deployed, you will need to do ant remove installto undeploy the old version first. Step 8: quick test of the deploymentAlthough we want to test the deployment properly using a test client, it is helpful at this stage to do a quick test, and prove that the packaging and deployment were successful. If this test fails, there is no point using a test client: it is bound to fail as well. Point your Web browser at:http://localhost:8080/InterestService/jaxrpcYou should get an message of the form `a Web Service is installed at this URL...'. Any kind of exception represents a failure, and must be resolved. Common problems include badly-formed web.xml, and missing or badly-formed
Web Service configuration file.
Step 9: generate client-side plumbingWe should now be in a position where we can peek at the Web Service using a Web browser, as described in step 8. However, what we really need is a way for other Java classes to call methods on it. Of course, we could code the Java to form the appropriate XML, and then issue it on the HTTP protocol, but this is a drag. What we really want is to hide all this stuff in helper classes (`stubs'), so we get a nice clean Java implementation of the client. Better still, we want the helper classes to be generated automatically. This is wherexrpcc comes
in (again):
xrpcc.sh -classpath build/shared/ -client -d build/client config.xmlNote that we need the `classpath' option so that xrpcc can find
the interface Interest; this is what will be used to build the
client-side stub with the right method calls available. Using Ant, we can
instead do:
ant xrpcc-clientThis operation puts the generated classes in subdirectories of build/client;
if you look at the generated files you will see classes with `SOAP' in the names; the
significance of this will be described later.
Technical note: you will also see that one of the generated
classes is called `
To make life easy for the client developer, we want to produce a single JAR file
that contains everything that the client needs to make method calls on the remote
service. So this JAR should contain the contents of ant jar-clientwhich also does the xrpcc step if necessary. This gives us the
file InterestService.jar in the directory dist.
This is the only thing a client needs to make method calls on the remote service,
apart from the implementation of XML-RPC itself, which will be product-specific.
Step 10: test the service with a test clientThere are three parts to this: writing the test client, compiling it, and running it.Step 10a: writing the test clientAs far as possible, the test client should contain only standard Java. Some XML-RPC stuff needs to be included, as the client needs to tell the RPC infrastructure where to find the remote service. I like to separate this part of the job from the `standard Java' stuff. For this example, here is a suitable test client:
import com.web_tomorrow.xmlrpctut.*;
public class InterestClient
{
/**
Invoke this test client with the service URL as an
argument
*/
public static void main(String[] args) throws Exception
{
/*
get the stub for the Web Service and set its endpoint URL
*/
Interest interest =
(Interest)(new InterestService_Impl().getInterest());
((Interest_Stub)interest)._setProperty
(javax.xml.rpc.Stub.ENDPOINT_ADDRESS_PROPERTY, args[0]);
/*
standard Java from this point on...
*/
System.out.println
("Interest on $100 at 10% per term for 10 terms is $"
+ interest.getCompoundInterest(100.0, 10.0, 10));
}
}
It is intended to be invoked with the URL of the service as a command-line argument.
If you are familiar with EJB or RMI/IIOP development, you will notice that this
code has similarities and differences with what you are used to. In these other technologies,
the manipulation and configuration of the stub, although it has to be done, is usually
wrapped up inside JNDI API calls, and is less ugly. At present, as far as I know, there
is no JNDI implementation for Web Services stubs.
It's also worth remembering that the configuration of the XML-RPC stub is defined by the JAX/RPC API, and should not be product specific; this client code should work on any XML-RPC implementation that follows the JAX specifications. Step 10b: compiling the test clientTo compile the test client we need the client JAR file created in step 9, as this provides the definitions ofInterest, Interest_Stub, etc.
However, the compiler also need access to the supporting classes that are provided
with the JWSDP; the client JAR does not contain the complete XML-RPC implementation
itself (this would make for a very large file to distribute). So:
javac -d build/testclient/ -classpath dist/InterestService.jar:[jwsdp-classes] src/testclient/InterestClient.java[jwsdp-classes] should reference the essential JAR files that come with JWSDP. In the present version of the product, the two important JARs are JWSDP_DIR/common/lib/jaxrpc-ri.jar and
JWSDP_DIR/common/lib/jaxrpc-api.jar. In later versions of the product this may
will change, and you may need to try adding JARs in the `lib' directory until all the
`class not found' messages go away.
To compile with Ant, just do: ant compile-test-client Step 10c: running the test clientTo run the client, the class search path needs to include almost all the JAR files supplied with JWSDP, so I won't list them here (refer tobuild.properties).
It will also be necessary to specify the Service URL on the command line. For
this example, the Service URL is
http://localhost:8080/InterestService/jaxrpc/InterestDon't forget to change the port number in build.properties if you did not
use the default when you installed JWSDP.
The simple way to run the test client is with Ant: ant runNote that this does not compile first; do the compile stage separately. All being well, the output should be: Interest on $100 at 10% per term for 10 terms is $159.37 Development cycleAssuming this all worked according to plan, what do you need to do if you change the implementation of the service and want to redeploy and test again? This is inevitable unless you are one of those remarkable people who can get your code to work first time. If you have changed the implementation, but not the interface, you can re-deploy and re-test very simply:ant remove install ant runIf you have changed the interface, then you need to recompile the client support classes as well (and you will probably have to modify the test client and recompile it): ant remove install ant jar-client ant compile-test-client ant runIt's as simple as that. This demonstrates the power of Ant: once the source tree and build files are set up, the edit-compile-deploy-test cycle become trivial. So what have we achieved?The operations described above may seem quite complicated but, as I hope to have demonstrated, the Java coding itself is perfectly straightforward -- it's just ordinary Java. In addition, with tools like Ant the code maintenance is straightforward as well. By doing this we have implemented a Web Service that can, in principle, be called by any client written in any programming language, using a protocol (HTTP) that is easy to secure and with a data format (XML) that everyone understands. For example, an open-source implementation of an XML-RPC implementation for C/C++ clients is available at SourceForge. This allows C and C++ clients to make calls on Web Services written in Java, for example, and vice-versa. There is even provisional support for implementing XML-RPC services in Microsoft's .NET environment, which suggests that it should be possible to write .NET clients in Java, or using non-Microsoft tools. This is a big step forward for interoperability.How does it work?Most of the `magic' takes place in the stubs and skeletons. When the client makes a call on theInterest interface, it is really making a
call on the stub that implements that interface. Inside the stub is code that
serializes the method call information into a SOAP document. SOAP is a
widely-supported method for representing the structure of an object in XML.
In this example, the SOAP that is generated looks like this:
<env:Envelope ...>
<env:Body>
<ns0:getCompoundInterest>
<double_1 xsi:type="xsd:double">100.0</double_1>
<double_2 xsi:type="xsd:double">10.0< /double_2>
<int_3 xsi:type="xsd:int">10</int_3>
</ns0:getCompoundInterest>
</env:Body>
</env:Envelope>
This document specifies that the method call is to be made on a method called
getCompoundInterest, and that there are three parameters: two doubles
and an integer. The values of these parameters are specified in text format in
the XML. Notice that we aren't specifying the object or service that the method call
is directed to; this was specified by the URL that the client used to reach the
service in the first place.
The stub and supporting code then establish an HTTP connection to the URL, and issue the SOAP document as a request. The servlet on the server reads the XML, parses it, then uses the data extracted to make the method call specified. It knows which Java object to call, because we specified this in the .properties file that
got deployed in the WAR. This method (we hope) completes successfully and returns
a value. The servlet then serializes this value into another SOAP document,
which looks (in this case) like this:
<env:Envelope ...>
<env:Body>
<ns0:getCompoundInterestResponse>
<result xsi:type="xsd:double">159.37424601000026</result>
</ns0:getCompoundInterestResponse>
</env:Body>
</env:Envelope>
This says that there is one return element of type double, and gives its value.
The client then receives this XML in the response it obtains from the servlet. The
stub and its supporting classes parse the XML, extract the value, and pass it back
to the client, completing the method call.
Going forwardIf you have followed and understood the preceeding material, you may care to give some thought to the following questions:
|