程序
Java 2平台1.3版本为Java映像API(Reflection API)增加了一个极其实用的扩展:动态代理类。一个动态代理类就是一个实现了一系列运行时指定的接口的类。这个代理可以象它真正实现了这些接口一样使用。换句话说,可以直接在代理对象上调用任意接口的任意方法——当然,必须先进行必要的类型定型(casting)。由此,我们可以用动态代理类为一组接口创建一个类型安全的代理对象,且不必象使用编译时工具一样预先生成代理(有关动态代理类更详细的说明,请参见本文最后的参考资源)。
接下来我将介绍一个以动态代理类为基础的框架,这个框架使得SOAP(简单对象访问协议)客户程序的创建更加简单和直观。SOAP是一种用XML编码数据的有线协议。在本系列文章的第二篇、第三篇构造SOAP服务的过程中,我们发现客户程序的开发者必须多做许多原来不必做的工作。为帮助回忆,你可以看一下第二篇文章中的SOAP服务代码,看看和客户程序代码相比较时,服务程序的SOAP代码是多么微不足道。本系列文章前几篇所创建的简单SOAP服务显示出,基于SOAP的服务只包含无论用不用SOAP都必须提供的代码。服务程序的开发者要编写的额外代码很少,而客户程序开发者却有许多额外工作要做。本文介绍的类将把这些额外工作减到最少。
一、介绍SOAP代理类
首先,我要给出如果客户程序使用了本文创建的框架,它将变成什么样子:
package hello;
import soapproxy.*;
public class Client
{
public static void main(String[] args)
{
try
{
Class[] interfaces = new Class[] {hello.Hello.class};
Hello hello = (Hello)(Proxy.newInstance("urn:Hello",interfaces));
// 调用sayHelloTo方法
// 这个sayHelloTo方法需要一个字符串参数
System.out.println(hello.sayHelloTo("John"));
// 调用sayHelloTo方法
// 这个sayHelloTo方法需要一个Name JavaBean参数
Name theName = new Name();
theName.setName("Mala");
System.out.println(hello.sayHelloTo(theName));
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
也许是出于我的个人爱好,我认为上面的客户代码比第二篇和第三篇文章中的客户代码更好。如果你现在不能理解上面的代码,这很正常,但我想待到本文结束时你会理解的。
要理解客户程序的代码,你必须深入了解SOAP Proxy类,它在soapproxy包内,可以在Proxy.java内找到(参见本文最后的参考资源)。Proxy类有一个私有的构造函数,它意味着Proxy实例不能从Proxy之外创建;新建Proxy实例的唯一方法是通过静态的newInstance()方法。newInstance()方法有两个参数:SOAP服务的对象ID,以及一个数组,数组中包含一组该代理要实现的接口的名字。对象ID很简单,但这些接口名字是什么?从哪里去得到这些接口的名字?SOAP服务的开发者直接把服务上所有可被客户程序调用的方法堆在一起得到一个接口。相当简单,不是吗?
现在我们为HelloWorld服务定义一个接口。第二篇文章中,这个服务的最终版本有sayHelloTo()方法的两个重载版本:一个版本的参数是一个字符串,另一个版本的参数是一个Name JavaBean。这两个方法就可以构成一个接口,称为Hello,如下所示:
package hello;
public interface Hello
{
public String sayHelloTo(String name);
public String sayHelloTo(Name name);
}
服务开发者决定要创建多少接口,以及为这些接口取什么样的名字。例如,你可以为HelloWorld服务创建两个接口,每一个接口包含一个方法。一般地,你应该避免创建方法数量大于七个的接口。另外,注意只把那些看来有必要放在一起的方法用一个接口组织起来。例如,如果HelloWorld服务还有一个返回定制的Good-Bye消息给调用者的sayByeTo()方法,设计两个独立的接口也许比较明智:一个接口用于sayHelloTo()方法,一个接口用于sayByeTo()方法。
现在我们有了定义HelloWorld服务和客户程序之间契约的接口,下面返回来看newInstance()方法。如前所述,newInstance()方法创建Proxy类的一个新实例。newInstance()方法可以创建新实例是因为它属于Proxy类,能够访问私有的构造函数。newInstance()方法为新创建的实例调用initialize()方法。initialize()值得关注,因为动态代理就是在这里创建和返回。initialize()的代码如下所示:
private Object initialize(Class[] interfaces)
{
return(java.lang.reflect.Proxy.newProxyInstance(getClass().getClassLoader()
,interfaces,this));
}
注意newProxyInstance()方法的应用。创建动态代理类实例的唯一办法是调用该类(即java.lang.reflect.Proxy类)静态的newProxyInstance()方法。java.lang.reflect.Proxy类为创建动态代理类提供了静态方法,而且它还是所有由这些方法创建的动态代理类的超类。换句话说,它不仅是一个创建动态代理类的工厂,而且它本身也是一个动态代理类!因此,在我们的例子中,SOAP代理不是动态代理;相反,这个动态代理实际上是newProxyInstance静态方法返回的java.lang.reflect.Proxy类的一个实例。从本文后面可以看到,这个动态代理实际上通过SOAP代理实现的invoke()方法完成它的所有工作。那么,这个动态代理如何建立和SOAP代理的联系呢?因为有一个对SOAP代理的引用传递给了newProxyInstance()方法。也许现在这听起来有点费解,但只要你分析一下invoke()方法,这一切就很明白了。
java.lang.reflect.Proxy类构造函数的第一个参数是一个类装载器实例,第二个参数是需要动态实现的接口的数组(它就是客户程序传递给newInstance()的数组),第三个参数是一个实现了java.lang.reflect.InvocationHandler接口的类的实例。因为SOAP Proxy类实现了InvocationHandler接口,所以第三个参数是代理实例本身(即this)。InvocationHandler接口有一个方法invoke()。当动态代理的动态实现的接口被调用时,Java运行时环境调用invoke()方法。因此,举例来说,当客户程序调用动态代理的Hello接口的sayHelloTo()方法时,Java运行时环境将调用SOAP代理的invoke()方法。
你可能已经发现,SOAP代理的newInstance()方法不返回SOAP代理的实例;相反,它返回newInsance()刚刚创建的动态代理,而动态代理动态地实现客户程序传入的接口数组。客户程序可以将这个返回的动态代理定型为传入newInstance()的任意接口类型,在动态代理上调用接口所定义的各个方法,就象动态代理真地实现了那些接口一样。
.
.
try
{
Class[] interfaces = new Class[] {hello.Hello.class};
Hello hello = (Hello)(Proxy.newInstance("urn:Hello",interfaces));
// 调用参数为字符串的sayHelloTo方法
System.out.println(hello.sayHelloTo("John"));
// 调用参数为Name JavaBean的sayHelloTo方法
Name theName = new Name();
theName.setName("Mala");
System.out.println(hello.sayHelloTo(theName));
}
.
.
在上面的代码中,invoke()方法将被调用两次,每次调用sayHelloTo()方法时执行一次。现在我们来看看invoke()方法。简而言之,invoke()方法的工作正是第二篇文章中每一个客户程序必须手工完成的工作,其中包括:用合适的调用参数设置一个Call对象,定制的调用参数所需要的类型映射。由于SOAP代理中的invoke()方法担负了所有这些任务,客户程序释放了这份负担。
在invoke()方法接收到的三个参数中,我们只对后面两个感兴趣。第二个参数,即Method对象,给出了被调用方法的名字。记住,被调用方法的名字对应着一个SOAP服务导出的已知方法。服务的对象ID作为参数传递给newInstance()方法,所以invoke()方法已经拥有该对象ID。invoke()方法利用这些信息,按照如下方式设置Call对象:
Call call = new Call();
call.setTargetObjectURI(urn);
call.setMethodName(m.getName());
call.setEncodingStyleURI(Constants.NS_URI_SOAP_ENC);
现在要做的是为远程服务调用设置参数。为此,我们要用到invoke()方法的第三个参数:传入动态代理上被调用方法的一个参数数组。数组中索引为0的参数是方法调用中最左边的参数,索引为1的参数是方法的第二个参数,依此类推。举例来说,如果客户程序调用了sayHelloTo(String name)方法,那么参数数组就是包含一个字符串的数组。invoke()方法处理该数组的每一个元素,创建一个由Parameter对象构成的向量(Vector)(正如第二篇文章中客户程序所做的那样):
java.util.Vector params = new java.util.Vector();
for( int i=0; i<args.length; i++ )
{
if( isSimple(args[i]) || isSimpleArray(args[i]) )
{
params.add(new Parameter(_paramName+(i+1),
args[i].getClass(),args[i],null));
}
else if( isVector(args[i]) )
{
addMapping((java.util.Vector)args[i]);
params.add(new
Parameter(_paramName+(i+1),args[i].getClass(),args[i],null));
}
// 如果这个数组的元素不属于Java基本数据类型
// 则假定这是一个JavaBean的数组
else if( isArray(args[i]) )
{
if( smr == null )
smr = new SOAPMappingRegistry();
if( beanSer == null )
beanSer = new BeanSerializer();
ArraySerializer arraySer = new ArraySerializer();
smr.mapTypes(Constants.NS_URI_SOAP_ENC,
null, null, beanSer, beanSer);
smr.mapTypes(Constants.NS_URI_SOAP_ENC,
null,args[i].getClass(), arraySer, arraySer);
params.add(new Parameter(_paramName+(i+1),
args[i].getClass(),args[i],null));
}
// 假定这是一个Bean
else
{
if( smr == null )
smr = new SOAPMappingRegistry();
if( beanSer == null )
beanSer = new BeanSerializer();
String qnamePart = args[i].getClass().getName();
smr.mapTypes(Constants.NS_URI_SOAP_ENC,
new QName(urn, qnamePart),args[i].getClass(), beanSer,
beanSer);
params.add(new Parameter(_paramName+(i+1),args[i].getClass(),args[i],null));
}
}
invoke()方法用到了许多私有的辅助方法,比如用isSimple()来确定参数的类型。如果参数是一个JavaBean或者一个数组,那么,程序必须设置一个定制的SOAP映射注册项,并通过setSOAPMappingRegistry()方法对Call对象作相应的设置(参见第二篇文章)。SOAP代理假定,当出现JavaBean时,SOAP服务用到的所有JavaBean按照如下方式映射:NameSpace URI设置成对象ID,Local Part设置成JavaBean完整的类名。我们部署HelloWorld服务时正是按照这个要求进行,所以一切都不存在问题。
invoke()方法的剩余部分相当简单:设置Call对象参数,设置定制SOAP映射注册项(如果有必要的话),发出调用,接收方法调用的返回值。如下所示:
if( params.size() != 0 )
call.setParams(params);
if( smr != null )
call.setSOAPMappingRegistry(smr);
// 发出调用
Response resp = call.invoke(serverURL, "");
if( !resp.generatedFault() )
{
Parameter ret = resp.getReturnValue();
return(ret.getValue());
}
else
{
Fault fault = resp.getFault();
throw new
SOAPException(fault.getFaultCode(),fault.getFaultString());
}
二、HelloWorld服务
下面是HelloWorld服务的完整代码。有似曾相识的感觉吗?
package hello;
public class HelloServer
{
public String sayHelloTo(String name)
{
System.out.println("sayHelloTo(String name)");
return "Hello " + name + ", How are you doing?";
}
public String sayHelloTo(Name theName)
{
System.out.println("sayHelloTo(Name theName)");
return "Hello " + theName.getName() + ", How are you doing?";
}
}
回忆一下,Name是一个简单的JavaBean,代码如下:
package hello;
public class Name
{
private String name;
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
}
事实上,这里服务的代码与第二篇文章中的服务程序代码完全一样。对于服务开发者来说,唯一增加的工作是创建Java接口。部署服务的方法也和第二篇文章中讨论的完全一样,所以这里我不再介绍。相同的地方还不止如此,编译和运行客户程序的方法也和第二篇文章介绍的一样。为什么有这么多相同之处呢?因为我们创建的代理是一个非插入式的框架,它不会修改和干涉任何Apache SOAP部件的内部工作——无论是客户端还是服务端。
三、其他说明
本文讨论的SOAP代理(可以从文章后面下载)支持以下参数类型:
⑴ 下面的Java基本数据类型及其对应的对象形式。
boolean, Boolean,
double, Double,
float, Float,
long, Long,
int, Integer,
short, Short,
byte, Byte
注:服务器端总是接收基本数据类型。
⑵ 任何JavaBean
注:
该JavaBean不能包含其他JavaBean。 如果数组或向量包含除字符串或1列出数据类型之外的类型,则JavaBean不能包含这类数组或向量。 ⑶ 下面的类:String, Vector
注:
Vector可以包含1、2列出的所有类型和字符串。 服务器端把Vector作为一个对象的数组接收。 ⑷ 数组。数组元素可以是在1、2中列出的所有类型和字符串(上面已注明的除外)。
■ 结束语
在这个四篇文章构成的系列中,我不仅介绍了SOAP的基础知识,而且介绍了SOAP 1.1标准的一个优秀的实现:Apache SOAP。在本文中,我提供了一个以动态代理类为基础的框架,这个框架极大地简化了使用Apache SOAP的客户程序开发者的工作。
我深切地感到SOAP有着美好的前景,至少有两个理由使我这么认为:首先,SOAP以一些开放的标准为基础,比如XML。这使得无论是Microsoft,还是反Microsoft的企业,都广泛地接受了SOAP。对于开发者来说,这无疑是一个天大的好消息。第二,SOAP正在成为其他许多标准的基础,比如UDDI(Universal Description,Discovery,and Integration)。许多人认为,Web服务代表着下一代的Web应用开发,而SOAP和UDDI都是Web服务的关键组成部分。
■ 参考资源
下载本文的完整代码:JavaAndSOAP4_code.zip
W3C的SOAP 1.1规范:
http://www.w3.org/TR/SOAP/ 有关动态代理类的更多信息:
http://java.sun.com/j2se/1.3/docs/guide/reflection/proxy.html 关于IBM SOAP工程的更多信息:
http://www.alphaworks.ibm.com/tech/soap4j 下载Apache SOAP:
http://xml.apache.org/dist/soap/