用Delphi实现动态代理(1):概述

一、问题

所谓动态代理(Dynamic Proxy),要先从GoF的Proxy模式说起。

假设有一个IFoo接口:

{$M+}
IFoo = Interface( IInterface )
['{3A85E46D-F3D4-4D9C-A06C-4E7C1BAC9361}']
Function doSth( dummy : Integer ) : String; StdCall;
Procedure bar; StdCall;
End;
{$M-}

接口提供者对其作了实现,并提供了一个工厂方法(Factory Method)来向用户提供了实例的创建,如下:

TFooImpl = class(TInterfacedObject, IFoo)
Protected
Function doSth( dummy : Integer ) : String; StdCall;
Procedure bar; StdCall;
end;

(*
TFooImpl的实现代码,略
*)

// 创建实例的工厂方法
Function GetFooObject( ) : IFoo;
Begin
Result := TFooImpl.Create As IFoo;
End;

作为这个接口的用户,只有IFoo接口的定义,并且可以一个创建的实现IFoo接口的实例,但没有实现类TFooImpl的定义和实现代码。如果现在用户需要为IFoo.doSth增加事务功能(假设doSth被实现为对数据库作更新操作),要怎么办?
 

二、静态代理解决方案

GoF的Proxy模式就是解决方案之一:

如图所示,首先要定义一个新的IFoo接口实现--TStaticProxy。其中用了一个属性FImpl记录了TFooImpl的实例。然后在 TStaticProxy中实现doSth和bar,并且将不需要变更的bar函数直接委托给FImpl处理,而在doSth的实现里加入事务处理即可。 TStaticProxy的代码大致如下:

TStaticProxy = class(TInterfacedObject, IFoo)
Private
FImpl : IFoo;
Protected
Function doSth( dummy : Integer ) : String; StdCall;
Procedure bar; StdCall;
public
constructor Create( aImpl : IFoo );
end;

{ TStaticProxy }

constructor TStaticProxy.Create(aImpl: IFoo);
begin
FImpl := aImpl;
end;

// 新的doSth,加入了数据库事务处理
function TStaticProxy.doSth(dummy: Integer): String;
begin
DBConn.StartTransaction;
Try
FImpl.doSth( dummy );
DBConn.Commit;
Except
DBConn.Rollback;
End;
end;

procedure TStaticProxy.bar;
begin
FImpl.bar;
end;

// 新的工厂方法
Function NewGetFooObject( ) : IFoo;
Begin
Result := TStaticProxy.Create( GetFooObject( ) ) As IFoo;
End;

现在,用户只需要用新的工厂方法NewGetFooObject来代替原来的GetFooObject即可,新的工厂方法返回的实例就已经具备了为doSth增加事务处理的能力。

可见,我们通过了一个Proxy类代理了所有对IFoo接口的操作,相当于在Client与TFooImpl之间插入了额外的处理代码,在某种程度上,这就是AOP所谓的“横切”。
 

三、静态代理的问题

但上面这种静态代理解决方案还是很麻烦:

  • 首先,如果IFoo的成员函数很多的话,必须要一一为它们加上代理实现;
  • 其次,如果在应用中有很多接口需要代理时,就必须一一为它们写这样的专用代理类;
  • 第三,需要变更代理功能时,需要修改所有的代理类
  • ……

当然,这些问题也不是非要“动态代理”不可。

比如第一点。如果用户拥用TFooImpl的代码,就可以直接从TFooImpl派生一个TNewFooImpl,然后在其中Override一下TFooImpl中的doSth即可。最后修改工厂方法,改为创建并返回TNewFooImpl的实例。如下图所示:

问题就在于必须拥用TFooImpl的代码才行,而这在很多时候是做不到的--除非不是用DELPHI,而是如 Python一类的动态语言。在一些比如组件容器,比如远程接口调用,还有像“虚代理”(就是当创建FImpl代价很高时,就在创建时只创建代理类,然后 在真正需要时才创建FImpl的实例)这样的应用,通常都是只能得到接口定义和相应的实例。

正因为没有TFooImpl的代码,所以我们不得不用比较麻烦一些的静态代理。可以注意一下前面的代码,其中并没有用到TFooImpl类。

至于第二第三两个问题,如果对于像C++那样支持GP(泛型编程)的语言,则可以通过template来实现。可惜在Delphi.net以前,并不支持这个Feature。

再说对于像组件容器或是通用远程接口调用这样的应用,被代理的接口要到运行时才可以确定的情况下,静态代理一点用也没有--因为它必须实现所要代理 的接口,如上面那个TStaticProxy就实现了IFoo接口。这一点GP也是无能为力的,因为模板毕竟只是一种编译期动态化的特性。
 

四、动态代理

所以我们需要“动态代理”。这个概念是JAVA在JDK1.3中提出的,就是在java.lang.reflect中的那个proxy[1]。因为 DELPHI是所有静态编译语言中,动态性最强的,所以也是可以实现这样的功能,我已经用DELPHI完成了一个与JAVA类似的动态代理实现[2]。

一个典型的动态代理应用如下:

//  因为TMInterfaceInvoker需要类实例,所以原来这个工厂方法需要改成返回对象
Function GetFooObject : TObject;
Begin
Result := TFooImpl.Create( );
End;

TFooInvHandler = class( TInterfacedObject, IMInvocationHandler )
private
FImpl : IFoo;
FInvoker : IMMethodInterceptor;
Protected
Procedure Invoke( const aProxy : TMDynamicProxy;
const aContext: TMMethodInvocation ); StdCall;
Public
Constructor Create;
end;

{ TFooInvHandler }

constructor TFooInvHandler.Create;
Var
tmp : TObject;
begin
tmp := GetFooObject( ); // tmp是实例,不会影响引用计数
FInvoker := TMInterfaceInvoker.Create( tmp );
Supports( tmp, IFoo, FImpl ); // 将对象转为接口实例,
// 主要是为了将引用计数设置为1,以免对象被无意中释放
end;

Procedure TFooInvHandler.Invoke( const aProxy : TMDynamicProxy;
const aContext: TMMethodInvocation );
begin
If ( aContext.MethMD.Name = 'doSth' ) Then
Begin
DBConn.StartTransaction;
Try
FInvoker.Invoke( aContext );
DBConn.Commit;
Except
DBConn.Rollback;
End;
End
Else
FInvoker.Invoke( aContext );
end;

// 新的工厂方法
Function NewGetFooObject( ) : IFoo;
Begin
Result := TMDynamicProxy.Create( TypeInfo( IFoo ), TFooInvHandler.Create( ) ) As IFoo;
End;

上面代码实现的功能与那个静态代理的例子是一样的。

首先看一下新的工厂方法。其实现与静态代理是比较相似的,重要的不同点就在于:这个TMDynamicProxy是一个通用的代理类,不像 TStaticProxy,必须根据要实现的接口来定制。而TMDynamicProxy实现对接口调用的动态代理功能和附加功能的切入是通过两个参数实 现,根据运行时传入参数的不同,它就可以“动态”地实现对不同接口的代理,以及不同附加功能的切入。

所以它叫做“动态代理”。

不过因为DELPHI毕竟还是一种编译型的语言,所以对于这个动态代理的实现除了大量使用DELPHI本身强大的RTTI功能以外,还用到了像 Thunk这样的技术,在某种程度上侵入了编译器的“势力范围”,但这也是不得已的。幸好这些仅存在于动态代理本身的实现中,对于使用动态代理的应用,基 本上可以做到跟JAVA中差不多。

TMDynamicProxy的构造参数中,TypeInfo( IFoo )就是传入的接口类型信息,用于实现动态接口实现。而TFooInvHandler的实例则是切入的附加功能代码。

所以接下来要关注的就是这个TFooInvHandler的实现。TFooInvHander是一个实现了IMInvocationHandler的类。而IMInvocationHandler的定义如下:

  IMInvocationHandler = Interface
Procedure Invoke( const aProxy : TMDynamicProxy;
const aContext : TMMethodInvocation ); StdCall;
End;

TMMethodInvocation = class
public
Property IID : TGUID;
Property CallID : Integer;
Property MethMD : TIntfMethEntry;
Property Params[aIndex : Integer] : Variant;
Property RetVal : Variant;
End;

这个接口只定义了一个Invoke方法,TMDynamicProxy将所有对被代理接口的方法调用都代理到此方法上。类型为 TMMethodInvocation的参数aContext记录了方法调用的上下文,包括接口ID、方法ID、Method Meta Data(方法的RTTI元数据)、参数列表、返回值等。

在例子中实现的TFooInvHandler的Invoke方法实现中,判断被调用的方法名是否是“doSth”,如果是则插入事务处理,否则将 Invoke委托给一个IMMethodInterceptor接口实例处理。我设计此接口是准备用于实现AOP中的动态拦截器,但在此例中,这个实例对 应的是一个TMInterfaceInvoke类对象。这个类也是一个像TMDynamicProxy一样的通用类,用于实现将Invoke调用 Dispatch到具体实现类对象的相应方法调用上。因为它是通过TObject的一些RTTI特性实现,这些功能无法通过接口实例得到,所以需要将原来 的工厂方法返回的接口对象改为一般类对象,返回TObject类型并不失一般性(仍然是没有TFooImpl的实现代码)。

注意,在TFooInvHandler的实现中,只判断了方法名,没有判断接口ID。这是因为在这个例子中,它只处理IFoo接口的调用,所以不需要。但如果是AOP应用,一个拦截器通常可以用于多个接口,这里就必须要判断IID了。

整个动态代理应用的结构大致如下图:

有了这样一个动态代理,除了可以像这个例子一样切入事务处理以外,还可以很方便地切入如安全性检查,LOG等。这样的话,用DELPHI来实现AOP也不成问题了。

(未完待续)


参考文献:
[1]透明《动态代理的前世今生》(《程序员》2005年第1期)
[2]我用DELPHI实现的动态代理代码可以在这里下载,还在改进中,仅供参考。