|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| The simplest route to interapplication
communication
Roy
Miller (mailto:roy@roywmiller.com?cc=&subject=XML-RPC
in Java programming) Independent consultant 13 January 2004
Interapplication communication can be a nasty problem for
programmers. Many of the available options, such as JNI, can be
difficult to use. XML-RPC provides a much easier solution. It's clean,
simple to implement, and well supported by open source libraries for
most popular programming languages (such as Java language and C++). If
you have a Java application, for example, that needs to talk to an
application written in C++, XML-RPC just might be the simplest approach.
In this article, software developer and coach Roy Miller talks about
what XML-RPC is and how to use it effectively.
I can't tell you how many times I've heard from fellow developers that
the latest hot technology is the cure for what ails the software
development world. Many people said that when XML made its debut. I wasn't
as excited at that point, and my attitude hasn't changed much since then.
I've always thought that XML is a great way to define structured data
without necessarily flattening it into a relational structure, which can
be awkward. But XML isn't a programming language -- XLST is syntactically
onerous and, at least to me, kind of odd. So I've waited for some time for
a problem to come along that required structured data exchange, which is
exactly what XML was created for. That specific problem came up on a
recent project, and XML, as used by XML-RPC, was the just the right tool
for the job.
The programming
challenge Our client made a hardware device. Prior to our
involvement on the project, the only way a user could configure each
device was with a command-line interface. That's not necessarily bad,
except that each customer might have 20 or more (perhaps even hundreds or
thousands) of these hardware devices on each network. Forcing customers to
configure each device one by one with a command-line interface would
certainly hurt sales. The problem would be especially acute when customers
had to do initial setups and configurations of multiple devices after
their orders arrived. The configuration for each device was contained in
an XML file that the device read on startup.
Our client hired us to create a configuration app that could run on one
or more centrally located management machines. The app needed to simplify
setting up all of the devices initially, reconfiguring them as necessary
(with firmware upgrades, to correct errors, and so on), and monitoring
existing devices. What made that a somewhat sticky challenge was that the
software on the device was written in C, and our desktop app needed to be
written in the Java programming language.
We briefly considered JNI, but figured there had to be something
simpler -- and there was: a nifty little thing called XML-RPC.
Enter XML-RPC The XML-RPC
Web site (see Resources) describes it
this way:
It's a spec and a set
of implementations that allow software running on disparate operating
systems and in different environments to make procedure calls over the
Internet. It's remote procedure calling using HTTP as the transport and
XML as the encoding. XML-RPC is designed to be as simple as possible,
while allowing complex data structures to be transmitted, processed, and
returned.
When we read that we knew we had our answer. The configuration for each
device was in a file (the content was XML as well, but that doesn't matter
for this discussion). That meant we already had the semantics for telling
each device how to configure itself. If we sent it the configuration file
it was expecting, it would be happy. But how would we send it? We could
just send bytes, but that posed a security risk, and doing all that byte
manipulation wasn't really what anybody wanted. We realized we could send
a string payload in a well-defined XML-RPC message, which would allow us
to invoke C functions in the very restricted public interface of the
software on each device.
XML-RPC highlights In a
nutshell, you can think of XML-RPC as a simplified SOAP. It might be the
only interapp communication you ever need. There's an excellent "how-to"
document on the XML-RPC Web site that provides some history and examples
in various languages. Then again, you might just want to read the spec. At
fewer than six pages, it is a model of simplicity. We'll go over some
highlights in this section to set the stage for how we used XML-RPC on our
project.
An XML-RPC message is an HTTP-POST request with an XML body. You need
an XML-RPC client to create the message, and an XML-RPC server to receive
it. Once the server completes the request, it sends back an XML-RPC
response message, also in XML. The request can contain parameters
(integers, strings, dates, and other types, including arrays and complex
records if you need those). The format of each request is extremely
simple, as Listing 1 shows: Listing 1. Sample
XML-RPC request
POST /RPC2 HTTP/1.0
User-Agent: Frontier/5.1.2 (WinNT)
Host: betty.userland.com
Content-Type: text/xml
Content-length: 181
<?xml version="1.0"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value><i4>41</i4></value>
</param>
</params>
</methodCall>
|
You need a methodName string that specifies a "handler"
name (examples in Listing 1) and a method to call on that
handler (getStateName in Listing 1). The server can interpret
this name string however it wants. The Java server we used, which we'll
discuss a bit later, finds an object with the handler name of
examples , and calls the getStateName method on
it.
The response is just as simple, as shown in Listing 2: Listing 2. Sample XML-RPC response
HTTP/1.1 200 OK
Connection: close
Content-Length: 158
Content-Type: text/xml
Date: Fri, 17 Jul 1998 19:55:08 GMT
Server: UserLand Frontier/5.1.2-WinNT
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value><string>South Dakota</string></value>
</param>
</params>
</methodResponse>
|
When you make an XML-RPC call, you'll get an XML response, which
contains one <params> element, which in turn contains
one <param> element, which contains one
<value> element, which contains a return value you need
to handle. In most cases, this is the response you hope to get. But life
is never that simple. If something goes wrong, the server should return
the "fault" response, which looks something like Listing 3-- a fault
reflecting too many parameters sent in the RPC: Listing 3. Sample XML-RPC fault response
HTTP/1.1 200 OK
Connection: close
Content-Length: 426
Content-Type: text/xml
Date: Fri, 17 Jul 1998 19:55:02 GMT
Server: UserLand Frontier/5.1.2-WinNT
<?xml version="1.0"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><int>4</int></value>
</member>
<member>
<name>faultString</name>
<value><string>Too many parameters.</string>
</value>
</member>
</struct>
</value>
</fault>
</methodResponse>
|
The <value> element of the
<fault> element contains a struct with a
faultCode member and a <faultString>
member. This is like toString() in a Java class. If something
goes wrong, toString() tells you what it is, complete with an
error code and an error message, assuming you coded it to do that. The
XML-RPC fault response does the same thing.
And that's about all you need to understand what's going on with
XML-RPC. In fact, you really don't need to know the details of the XML
layout for messages. The XML-RPC implementation library you choose will do
all that work for you, if you provide valid inputs. So, the only tool
you'll lack after reading the spec is a client and server implementation.
In this application, we needed a Java implementation of the client and a C
implementation of the server.
Making it work The
XML-RPC Web site includes links for client and server implementations of
the spec in multiple languages, including Java programming language, Ruby,
Python, C/C++, and Perl.
There's an implementation written by the Apache team, and there are
some written by individual developers. After reviewing the code for some
of these, we chose the Marquee XML-RPC client implementation, written by
Greger Ohlson. Ohlson wrote a server as well, but the fellow writing the
code for the hardware device we'd be configuring chose the Apache XML-RPC
server. It really doesn't matter, as long as the input and output are
predictable.
We did all of our development in Eclipse, so we simply downloaded the
Marquee library, created a project for it, and loaded it into our
workspace. Putting it on the classpath of our app gave us access to the
Marquee interface. All we had to do at that point was use it. To simplify
our approach, we created a wrapper for the device, which let the rest of
the app deal with a domain object instead of worrying about the minutia of
XML-RPC, and created an XML-RPC object to package requests and decode
responses.
When the app needed to interact with a device for some reason (to check
its status, for example), it simply called a method on the wrapper for
that device, which interacted with the XML-RPC object to do the XML-RPC
magic. Listing 4 shows a simplified example of what our wrapper looked
like. Listing 4. Device wrapper class
public class Device {
protected DeviceConfiguration configuration;
protected Status status = Status.UNREACHABLE;
protected Device(DeviceConfiguration configuration) {
this.configuration = configuration;
}
public Status getStatus() {
return obtainRpcClient().getStatus();
}
public void setStatus(Status status) {
if (this.status != status) {
this.status = status;
}
}
public void reboot() {
Status status;
try {
status = obtainRpcClient().reboot();
} finally {
makeUnreachable();
}
setStatus(status);
}
public DeviceConfiguration getConfiguration() {
this.configuration =
DeviceConfigurationBuilder.toConfig(
obtainRpcClient().getDeviceConfiguration());
makeOk();
return this.configuration;
}
public Status putConfiguration() {
Status status =
obtainRpcClient().replaceDeviceConfiguration(
DeviceConfigurationBuilder.toData(this.configuration));
setStatus(status);
return status;
}
protected RpcClient obtainRpcClient() {
return new RpcClient(
this.configuration.getIpAddress(),
80,
this.configuration.getUserPassword(),
100);
}
public void makeOk() {
setStatus(Status.OK);
}
public void makeUnreachable() {
setStatus(Status.UNREACHABLE);
}
}
|
The Device class uses three helper classes:
DeviceConfiguration , Status , and
DeviceConfigurationBuilder .
Details of these three classes are beyond the scope of this article,
though I will say that instances of the DeviceConfiguration
class hold values extracted from the XML configuration for each hardware
device. It's just a convenient way to keep track of those values. As you
can see, the Device class uses
DeviceConfigurationBuilder to convert from raw configuration
data to DeviceConfiguration instances, and vice versa.
This sample includes methods to ask a device for its status, to tell a
device to reboot itself, and to get and put configuration data. But the
Device instance didn't handle talking to the device it
modeled, delegating to the XML-RPC wrapper class instead, where the
XML-RPC excitement really happened. Listing 5 shows a simplified sample of
what our XML-RPC wrapper class looked like. We called it
RpcClient to distinguish it from the Marquee
XmlRpcClient . Listing 5. XML-RPC
client wrapper class
import java.util.Hashtable;
import marquee.xmlrpc.XmlRpcClient;
import marquee.xmlrpc.XmlRpcException;
public class RpcClient {
protected static final Object[] EMPTY_ARRAY = new Object[0];
protected XmlRpcClient xmlRpcClient;
protected String ipAddress;
protected String password;
protected int port;
protected int timeout;
public RpcClient(
String ipAddress,
int port,
String password,
int timeout) {
super();
this.ipAddress = ipAddress;
this.port = port;
this.password = password;
this.timeout = timeout;
xmlRpcClient = new XmlRpcClient(ipAddress, port, "/RPC2");
}
protected Object invoke(final String rpcMethodName) {
return invoke(rpcMethodName, EMPTY_ARRAY);
}
protected Object invoke(final String rpcMethodName, Object[] parameters) {
try {
Object result = xmlRpcClient.invoke(rpcMethodName, parameters);
if (result instanceof Hashtable) {
Hashtable fault = (Hashtable) result;
int faultCode = ((Integer) fault.get("faultCode")).intValue();
throw new RuntimeException(
"Unable to connect to device via XML-RPC. \nFault Code: "
+ faultCode
+ "\nFault Message: "
+ (String) fault.get("faultString"));
}
if (result instanceof Integer) {
return Status.getStatus((Integer) result);
}
return result.toString();
} catch (XmlRpcException e) {
throw new RuntimeException(e);
}
}
public Status getStatus() {
return (Status) invoke("Device.getStatus");
}
public String getDeviceConfiguration() {
return (String) invoke("Device.getConfiguration");
}
public Status replaceDeviceConfiguration(String configurationData) {
return (Status) invoke(
"Device.replaceConfiguration",
new Object[] { configurationData });
}
public Status reboot() {
return (Status) invoke("Device.reboot");
}
}
|
Notice that RpcClient 's public interface has five methods,
including the constructor. Each method calls a version of
invoke() . One version takes no parameters (called from
reboot() , for example), and the other takes an
Object array of parameters (called from
replaceDeviceConfiguration() , for example).
The version that takes no parameters calls the other version with an
empty Object array. The invoke() method that
takes parameters is the only place we interact with the Marquee library.
That method calls invoke() on the XmlRpcClient
instance contained in RpcClient . The Marquee library does its
magic, wrapping our method string (remember, according to the spec it
looks something like handlerName.methodName ) and our
Object array of parameters in XML, which it then sends to the
server. The result it gives back could be a Hashtable
(Marquee's choice for the "fault" version of an XML-RPC response), an
Integer wrapper (for numeric return values), or a
String (the default return type for XML-RPC messages).
In our case, if we get a fault back, we throw a
RuntimeException with details extracted from the fault
Hashtable . If we get a numeric status value, which we will
when we call reboot() for example, we instantiate a
Status object to hold it, along with a nice text translation
for display in our UI. If we get a String back, which we will
when we call getDeviceConfiguration() , we just return
that.
Rebooting aDevice Now
that you have all the pieces, let's connect the dots. Let's say our
application tells a particular Device -- we'll call it
aDevice -- to reboot itself. Some class somewhere in the UI
world calls reboot() on aDevice . Here's what
happens next:
aDevice creates an RpcClient instance,
anRpcClient , to connect to the XML-RPC server on the
physical device (using the IP address and user password from
aDevice 's DeviceConfiguration
instance).
aDevice calls reboot() on
anRpcClient .
anRpcClient calls invoke() on its Marquee
XmlRpcClient instance, xmlRpcClient , passing
it an empty parameter list.
xmlRpcClient returns a Hashtable if it got
a fault XML-RPC message from the server, an Integer if it
got a numeric return value, or a String in all other
cases.
anRpcClient returns what it got from
xmlRpcClient , which in this case would be simply a return
code that indicates all went well (that is, the server isn't giving us
any data back per se).
- If anything went wrong,
aDevice sets its
Status instance to UNREACHABLE so the UI will
report that.
- If all went well,
aDevice just updates its
Status instance to whatever anRpcClient gave
it
Asking aDevice for its current
status The case where we asked aDevice for some
data wasn't much more difficult. The most basic example was when our
application asked aDevice for its current status. In this
case, some class somewhere in the UI world calls getStatus()
on aDevice . Here's what happens next:
aDevice creates an RpcClient instance,
anRpcClient , to connect to the XML-RPC server on the
physical device.
aDevice calls getStatus() on
anRpcClient .
anRpcClient calls invoke() on its Marquee
XmlRpcClient instance, xmlRpcClient , passing
it an empty parameter list.
xmlRpcClient returns a Hashtable if it got
a fault XML-RPC message from the server, an Integer if it
got a numeric return value, or a String in all other
cases.
anRpcClient returns the status it got from
xmlRpcClient .
- If anything went wrong,
aDevice sets its
Status instance to UNREACHABLE so the UI will
report that.
- If all went well,
aDevice just updates its
Status instance to whatever anRpcClient gave
it.
The case where we asked aDevice for its current
configuration data was virtually the same, the only difference being that
we had to take the raw configuration data returned by the XML-RPC call (as
a String ) and adapt it into a
DeviceConfiguration instance. When we sent new configuration
data to aDevice , we did the opposite by extracting the
configuration data from a DeviceConfiguration instance and
then building a string payload out of it.
Note in this sample code that we didn't have to do any XML
manipulation. None. The Marquee library did it all for us. Now, the
XML-RPC spec is rather simple, so you could probably roll your own client,
but there's little need to do that -- the Marquee library is quite good
and has capabilities I didn't explore in this article. The documentation
is intuitive and complete. In my opinion, it's a joy never to have to
parse XML.
The server side Up to
this point I haven't mentioned much about the server side of the equation.
That's because somebody else on the team created the XML-RPC server side
for this application (using the C implementation from Apache). That's
great, but what if we had had to develop an XML-RPC server in the Java
language as well? Piece of cake, actually. On our project, we could have
used the Marquee XML-RPC server implementation, also written in the Java
language (see Other uses for XML-RPC
to see how we did in fact do this, but for another purpose).
Listing 6 shows a simple XML-RPC server that handles the requests from
our XML-RPC client discussed earlier. It simulates a real physical device,
just for example purposes. Let's dissect this code to see the XML-RPC
server particulars. Listing 6. Simple XML-RPC
server
import java.io.IOException;
import marquee.xmlrpc.XmlRpcServer;
import marquee.xmlrpc.handlers.ReflectiveInvocationHandler;
public class DeviceServer {
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
protected boolean isShuttingDown;
protected String configuration = "initial configuration";
protected String host;
protected String password = PASSWORD;
protected XmlRpcServer rpcServer;
protected Thread rpcThread;
protected int port;
protected Status status = Status.OK;
public DeviceServer(String theHost, int thePort) {
host = theHost;
port = thePort;
createRpcServer();
startRpcServer();
}
public void shutDown() {
isShuttingDown = true;
if (rpcServer != null)
rpcServer.shutDown();
}
public Object getStatus() {
return new Integer(status.getCode());
}
public Object getConfiguration() {
return "valid configuration data";
}
public Object replaceConfiguration(String xml) {
configuration = xml;
return new Integer(status.getCode());
}
public Object reboot() {
return new Integer(status.getCode());
}
public void setStatus(Status newStatus) {
status = newStatus;
}
protected void startRpcServer() {
rpcThread = new Thread(new Runnable() {
public void run() {
try {
rpcServer.runAsService(port);
} catch (IOException ioe) {
if (!isShuttingDown)
ioe.printStackTrace();
}
}
});
rpcThread.setName("DeviceServer[" + this.host + "] on " + this.port);
rpcThread.start();
}
protected void createRpcServer() {
rpcServer = new XmlRpcServer();
rpcServer.registerInvocationHandler(
"Device",
new ReflectiveInvocationHandler(this));
}
}
|
The constructor for our server gives a synopsis of what happens. We
save the host and port information passed in, then we call
createRpcServer() to instantiate the Marquee
XmlRpcServer and register a
ReflectiveInvocationHandler with it (more on this in a
moment). We then call startRpcServer() to give the server a
helpful name and run it in its own thread. The Marquee
XmlRpcServer requires that you call runAsService()
with a port number int before you start the thread. Once you
start it up, the server listens for requests coming into that port from
any client connected to that port.
Most of that's straightforward, but what about this
ReflectiveInvocationHandler stuff? The XML-RPC spec doesn't
spell out how to implement either a client or a server. It does say that an XML-RPC
server has to handle an incoming request with a
<methodcall> element. Within that element is a call
string of the form handlername.methodname . When you call
invoke() on a Marquee client, it manufactures the correct XML
to pass your method string and any parameters to the server in a
well-formed XML-RPC message. On the server side, the Marquee XML-RPC
server:
- Parses the method string into a handler name and a method to call on
that handler
- Finds the registered handler with the indicated name
- Calls the method on it, passing in any parameters you sent over in
the request
- Packages the results in an XML-RPC response and sends it back to the
client
To use a Marquee XML-RPC server, a running server instance has to know
how to decode your method string. It has to know what object corresponds
to the handlername key value. Having said that, it should be
obvious that the server will have no idea how to decode that
handlername unless you tell the server that the name "Device"
corresponds to a given object. That can be any instance. As Listing 7
shows, we simply had the server handle all incoming method requests. We
did that in this code within createRpcServer() . Listing 7. Simple XML-RPC server
rpcServer.registerInvocationHandler("Device", new
ReflectiveInvocationHandler(this));
|
Now, whenever our server gets a request with a method string that looks
something like Device.someMethod , it knows to find
someMethod() on itself to handle the request. In our sample
server, all we needed was the basic "find the requested method on the
requested object" behavior, so we used a Marquee
ReflectiveInvocationHandler . The basic one that comes with
Marquee does just fine, so we didn't need to write our own. That handler
simply finds the requested method on the object with which the handler was
instantiated. If you look at the code, you'll see all the Java Reflection
logic Marquee has saved you the trouble of writing.
The handler concept isn't required by the XML-RPC spec, but Marquee is
built around it, and it works well. If the basic
ReflectiveInvocationHandler doesn't do the job for you, you
can subclass XmlRpcInvocationHandler to roll your own.
Other uses for XML-RPC On
the project I've described in this article, we used XML-RPC to facilitate
external scripting of our application for testing purposes. We wrote a
simple testing framework in Ruby and had it make XML-RPC requests to our
app, which included the Marquee Java XML-RPC server. When it came time to
ship the app, we simply turned off the XML-RPC server.
SWT meets Ruby On a side project I
worked on, we used XML-RPC to create an app with an SWT UI and a
Ruby back end. Ruby was simpler to use for the app we were writing,
but UI libraries for Ruby don't hold a candle to SWT. XML-RPC
allowed us to marry those worlds without too much
trouble. |
Summary The example we
reviewed in this article was admittedly simplistic. The Marquee XML-RPC
library contains many features I didn't discuss (such as invocation
pre-processors and serializers to translate Java objects into structs for
XML-RPC transmission). Those additional features are a bonus, though. You
don't need them to get tremendous value out of XML-RPC. XML-RPC is truly
simple, which makes it worth considering for distributed applications. In
general, if you need to communicate between two applications, especially
if those apps are written in different languages, XML-RPC is worth a look.
The XML-RPC Web site quotes a Byte magazine reviewer
as saying, "Does distributed computing have to be any harder than this? I
don't think so." I agree, at least for the project I talked about here. In
this case, XML-RPC let us do what we needed to do, without getting in the
way. Good tools are like that.
Resources
- Find the definitive source for XML-RPC information at the official XML-RPC Web
site.
- The implementations page
contains links to various XML-RPC client and server
implementations.
- Find information about the Marquee XML-RPC client and
server Java implementations on sourceforge.
- "Messaging: The transport
part of the XML puzzle" (developerWorks, July
2000) by Gordon Van Huizen mentions XML-RPC as a precursor to
SOAP.
- "Using XML-RPC for Web
services, Part 1" and Part 2 by Joe Johnston
(developerWorks, March
2001) discuss using XML-RPC for Web services.
- David Mertz has written several articles on XML-RPC for the developerWorks XML
zone. "XML-RPC as object
model" (developerWorks,
December 2001) examines XML-RPC as a way of modeling object data, and
compares XML-RPC as a means of serializing objects with the xml_pickle
module; "Make your CGI scripts
available via XML-RPC" (developerWorks, April
2003)shows how to provide a programmatic interface to Web
services.
- If you're a Web services developer, don't miss "XML-RPC for Python" by
Uche Ogbuji and Mike Olson (developerWorks, August
2002).
- If you still have questions regarding XML-RPC, a good place to pose
them is in the XML and Java Technology
discussion forum, hosted by Brett McLaughlin.
- Find hundreds more Java technology resources on the developerWorks Java
technology zone.
About the
author Roy W. Miller has been a technology consultant,
software developer and coach for over ten years, first with Andersen
Consulting (now Accenture). He spent almost three years with
RoleModel Software, Inc. in North Carolina, where he focused on
building Java language applications using Extreme Programming (XP).
He is now an independent consultant and coach. He has used
heavyweight methods and agile ones, including XP, and co-authored a
book in the Addison-Wesley XP Series (Extreme Programming
Applied: Playing to Win). His most recent book, Managing Software
for Growth: Without Fear, Control, and the Manufacturing
Mindset, discusses how complexity science can help software
development and other IT managers understand how to help their teams
create great software that real people will enjoy using, without
controlling or killing programmers. Contact Roy at roy@roywmiller.com. |
|
|