用Delphi实现动态代理(2):设计说明
在上篇《用Delphi实现动态代理(1):概述》中,对动态代理作了一个概要的说明,比如为什么需要这样的动态代理,它有什么用等。本篇将对我实现的这个动态代理的设计思路作一下介绍。
一、设计目标
如上篇中这幅动态代理结构图所示:
大致的设计目标有以下几项:
- TMDynamicProxy可以将任意接口代理到一个通用接口IMInvocationHandler上;
- IMInvocationHandler的实现不能太复杂,即TMMethodInvocation的定义要尽量简单;
- IMInvocationHandler要能够实现Remoting,即TMMethodInvocation必须可序列化;
- 需要一个IMMethodInterceptor接口,以便于实现AOP所需要的各种拦截器;
- 需要一个TMInterfaceInvoker来把IMInvocationHandler转为正确的对象调用。
从上面列出的目标可以看出,我的目标是要实现一套全新的多层框架,并且几乎是对JAVA世界里最流行的轻量容器的模仿。接下来就说明一下原因所在。
二、原因
我之所以对这个动态代理如此热情,源自于对DELPHI下多层技术的愤怒。想想从前在DELPHI开发多层应用有些什么?MIDAS?不可否认, MIDAS是一项很优秀的开发技术,可以在很大程度上简化多层应用的开发。但是正因为它的简单化,所以它跟RAD一样,容易让人在简单化中迷失,而看不到 问题的本质--对于这种事情,我喜欢引用老郑钟道新的一个经典比喻:它(比基尼泳装)展示了令人感兴趣的部分,但却隐藏了关键的部分所显示的是让人感兴趣的部分,所掩盖的则是关键的部分(刚找到这句话的准确出处,特此纠正)。这导致的结果就是开发出大量的垃圾多层应用--至少跟大部分JAVA或CORBA多层应用相比是这样的。
虽然李维写了《Delphi 5.x多层分布式应用》系列三本书,试图深入地解析一下MIDAS,但是一方面是在这浮躁的氛围下大多数人没有心思深入研究,另一方面则是MIDAS的先天不足。
上图是我在2001年写的一份书稿[1]里的一幅插图,基本上可以说明MIDAS的整体结构,从图上可以看出MIDAS没有自己的基础技术。
其中DCOM、Socket/TCP、Web/HTTP三种连接方式,本质上还是通过COM技术实现的。Socket和Web连接是借助于 ScktSrvr和HttpSrvr两个代理程序在服务端与客户端之间建立一个Tunnel来绕过Windows的安全机制或是Firewall。在这样 的应用中,Remoting是靠DCOM的ORPC实现,Transactional Data Module服务端的事务处理、Pooling则是交由MTS/COM+ Container实现,而如果是用Remote Data Module/DCOM的话,则得不到这些方面的支持。
那么用CORBA连接的MIDAS呢?在后来版本的DELPHI中--大约是D5或D6开始--取消了这一支持。如果我没记错的话,在D4中的CORBA/MIDAS实现也是基于COM技术的。
最值得一提的是D6开始的SOAP支持,在这里,DELPHI终于提供了一个自己的Remoting实现,也是本文所要讨论的动态代理。不过遗憾的是,这里用的动态代理并没有被做成通用的东西,而是专门为SOAP实现定制了一个,与DELPHI的SOAP实现紧密捆绑。
正因为以上所说的原因,当Kylix出现时,我们面临着在非Windows平台下没有MIDAS的困境。虽然这并非绝境,至少还可以用
SOAP/MIDAS,或是纯正的CORBA。遗憾的是SOAP的性能太差,而CORBA--且不说OMG没有提供IDL2PAS的标准--还面临着复杂
性的问题。
三、为什么要模仿JAVA
我选择JAVA作为模仿对象的一个原因是我认为:DELPHI需要一个像RMI这样的Remoting技术。
目前流行的Remoting技术有两类:
- 一是类似于CORBA/DCOM这样的跨语言技术,这样技术需要使用“代码生成”--比如CORBA,需要先写一个定义远程 对象的IDL文件,然后通过ORB提供的IDL编译器来生成具体语言的Stub/Skeleton代码;DCOM也类似,需要先写一个MIDL (DELPHI中使用TypeLib Editor实现可视化编辑)来定义服务端对象,生成服务端接口代码,编译服务端程序时,MIDL也被编译成TypeLib并以资源形式链接到服务端程序 里;
- 另一种是类似于RMI这样的独有技术,Remoting的实现依赖于语言本身的动态特性或由平台提供(如.net remoting)。比如RMI,将接口实现从RemoteObject派生即可为接口提供远程访问的语义。
显然,RMI虽然牺牲了跨语言的优势(其实RMI可以通过RMI over IIOP实现与CORBA的互操作,并没有完全牺牲这一优势),但同时也避免了“代码生成”,这是一个很大的好处。
对于“代码生成”这一恶行,我是深有体会的。去年我在试图开发一个基于XML的WEB框架时,一直是用DELPHI提供的XML Data binding来做的。基本的做法是:先用工具(如XMLSPY)做好一个XML Schema(XSD),然后用XML Data binding wizard生成DELPHI的接口和类。当然,一旦生成就可以很方便地使用了,只要在程序里操作这些接口就好了,其中各个Field都会被变成属性,并 且类型也都如XSD中的定义。但问题在于程序在开发过程中,总是会有一些变化的,在这种情况下,就不得不同时开着XMLSPY修改XSD,然后重新用 XML Data binding的Wizard跑一遍,非常的麻烦。做过CORBA开发的人大概也会有类似这样的体会。虽然在DELPH下做COM开发很方便,有一个可视 化的TypeLib Editor可以用,但是还是不爽。
当然,如果是在“理想”的“软件蓝领”环境里,那大概要幸福得多,因为交到Coder手里的接口已经是基本上定死,照着文档做就是了,没有这样的烦恼。但对于Agiler们,这样的麻烦是不可忍受的。
模仿JAVA的另一个原因是我对非侵入的轻量级容器的向往。所谓轻量级和重量级,是相对而言的,评价的标准就是系统对用户代码的侵入性大小。
先来看看一个侵入性强的例子。比如一个典型的MTS/COM+应用,用户代码除了要实现自己定义的业务逻辑接口以外,还至少必须要实现 IObjectControl接口。另外,还必须通过ObjectContext与容器进行互动,比如通过IObjectContext接口进行基本的事 务和安全性操作;如果要更进一步操作事务,还要通过ITransactionContext;要进行更多安全性操作,还要通过 ISecurityProperty接口等。
可以看到,像事务处理、安全性检查这些都是属于基础设施的部分,与用户的业务逻辑没有一点关系,但是这些部分又是“横切”于用户系统结构,与用户代 码紧密交织在一起。而且更重要的是这些的控制权都掌握在容器框架手上,用户只能按框架的要求来做,比如MTS/COM+的安全性控制就是与Windows 的域用户管理机制紧密捆绑。这带来很大的学习成本,低的灵活性,以及开发的复杂性。
为此,专家们提出了AOP的思想。AOP即Aspect Oriented Programming,面向方面编程。但AOP并不是一种要取代OOP的新技术,而是对OOP处理“横切”问题的一个补充。经典的OOP处理“横切”问 题通常是采用Template Method模式或类似的方法来解决,结果就是导致了强侵入性的框架。
基于AOP的思路,非侵入的框架应运而生。
一个典型的非侵入的框架中,用户代码始终是像下面这么简洁明了:
// 业务逻辑定义
{$M+}
IBizIntf = Interface
['{3A85E46D-F3D4-4D9C-A06C-4E7C1BAC9361}']
// 定义业务逻辑
End;
{$M-}
// 服务端业务逻辑实现
TBizImpl = class(TInterfacedObject, IBizIntf)
Protected
// 实现业务逻辑
end;
// 客户端调用代码
Var
BizObj : IBizIntf;
BizObj := TMDynamicProxy( TypeInfo( IBizIntf ), RemoteHandler ) As IBizIntf;
BizObj.xxx // 直接操作服务端对象
在这样的应用中,业务逻辑定义是用DELPHI原生的接口,而不需要另外的像IDL或TypeLib之类,这样整个开发过程都可以在DELPHI语言的范围,用户代码的开发也可以将注意力完全集中在业务逻辑的处理上。
但光有AOP的思想是不够的,还需要有实现。最初的实现是像AspectJ,在JAVA代码编译前作一次预编译,将Aspect代码织入后再用 JAVA编译器编译成ByteCode。遗憾的是,这也是一种“代码生成”技术。后来一个叫Jon Tirsen的人利用动态代理设计了一种运行时织入Aspect的实现--Naning[2]。
使用这种动态织入的Interceptor技术,框架提供的附加服务都可以通过Interceptor进行切入。不论是Remoting、事务、安 全性,都可以做成相应的Interceptor,然后通过配置或代码在运行时动态地将所需的Interceptor织入来实现所需要的功能。
而这一切的核心,还是“动态代理”。
参考文献
[1]此书已经夭折,引用的插图出自已经完成的第四章,在这里下载。
[2]透明《动态代理的前世今生》(《程序员》2005年第1期)
附:我用DELPHI实现的动态代理代码可以在这里下载,还在改进中,仅供参考。(链接已更新)