在前一個例子(見 《DELPHI 6 搶先研究 -- BizSnap/SOAP/WebService 之一 -- 一個 Hello world! 的例子》)中我們看到,通過 SOAP 可以很方便地進行遠程對象調用,雖然那個例子用的對象是一個 Delphi 類,但實際上只需要對對象作一個 SOAP 封裝,即可調用包括 COM/CORBA/EJB 等各種對象(除 EJB 必須用 Java 實現外, COM/CORBA 都已可以用 Delphi 實現)。在那個例子中,介面方法用到的資料類型都是標準類型,但實際應用中常常會碰到要傳遞自訂類型的情況,這時的操作略麻煩一些,詳情如李維 《樂趣無窮,可能無限的新技術-Web Service》 一文中的例子所示。 同樣,這裡也要用一個例子來說明通過 SOAP 傳遞自訂資料類型的方法,這個例子會是一個比較麻煩的例子: 服務端: 1.New|WebServices|Soap Server Application ,如: 這個例子是用 Web App Debugger (詳見《DELPHI 6 搶先研究 -- Web 應用程式開發及調試》), 設定其 CoClass Name 為 wadSoapDemo2 , 如: 2.SaveAll , Unit2 命名為: SvrWMMain , Unit1 不改名, Project1 命名為: Server ; 3.New|Data Module ,將此單元儲存為 SvrDataMod ; 4.在其中放入兩個 dbExpress 控制項: SQLConnection1 和 SQLDataSet1 ,如: 其屬性設定為:
| SQLConnection1 |
ConnectionName := IBLocal; LoginPrompt := false; Params.Values['Database'] := '[...]/Examples/Database/Employee.gdb'; // 上面的 [...] 為你的 InterBase 安裝路徑 |
| SQLDataSet1 |
SQLConnection := SQLConnection1; CommandText := 'select FULL_NAME, PHONE_EXT from EMPLOYEE WHERE EMP_NO = :EMP_NO'; |
5.New|Unit ,將此單元儲存為 SvrDataType ,其內容如下: unit SvrDataType;interfaceUses InvokeRegistry;Type TEmpInfo = Class( TRemotable ) Private FName : String; FPhone : String; published Property Name : String Read FName Write FName; Property Phone : String Read FPhone Write FPhone; end;implementationInitialization RemClassRegistry.RegisterXSClass( TEmpInfo );Finalization RemClassRegistry.UnRegisterXSClass( TEmpInfo );end. 此單元中定義了類: TEmpInfo ,用於記錄員工資訊,包括 Name 和 Phone 兩個域,均為字串類型。對於需要傳遞到用戶端的資料類型,必須從 TRemotable 類派生,它能夠自動處理類型資訊的傳遞。如果要手工處理自訂資料類型的傳遞,則必須從 TRemotableXS 類派生,其用法與 TRemotable 類似,但這樣的話,必須實現兩個轉換方法: NativeToXS 和 XSToNative ,詳見 Delphi6/Source/Soap/XSBuiltIns.pas 中的幾個類的實現。 需要注意的是,此類中將兩個屬性放在 Published 中,這裡一定要這麼做,我曾經因為將它們放在了 Public 中,導致用戶端無法取得服務端的資料類型資訊,後來才發現它們必須放在 Published 中才行,所以雖然這裡並不是控制項,這些屬性也不是為了要在 Object Inspector 中顯示,但仍然需要放在 Published 中。這可能是因為 Published 較 Public 多一些 RTTI(Run Time Type Info,運行時類型資訊) 的東東,而遠端資料類型是依賴於 RTTI 的。 最後是在遠程類註冊資訊庫中註冊和反註冊此類。 6.New|Unit ,將此單元儲存為 SvrSoapIntf ,其內容如下: unit SvrSoapIntf;interfaceUses InvokeRegistry, SvrDataType;Type ISoapEmployee = Interface( IInvokable ) ['{31903B5A-96B3-43C2-A7B5-F67F6DB829E5}'] Function GetEmployee( aEmpNo : Integer ) : TEmpInfo; StdCall; End;implementationInitialization InvRegistry.RegisterInterface( TypeInfo( ISoapEmployee ) );end. 此單元中定義了 SOAP 介面,這與前一個例子並沒有大的不同,只是這次為了清晰起見,將此介面放在一個單獨的單元裡實現。唯一區別較大的是此介面中的方法 GetEmployee 返回了一個自訂資料類型: TEmpInfo 。 7.在 SvrWMMain 單元中加入 SOAP 實作類別,完整的單元內容如下: unit SvrWMMain;interfaceuses SysUtils, Classes, HTTPApp, WSDLPub, SOAPPasInv, SOAPHTTPPasInv, SoapHTTPDisp, WebBrokerSOAP;type TWebModule2 = class(TWebModule) HTTPSoapDispatcher1: THTTPSoapDispatcher; HTTPSoapPascalInvoker1: THTTPSoapPascalInvoker; WSDLHTMLPublish1: TWSDLHTMLPublish; private { Private declarations } public { Public declarations } end;var WebModule2: TWebModule2;implementationuses WebReq, InvokeRegistry, SvrDataType, SvrSoapIntf, SvrDataMod;{$R *.DFM}Type TSoapEmployee = class( TInvokableClass, ISoapEmployee ) Protected Function GetEmployee( aEmpNo : Integer ) : TEmpInfo; StdCall; End;{ TSoapEmployee }Function TSoapEmployee.GetEmployee(aEmpNo: Integer): TEmpInfo; StdCall;Begin Result := TEmpInfo.Create; If ( Not Assigned( DataModule2 ) ) Then DataModule2 := TDataModule2.Create( Nil ); Try DataModule2.SQLConnection1.Open; With DataModule2.SQLDataSet1 Do Begin ParamByName( 'EMP_NO' ).AsInteger := aEmpNo; Open; If ( Not Eof ) Then Begin Result.Name := FieldByName( 'FULL_NAME' ).AsString; Result.Phone := FieldByName( 'PHONE_EXT' ).AsString; End Else Begin Result.Name := ''; Result.Phone := ''; End; Close; End; DataModule2.SQLConnection1.Close; Finally DataModule2.Free; DataModule2 := Nil; End;End;initialization WebRequestHandler.WebModuleClass := TWebModule2; InvRegistry.RegisterInvokableClass( TSoapEmployee );end. 這裡介面的實作類別 TSoapEmployee 的定義與實現與前一例子類似。 GetEmployee 的實現也不複雜:首先,如果未建立 DataModule2 的執行個體(需要在 Project|Options 中將 DataModule2 從自動建立列表中移去)則建立一個 DataModule2 的執行個體;然後串連到資料庫,查詢指定員工號的員工資訊;最後返回此資訊。注意:這裡用了 dbExpress ,有些地方與 BDE/ADO 不太一樣,如不能使用 RecordCount ,只能用 Eof 來判斷是否有查詢結果。 8.至此完成服務端的全部程式,編譯並運行,然後退出即完成 Web App Debugger 應用程式的註冊。 啟動 Web App Debugger ,再啟動瀏覽器,在地址欄輸入: http://localhost:1024/Server.wadSoapDemo2/wsdl/ISoapEmployee 即可瀏覽其 WSDL 內容,在其中包含了自訂類型的必要資訊,但如果前面 SvrDataType 單元中的 TEmpInfo 類的屬性不是放在 Published 部分的話,這裡將看不到類型資訊。下面是這個 WSDL 中的 types 標記部分內容: <types> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:SvrDataType"> <xs:complexType name="TEmpInfo"> <xs:sequence> <xs:element name="Name" type="xs:string"/> <xs:element name="Phone" type="xs:string"/> </xs:sequence> </xs:complexType></xs:schema> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:WSDLSoap"> <xs:complexType name="TWSDLSOAPPort"> <xs:sequence> <xs:element name="PortName" type="xs:string"/> <xs:element name="Addresses" type="ns3:TWideStringDynArray"/> </xs:sequence> </xs:complexType></xs:schema> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:Types"> <xs:complexType name="TWideStringDynArray"> <xs:complexContent> <xs:restriction base="soapenc:Array"> <xs:sequence/> <xs:attribute ref="soapenc:arrayType" n1:arrayType="xs:string[]"xmlns:n1="http://schemas.xmlsoap.org/wsdl/"/> </xs:restriction> </xs:complexContent> </xs:complexType></xs:schema> </types> 從上面這一段 WSDL 中可以看出服務端匯出了三個“複雜類型” -- complexType : TEmpInfo, TWSDLSOAPPort, TWideStringDynArray ,其中除了 TEmpInfo 是我們自己定義的資料類型以個,另兩個是 Delphi 內部定義使用的類型,在用戶端匯入 WSDL 時我們會再看到它們的。 再來看用戶端的實現: 1.New|Application 建立一個普通的 VCL 應用程式; 2.SaveAll , Unit1 命名為 ClnMain , Project1 命名為 Client ; 3.在 Form1 上放上 HTTPRIO1, Edit1, Button1, Label1, Label2 等控制項,如: 其中 Edit1 的 Text 設定為 1 , Button1 的 Caption 設定為 GetEmployee , HTTPRIO1 的 URL 屬性設定為: http://localhost:1024/Server.wadSoapDemo2/soap ; 4.New|Web Services|Web Services Importer ,與前一例子相似,只是匯入的 URL 改為: http://localhost:1024/Server.wadSoapDemo2/wsdl/ISoapEmployee ; 5.如果服務端的 WSDL 如前面所述的那樣,則將匯入三個單元,分別包含了 TWSDLSOAPPort、 TEmpInfo、 ISoapEmployee ,其中 ISoapEmployee 是我們所認識的 SOAP 介面單元, TEmpInfo 是我們在服務端定義的資料類型, TWSDLSOAPPort 是 Delphi 內部定義的一個資料類型,我們曾在服務端的 WSDL 中看到過這個類型。 Save All ,將 TWSDLSOAPPort 的單元儲存為 ClnSoapPort ,將 TEmpInfo 儲存為 ClnDataType ,將 ISoapEmployee 儲存為 ClnSoapIntf 。注意要將 ClnSoapIntf 單元中的 Uses 中的兩個名為 UnitN 的單元相應改為 ClnSoapPort 和 ClnDataType 。由於這三個單元的內容都不需要改變,只要服務端是正確的,可以不必瞭解這三個單元的內容(特別是 ClnSoapIntf 和 ClnDataType 與服務端的相應單元基本相同),所以這裡也就不列出它們的內容了。 6.雙擊 Button1 輸入下面的代碼: procedure TForm2.Button1Click(Sender: TObject);Var ei : TEmpInfo;begin ei := ( HTTPRIO1 As ISoapEmployee ).GetEmployee( StrToInt( Edit1.Text ) ); If ( Assigned( ei ) ) Then Begin Label1.Caption := ei.Name; Label2.Caption := ei.Phone; End;end; 7.編譯運行,在 Edit1 中輸入"1"或其它資料庫中沒有相應記錄的員工號,按 Button1 , Label1 和 Label2 都將顯示空;輸入"2"或其它資料庫中有記錄的員工號,則將在 Label1 中顯示員工全名,在 Label2 中顯示此員工的電話號碼,如: 做過一遍再看這個例子也不是那麼複雜的。 猛禽 Jun.20-01, Oct.20, Oct.24 |