利用反射机制实现XML-RPC
【IT168 技术文档】
消除习惯
有时候,我更喜欢非传统的编程。谈到这点,我必须使你确信,我不是一个总打破旧习俗的人,我不反对好的编程习惯;恰恰与此完全相反。“非传统”在这里主要表明我喜欢去避免程序中到处的定义字符串,而且这些代码可以在可编程的API里定义。考虑下面代码片断。
代码1 调用远程过程
Vector paras = new Vector();
paras.add("Herbert");
Object result = client.execute("app.PersonHome.getName", paras);
代码1展示了通过Apzche XML-RPC的实现如何调用远程过程。可以注意到,使用者需要去指导远程过程和传递到远程方法中参数的名字。也必须知道远程过程返回的对象类型。除非你已经实现了一个类去验证你使用的这些名字(app.PersonHome 和 getName)是否正确,那么你局需要去查找这些名字和声明,通常这些信息保存在文本文件或常量接口中(一个接口提供所有参数名字)。也可能放置到Javadoc中适当的地方。大家可以发现,这种约定有造成运行时错误的隐患,因为这些错误只在运行时才能显现出来,而在编译时不能显现。
现在,对比考虑下面一段代码:
代码2 调用远程过程
Person person = ((PersonHome)Invocator.getProxy(PersonHome.class)).getPerson("Herbert");
在这,我们调用Invocator类的静态方法getProxy()去检索PersonHome接口的实现。在这个接口里,可以调用getPerson(),得到Person对象。
代码2相对于代码1是一个较简洁的方式。在代码2种,使用简洁的定义在接口中的方法,在接口里可以把可用的方法,方法声明,返回类型都一起定义。因为不需要强制转换对象,所以也是类型安全。因为不需要额外的像Vector类的构造函数,代码可读性也较好。
而且,如果你使用了一个功能强大的IDE,代码助手将罗列所有可用的方法和他们的实现。因此,在类型安全远程方法调用上从IDE获得支持。
我必须承认,没有规范我们不能作任何事。我们必须坚持(除非我们准备接受高昂的成本和复杂化)的一个规则就是:假设所有的数据对象都遵从JavaBean规范。简单地说,对象的属性操作必须通过getter/setter方法。在我们讨论 把XML-RPC数据结构转换成Java对象时,这个假设的重要性将显现出来。
把所有数据对象遵从JavaBean规约是比在XML-RPC应用程序中的规约更高级一些,因为后者是一个通用规范。它也是所有Java 程序员的的通用规范。在本文结尾部分,我讨论XML-RPC的限制,还要建议其他一些有用的规范,而且可以更好的让你理解那些限制。
随后的部分,一起浏览Invocator类的实现和一个提供框架通信信道的一个本地服务器。
实现调用
让我们首先看看提供一个接口实现的方法。
代码3 创建代理
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));
}
所有的实现逻辑隐藏在了Proxy.newProxyInstance()方法中。Proxy类从Java1.3开始已经是Java Reflection包的一部分。经由newProxyInstance()方法,一些列的接口可以自动被实现。当然,一个proxy类不知道怎样处理方法调用。因此,它必须把调用传送给一个合适的处理器 – 一个实现 java.lang.reflect.InvocationHandler类的作业中。在这里,我已经选择取调用这个实现类XMLRPCInvocationHandler。InvocationHandler接口定义单一的方法,如代码4所示。
public interface InvocationHandler ...{
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
当一个proxy实例调用了一个方法,proxy实例传递这个方法和他的参数叨处理器类的invoke()方法,同时要识别它。让我们看看处理器的实现:
代码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);
}
}
在创建过程中,XMLRPCInvocationHandler就是有远程接口的实现类。我们使用这个类仅仅是为了获得远程接口的名字和可供调用的方法名。可以观察到远程方法的调用总是动态的:我们既不需要在桩类里调用方法,也不需要从外部获得任何接口的信息。
Client类从getClient()方法获得:
代码6. 获得client类
protected static XmlRpcClient getClient() throws MalformedURLException ...{
return new XmlRpcClient("localhost", 8080);
}
这里,我们能使用Apache XML-RPC去获得处理远程调用的Client类。可以看到我们返回一个Client类而没有考虑我们所有调用的方法的接口。
更重要的代码是在ValueConverter类中用静态方法调用。在那个方法里反射提供了这种方式的魅力所在。在下面部分我们分析这些代码。
XML-RPC 和Java之间的转换
这部分解释XML-RPC架构的核心。这个框架需要作两件事:转换Java对对象成能被XML-RPC所理解的数据结构和一个反向的转换处理。
我开始展示如何把一个Java对象转换为能被XML-RPC理解的数据结构:
代码7. Java 到 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;
}
}
转换Java对对象成能被XML-RPC所理解的数据结构,需要考虑如上面代码展示的5种情况:
- Null:如果要转换的对象是Null值,我们必须返回一个Null。
- 原始类型:如果要转换的对象是原始类型(或这些原始类的包装类)-- int、double、Boolean、string或date, 那么就返回该对象即可。
- base64:如果对象是一个字节数组,那么可以认为这个数组是base64类型数据。只需把数组直接返回即可。
- Array:如果对象不是一个字节数组,我们能从Java Reflection包中使用Array工具类获得数组长度。使用这个长度值,在一个循环中读数组的每个数据单元。把每个数据单元的数据传入ValueConverter封装进入一个Vector中。
- 复杂类型:如果对象不是上面所叙述的类型,我们可以假定它是一个JavaBean – 在程序开始我们可以设立这么一个共同遵守的假定原则。我们把属性插入HashTable内。要访问这些属性,可以使用JavaBean框架自举的机制:使用工具类Introspector去获得封装在BeanInfo对象里的信息。特别,我们可以循环访问PropertyDescriptor对象数组获得Bean的属性。从这样一个属性描述器里,可以检索到属性的名字,这些名字也是访问HashTable的键。我们通过读属性描述器获得这些键值,例如属性值。
理解了数据转换的约定,我们可以看看处理服务是如何实现的。
实现服务处理
已经解释了一个远程服务是如何被调用和在XML-RPC和Java对象之间转换时包含什么。我了解了所迷惑不解的问题的剩下部分:在一个服务端如何处理请求
这里是一个为这篇文章所实现简单的服务器实现代码:
代码9 服务器
public class Server ...{
private WebServer webserver = null;
public void start() ...{
webserver = new WebServer(8080);
webserver.addHandler (PersonHome.class.getName(), new Handler(PersonHome.class, new PersonHomeImpl()));
webserver.setParanoid(false);
webserver.start();
}
public void stop() ...{
webserver.shutdown();
webserver = null;
}
private static class Handler implements XmlRpcHandler ...{
private Object instance;
private Class type;
public Handler(Class ifType, Object impl) ...{
if (!ifType.isInterface()) ...{
throw new AssertionError("Type must be an interface");
}
if (!ifType.isAssignableFrom(impl.getClass())) ...{
throw new AssertionError("Handler must implement interface");
}
this.type = ifType;
this.instance = impl;
}
public Object execute(String method, Vector arguments) throws Exception ...{
String mName = method.substring(method.lastIndexOf('.') + 1);
Method[] methods = type.getMethods();
for (int i = 0;
i < methods.length;
i++) ...{
if (methods[i].getName().equals(mName))...{
try ...{
Object[] args = new Object[arguments.size()];
for (int j = 0;
j < args.length;
j++) ...{
args[j] = ValueConverter.convertToType (arguments.get(j), methods[i].getParameterTypes()[j]);
}
return ValueConverter.convertFromType(methods[i].invoke(instance,args));
}
catch (Exception e) ...{
if (e.getCause() instanceof XmlRpcException)...{
throw (XmlRpcException)e.getCause();
}
else...{
throw new XmlRpcException(-1, e.getMessage());
}
}
}
}
throw new NoSuchMethodException(mName);
}
}
public static void main(String[] args)...{
Server server = new Server();
System.out.println("Starting server...");
server.start();
try ...{
Thread.sleep(30000);
}
catch (InterruptedException e) ...{
e.printStackTrace();
}
System.out.println("Stopping server...");
server.stop();
}
}
关键类是ApacheXML-RPC包中的WebServer类。黑体字代码展示了我们主要的需求:我们必须注册一个服务句柄。这个句柄经由XmlRpcHandler接口定义,这个接口就像代理机制中InvocationHandler接口,有个方法对应方法调用的委派。在这里,叫做execute()方法,有着和InvocationHandler接口相同的实现精髓。最大的不同是,我们须注册联系接口和他的实现的一个句柄,不需要提供服务接口的实现(一桩程序形式)。然而,在服务器里,我们需要定义那块代码负责处理到来的请求。最后,可以看到,通过循环遍历接口方法调用服务方法,我们使用普通的方式去发现了争取的方法。这里,我们不依靠标准的自举JavaBean,因为服务方法不是仅仅有setter和getter方法。
