Borland發布了對 Delphi 6 意義重大的第二個補丁,在SOAP/Web Service開發方面有了很大的增強,詳見《DELPHI 6.02 搶先研究 -- BizSnap/SOAP/WebService 之四 -- 補丁2#的意義》。不過有一個方面我在那篇文章裡沒有說到,那就是用 SOAP 進行多層應用開發,當時我對這部分只是大致看了一下,沒發現與補丁前相比有什麼太多的不同,所以也沒在意,直到不久前,一位叫 liaoqian 的網友給我發來 Mail 詢問有關這方面的問題時,我才注意到 Delphi 6 Update 2# 在這部分有相當大的變化(當然不是表面上的),並且存在一個小 Bug 。 首先仿照《DELPHI 6 搶先研究 -- BizSnap/SOAP/WebService 之三 -- 用 SOAP 實現三層資料庫應用》 中的例子做一個 WebService 三層資料應用。當出現所示的自動詢問是否產生一個服務端介面的對話方塊時選擇“No”,然後建立 SOAP Server Data Module ,其它操作與《之三》相同。完成之後在IE中輸入:http://localhost:1024/Demo3.wadSoapDemo3/wsdl 將可以看到如下的介面列表,相比《之三》一文,可以看出增加了一個 IAppServerSOAP 介面,並且其 IWSDLPublish 介面的 Namespace URI 也有不同。
| Port Type |
Namespace URI |
Documentation |
WSDL |
| IAppServer |
urn:Midas-IAppServer |
|
IAppServer |
| IAppServerSOAP |
http://www.borland.com/namespaces/Types |
|
IAppServerSOAP |
| ISoapDemo3DM |
urn:SvrDMSoap-ISoapDemo3DM |
|
ISoapDemo3DM |
| IWSDLPublish |
http://www.borland.com/namespaces/Types |
Lists all the PortTypes published by this Service |
IWSDLPublish |
如果注意的話還可以發現一點問題,即:輸入 http://localhost:1024/Demo3.wadSoapDemo3 不會看到 Delphi 6.02 寫的一般 WebService 那樣的一個頁面,這有點不正常。 到編寫用戶端程式的時候,問題就出現了:在 ClientDataSet 的 ProviderName 屬性中下拉,將不會看到任何可用的 ProviderName ,但一閃而逝的服務端表單表明服務端是運行了的。通過察看 Web App Debugger 的記錄資訊可以看到如下的 SOAP 錯誤響應: <?xml version="1.0" encoding='UTF-8'?><soap-env:envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/" > <soap-env:body> <soap-env:fault> <faultcode>SOAP-ENV:Server</faultcode> <faultstring>No invokable class registered that implements interface SOAPMidas.IAppServerSOAP of (soap action/path) "http://www.borland.com/namespaces/Types-IAppServerSOAP" </faultstring> </soap-env:fault> </soap-env:body></soap-env:envelope> 注意其中 faultstring 標記中的資訊,它說明了錯誤的原因是“沒有可調用的登入的 IAppServerSOAP 介面實作類別”。如果不理睬這個問題,直接把 DataSetProvider1 寫到 ClientDataSet1 的 ProviderName 屬性中,再將 ClientDataSet1 的 Active 屬性設定為 true 將直接出現一個異常,其內容便是上面的 faultstring 的內容。 我非常喜歡包括 Delphi 在內的 Borland 的幾乎所有產品,也非常信任 Borland ,這種感情來源於我七八年來使用過的數十個不同的 Borland 的產品及其不同版本,所以我不喜歡一些人在一碰到問題時就沒理由地埋怨開發工具有 Bug 。雖然有很多傳聞說 Borland 的產品 Bug 如何的多,其實有軟體就有 Bug ,我也不是說 Borland 就一定沒有 Bug , Borland 當然也有 Bug ,在本站就曾幾次提到,難道 Microsoft 就沒有 Bug 嗎? 但這一次,我幾乎可以肯定是 Delphi 6 Update 2 所帶來的 Bug ,因為在打補丁2#之前,我也是完全按照上面的做法一步步做的,沒有任何問題,打了補丁2#就出現了這樣的問題。即便如此,我還是要找到具體的證據來證明這是一個 Bug 並要找到解決的辦法,所以我讀了 Delphi 6.02 的部分 SOAP 相關原始碼,並進行了調試。 附:《設定源碼察看和調試》 SOAP 部分源碼察看設定:在 Delphi 6 中選擇 Tools|Environment Options 選中 Library 頁,在 Browsing path 中加入路徑:$(DELPHI)/source/Soap ,然後就可以在程式中通過 Ctrl+MouseLeftButton 快速跳到相應的 SOAP 源碼檔案中了。 源碼調試設定:在 Delph 6 中選擇 Project|Options 選中 Compiler 頁,將其中 Debugging 框中的 Use Debug DCUs 一項選取後,重新編譯器,即可調試進入 VCL 的源碼中了。 通過跟蹤調試發現異常發生在 SOAPHTTPPasInv 單元中的 Procedure THTTPSoapPascalInvoker.DispatchSOAP 中: ... if IntfInfo = nil then raise Exception.CreateFmt(SInvInterfaceNotReg, [SoapAction]) else if InvClassType = nil then RaiseNoIntfException(SoapAction) ... 因為其中 InvClassType 為 Nil ,所以導致了那個異常。這提供的資訊和前面的出錯資訊其實是一樣的:即 IAppServerSOAP 介面的實作類別不存在或未註冊。所以我又找到了 IAppServerSOAP 介面的定義,在 SOAPMidas 單元中: IAppServerSOAP = interface(IInvokable) ['{C99F4735-D6D2-495C-8CA2-E53E5A439E61}'] function SAS_ApplyUpdates(...): OleVariant; stdcall; function SAS_GetRecords(...): OleVariant; stdcall; function SAS_DataRequest(...): OleVariant; stdcall; function SAS_GetProviderNames: TWideStringDynArray; stdcall; function SAS_GetParams(...): OleVariant; stdcall; function SAS_RowRequest(...): OleVariant; stdcall; procedure SAS_Execute(...); stdcall; end; 熟悉 MIDAS 技術的人看了這段代碼,立即可以發現它與 IAppServer 介面極其相似,只是在 IAppServer 中,那些方法都是以 AS_ 開頭,而這裡是以 SAS_ 開頭。那麼為什麼既然已經有了一個 IAppServer 介面了, Borland 還要在 Update 2 加入這麼一個 IAppServerSOAP 介面呢?在打補丁2之前,我也看過 SOAP 的部分代碼,那時的 SOAP Server Data Module 是從 IAppServer 派生的,雖然 IAppServer 並不是從 IInvokable 派生的(按 Delphi 6 的要求,所有 SOAP 介面都要從 IInvokable 介面派生),但其實 IInvokable 是直接從 IInterface 派生的,與 IInterface 完全相同,而 IInterface 其實就是所有 COM 介面的祖先介面--IUnknown,而 IAppServer 是從 IDispatch 介面(是 COM 的一個主要介面,用於 Automation ,其祖先當然也是 IUnknown)派生出來的,所以直接用 IAppServer 也並無不可。 在 IAppServerSOAP 介面定義前面有一段注釋,大意就是說: IAppServerSOAP 用 StdCall 類型的方法調用替代了 IAppServer 中的 SafeCall 類型的方法調用,為的是在不支援 SafeCall 的環境中也可以實現 IAppServer 的功能。這就是 IAppServerSOAP 出現的原因。 瞭解了這些就好多了。所有的 SOAP Server Data Module 都是從 TSoapDataModule 派生的,它定義在 SOAPDm 單元中,其中有如下代碼: { IAppServerSOAP } function SAS_ApplyUpdates(...): OleVariant; virtual; stdcall; function SAS_GetRecords(...): OleVariant; virtual; stdcall; function SAS_DataRequest(...): OleVariant; virtual; stdcall; function SAS_GetProviderNames: TWideStringDynArray; virtual; stdcall; function SAS_GetParams(...): OleVariant; virtual; stdcall; function SAS_RowRequest(...): OleVariant; virtual; stdcall; procedure SAS_Execute(...); virtual; stdcall; 這裡就是 IAppServerSOAP 介面的實現,由於所有的 SOAP Server Data Module 都是從 TSoapDataModule 派生的,所以 IAppServerSOAP 介面的實作類別一定是已經實現的,但有沒有註冊呢?在我們的 SOAP Server Data Module 單元--SvrDMSoap 中有如下代碼: procedure TSoapDemo3DMCreateInstance(out obj: TObject);begin obj := TSoapDemo3DM.Create(nil);end;initialization InvRegistry.RegisterInvokableClass(TSoapDemo3DM, TSoapDemo3DMCreateInstance); 顯然它也已經註冊了,但為什麼會出錯呢?看看 ISoapDemo3DM 介面的 WSDL 吧……怎麼只有 AS_XXX ?沒有 SAS_XXX ?原來如此,服務端只匯出了 IAppServer 介面,並沒有匯出 IAppServerSOAP 介面。這就是問題所在! 再來看看 SvrDMSoap 單元: ISoapDemo3DM = interface(IAppServer) ['{4F618288-9F81-4090-81EF-4ACE0BF6D0BE}'] end; TSoapDemo3DM = class(TSoapDataModule, ISoapDemo3DM, IAppServer) 原來如此,原來 ISoapDemo3DM 居然還是從 IAppServer 介面派生的,顯然這是 SOAP Server Data Module Wizard 的一個 Bug ,它還是按 IAppServer 介面的方法來產生 SOAP Server Data Module 的。知道問題的所在就好辦了,將上面的代碼中的兩個 IAppServer 都改為 IAppServerSOAP ,然後重新編譯。再來看看用戶端程式,一切正常了, ISoapDemo3DM 的 WSDL 也正確地匯出了 SAS_XXX ,特別是 hhttp://localhost:1024/Demo3.wadSoapDemo3 也可以看到標準的 Delphi 6.02 的 WebService 頁面了。 至此,這個 Bug 已經完全解決。另外,要補充說明的是:在 C++ Builder 6 中不存在這個問題。 [Mental Studio]猛禽 Apr.23-02 |