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/