DELPHI 6.02 抢先研究 -- BizSnap/SOAP/WebService 之五 - -- 补丁2#的 Bug 及其解决方法

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 中不存在这个问题。