Reflective XML-RPC

Dynamically invoke XML-based Remote Procedure Call

XML-based Remote Procedure Call (XML-RPC) receives occasional attention as a simple protocol for remote procedure calls. It is straightforward to use, and easily available implementations such as Apache XML-RPC facilitate the protocol's use.

If your application is small or uses a limited number of remote procedures, you might prefer not to formally define the names of remote procedures and their signatures, but instead use XML-RPC in a straightforward way. Yet, if your application grows and the number of remote interfaces increases, you might find that the necessary conventions—remote methods and data objects—must be somehow fixed. In this article, I show how Java provides all you need to define remote interfaces and access remote methods: procedures and their signatures can be defined via Java interfaces, and remote procedure calls with XML-RPC can be wrapped such that both sides of a communication channel see only interfaces and suitable data objects.

This article also shows that when given Java interfaces describing the remote procedures and datastructures conforming to the JavaBeans specification, you can use the power of Java reflection as incorporated into the Reflection and JavaBeans packages to invoke remote methods transparently and convert between the data types of XML-RPC and Java with surprising ease.

Hiding complexity is good practice in itself. Needless to say, not all complexity can and should be hidden. With respect to distributed computing, this point has been famously made by Jim Waldo et al. in "A Note on Distributed Computing" (Sun Microsystems, November 1994). The framework presented here does not intend to hide the complexity of distributed computing, but it promises to reduce the pains involved in calling a remote procedure. For simplicity, I discuss only concurrent remote procedure calls and leave the asynchronous case to the zealous reader.

XML-RPC can be viewed as an oversimplification of RPC via SOAP. And by extension, the simple framework I discuss here must be regarded as a simplistic version of a SOAP engine, such as Axis. This article's main purpose is educational: I wish to show how reflection is employed to build a simple XML-RPC engine on top of existing XML-RPC frameworks. This may help you understand the inner workings of similar but vastly more complex engines for other protocols or how to apply reflection to solve different problems. A simple RPC engine can be used where a SOAP engine is clearly not feasible, such as with small applications that are not exposed via a Web server and where other forms of middleware are unavailable. Roy Miller's "XML-RPC in Java Programming" (developerWorks, January 2004) explains a useful example.

In this article, we use the Apache implementation of XML-RPC (Apache XML-RPC) to set up our framework. You do not need to know XML-RPC, nor do you need to understand the Apache XML-RPC framework, even though a basic understanding will help you appreciate what follows. This article focuses on the framework's precise inner workings, but does not make use of the protocol's details.

Avoiding conventions

Occasionally, I prefer unconventional programming. Having said this, I must immediately assure you that I am no iconoclast and do not reject good programming habits; quite the contrary. The word unconventional here means that I like to avoid conventions expressed in terms of strings scattered throughout the code that could also be defined via a programmatic API. Consider the following piece of code:

Listing 1. Invoking a remote procedure call

 Vector paras = new Vector();
paras.add("Herbert");
Object result = client.execute("app.PersonHome.getName", paras); 

Listing 1 illustrates how a remote procedure might be called using the Apache XML-RPC implementation. Observe that we need to know both the name of the procedure and the parameters we are allowed to pass to the method. We must also know the object type returned to us by the remote procedure call. Unless you have the implementation class available to check whether you have all the names (app.PersonHome and getName) and parameters right, you will need to look up these names and signatures, usually in some text file or some constant interface (an interface that provides constants for all required names). A suitably placed Javadoc might also be used. Observe that this sort of convention is rather error-prone because errors will show up only at runtime, not at compile time.

Now, in contrast, consider the following piece of code:

Listing 2. Invoking a remote procedure call

 Person person = ((PersonHome)Invocator.getProxy(PersonHome.class)).getPerson("Herbert"); 

Here, we call a static method getProxy() on the class Invocator to retrieve an implementation of the interface PersonHome. On this interface, we can call the method getPerson() and, as a result, obtain a Person object.

Listing 2's code is much more economical than the code in Listing 1. In Listing 2, we can use a method defined on an interface, which neatly defines the available methods, their signatures, and the return types all in one place. Type-safety comes along free of charge, and the code is more readable because it is freed from redundant constructs such as the Vector class.

Furthermore, if you are using a sufficiently powerful IDE, code completion will list all available methods on PersonHome together with their signatures. Thus, we get IDE programming support on top of a type-safe remote method call.

I must admit that we cannot do without conventions. The one convention we must keep (unless we are prepared to accept considerable overhead and complications) is the assumption that all data objects conform to the JavaBeans specification. Simply stated, this means that object properties are exposed via getter/setter method pairs. This assumption's importance will become clear when I talk about converting XML-RPC datastructures into Java objects.

The demand for all data objects to be JavaBeans is a convention far superior to the conventions used in a straightforward XML-RPC application because it is a general convention. It is also a convention natural for all Java programmers. Towards the end of the article, I discuss XML-RPC's limitations and suggest other useful conventions that can help you live with those limitations.

The following sections walk you through an implementation of the Invocator class and a suitable version of a local server that provides the other end of our framework's communication channel.

Implementing Invocations

Let's first look at the method that provides an interface's implementation:

Listing 3. Creating the proxy

 public static Object getProxy(Class ifType) {
   if (!ifType.isInterface()) {
      throw new AssertionError("Type must be an interface");
   }
   return Proxy.newProxyInstance(Invocator.class.getClassLoader(),
      new Class[]{ifType}, new XMLRPCInvocationHandler(ifType));
} 

The magic is hidden in a simple call to the method Proxy.newProxyInstance(). The class Proxy has been part of the Java Reflection package since Java 1.3. Via its method newProxyInstance(), a collection of interfaces can be implemented dynamically. Of course, the created proxy object does not know how to handle method invocations. Thus, it must pass invocations to a suitable handler—a task for the implementation of the java.lang.reflect.InvocationHandler interface. Here, I have chosen to call this implementation XMLRPCInvocationHandler. The InvocationHandler interface defines a single method, as shown in Listing 4.

Listing 4. InvocationHandler

 public interface InvocationHandler {
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
} 

When a method is invoked on a proxy instance, the proxy passes that method and its parameters to the handler's invoke() method, while simultaneously identifying itself. Let's now look at our handler's implementation:

Listing 5. InvocationHandler

 

private static class XMLRPCInvocationHandler implements InvocationHandler {

private Class type;

public XMLRPCInvocationHandler(Class ifType) { this.type = ifType; }

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

XmlRpcClient client =

getClient()

; // Get a reference to the client Vector paras = null; // This will hold the list of parameters if (args != null){ paras = new Vector(); for (int i = 0; i < args.length; i++) { paras.add(

ValueConverter.convertFromType(args[i])

); } } else{ paras = new Vector(); // The vector holding the parameters must not be null } Class retType = method.getReturnType(); Object ret =

client.execute(type.getName() + '.' + method.getName(), paras)

; return

ValueConverter.convertToType(ret, retType)

; } }

On creation, an instance of XMLRPCInvocationHandler is given the class that defines the remote interface. We use this class only to get the remote interface's name, which, together with the method name available on method invocation, is part of the remote request. Observe that the remote method invocation is thus totally dynamic: we need neither invoke methods on a stub class nor require any knowledge from outside the interface.

The client is obtained from the method getClient():

Listing 6. Getting the client

 protected static XmlRpcClient getClient() throws MalformedURLException {
   return new XmlRpcClient("localhost", 8080);
}

Here, we are able to use Apache XML-RPC to get a client that handles the remote call for us. Observe that we return a client without consideration of the interface on which the method has been invoked. Needless to say, we could add considerable flexibility by allowing different service endpoints that depend on the interface.

The more important code for our present purposes is represented by the static methods invoked on the class ValueConverter. It is in these methods where reflection does its magic. We look at that code in the following section.

Converting from XML-RPC to Java and back

This section explains the core of our XML-RPC framework. The framework needs to do two things: It needs to convert a Java object into a datastructure understood by XML-RPC, and it needs to perform the reverse process of converting an XML-RPC datastructure into a Java object.

I start by showing how to convert a Java object into a datastructure understood by XML-RPC:

Listing 7. Java to XML-RPC

 

public static Object convertFromType(Object obj) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, IntrospectionException { if (obj == null) { return null;

} Class type = obj.getClass(); if (type.equals(Integer.class) || type.equals(Double.class) || type.equals(Boolean.class) || type.equals(String.class) || type.equals(Date.class)) { return obj; else if (type.isArray() && type.getComponentType().equals(byte.class)) { return obj; } else if (type.isArray()) { int length =

Array.getLength(obj)

; Vector res = new Vector(); for (int i = 0; i < length; i++) { res.add(convertFromType(

Array.get(obj, i))

); } return res; } else { Hashtable res = new Hashtable();

BeanInfo info = Introspector.getBeanInfo(type, Object.class);

PropertyDescriptor[] props = info.getPropertyDescriptors();

for (int i = 0; i < props.length; i++) { String propName = props[i].getName(); Object value = null; value = convertFromType(

props[i].getReadMethod().invoke(obj, null)

); if (value != null) res.put(propName, value); } return res; } }

To convert a Java object into a datastructure understood by XML-RPC, we must consider five cases, which are illustrated in the listing above:

  1. Null: If the object we need to convert is null, we just return null.
  2. Primitive type: If the object is of one of the primitive types (or their wrapping types)—int, double, Boolean, string, or date—then we can return the object itself, as XML-RPC understands these primitive types.
  3. base64: If the object is a byte array, it is understood to represent an instance of the base64 type. Again, we may simply return the array itself.
  4. Array: If the object is an array but not a byte array, we can use the utility class Array, which comes with the Java Reflection package to first find the length of the array. We then use this length to loop over the array and, again, using the Array utility, access the individual fields. Each array item is passed to the ValueConverter, and the result is inserted into a vector. This vector represents the array to Apache XML-RPC.
  5. Complex types: If the object is none of the above, we can assume it is a JavaBean, a basic assumption fundamental to the entire construction and the one convention we agreed on at the outset. We insert its attributes into a hashtable. To access the attributes, we use the introspective power of the JavaBeans framework: we use the utility class Introspector to get the bean information that comes encapsulated in a BeanInfo object. In particular, we can loop over the bean's properties by accessing the array of PropertyDescriptor objects. From such a property descriptor, we retrieve the name of the property that will be the key into the hashtable. We get this key's value, i.e., the property value, by using the read method on the property descriptor.
1 2 3 Page 1