DELPHI 6 抢先研究-- BizSnap/SOAP/WebService 之二 - -- 通过 SOAP 传递自定义类型数据

在前一个例子(见 《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;

interface

Uses
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;

implementation

Initialization
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;

interface

Uses
InvokeRegistry, SvrDataType;

Type
ISoapEmployee = Interface( IInvokable )
['{31903B5A-96B3-43C2-A7B5-F67F6DB829E5}']
Function GetEmployee( aEmpNo : Integer ) : TEmpInfo; StdCall;
End;

implementation

Initialization
InvRegistry.RegisterInterface( TypeInfo( ISoapEmployee ) );

end.

此单元中定义了 SOAP 接口,这与前一个例子并没有大的不同,只是这次为了清晰起见, 将此接口放在一个单独的单元里实现。唯一区别较大的是此接口中的方法 GetEmployee 返回了一个自定义数据类型: TEmpInfo 。


7.在 SvrWMMain 单元中加入 SOAP 实现类,完整的单元内容如下:

unit SvrWMMain;

interface

uses
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;

implementation

uses 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