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