热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

用Delphi实现动态代理(1):概述[MentalStudio]猛禽[Blog]一、问题所谓动态代理(DynamicProxy),要先从GoF的Proxy模式说起。假设有一个IFoo接口:{$

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

[Mental Studio]猛禽[Blog]

一、问题

所谓动态代理(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实现的动态代理代码可以在这里下载,还在改进中,仅供参考。

[Mental Studio]猛禽 Feb.03-05, Feb.27


推荐阅读
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 入门指南:使用FastRPC技术连接Qualcomm Hexagon DSP
    本文旨在为初学者提供关于如何使用FastRPC技术连接Qualcomm Hexagon DSP的基础知识。FastRPC技术允许开发者在本地客户端实现远程调用,从而简化Hexagon DSP的开发和调试过程。 ... [详细]
  • 长期从事ABAP开发工作的专业人士,在面对行业新趋势时,往往需要重新审视自己的发展方向。本文探讨了几位资深专家对ABAP未来走向的看法,以及开发者应如何调整技能以适应新的技术环境。 ... [详细]
  • 理解浏览器历史记录(2)hashchange、pushState
    阅读目录1.hashchange2.pushState本文也是一篇基础文章。继上文之后,本打算去研究pushState,偶然在一些信息中发现了锚点变 ... [详细]
  • 在Qt框架中,信号与槽机制是一种独特的组件间通信方式。本文探讨了这一机制相较于传统的C风格回调函数所具有的优势,并分析了其潜在的不足之处。 ... [详细]
  • 在PB数据窗口中,错误处理技术主要针对两类问题进行优化:一是由用户不当数据输入引发的错误,二是程序执行过程中因代码缺陷导致的异常。高效的应用程序设计需确保无论出现哪种类型的错误,系统都能有效应对,保证稳定性和用户体验。通过引入先进的错误检测与恢复机制,可以显著提升系统的健壮性和可靠性。 ... [详细]
  • 本文介绍了SIP(Session Initiation Protocol,会话发起协议)的基本概念、功能、消息格式及其实现机制。SIP是一种在IP网络上用于建立、管理和终止多媒体通信会话的应用层协议。 ... [详细]
  • 本文深入探讨了Go语言中的接口型函数,通过实例分析其灵活性和强大功能,帮助开发者更好地理解和运用这一特性。 ... [详细]
  • 解决JavaScript中法语字符排序问题
    在开发一个使用JavaScript、HTML和CSS的Web应用时,遇到从SQLite数据库中提取的法语词汇排序不正确的问题,特别是带重音符号的字母未按预期排序。 ... [详细]
  • 本文详细介绍了如何利用 Bootstrap Table 实现数据展示与操作,包括数据加载、表格配置及前后端交互等关键步骤。 ... [详细]
  • 本文详细探讨了在Web开发中常见的UTF-8编码问题及其解决方案,包括HTML页面、PHP脚本、MySQL数据库以及JavaScript和Flash应用中的乱码问题。 ... [详细]
  • v8对象机制1.概述v8中每一个API对象都对应一个内部实现对象(堆对象)2.对象创建过程(1)v8::internal::Factory类: ... [详细]
  • td{border:1pxsolid#808080;}参考:和FMX相关的类(表)TFmxObjectIFreeNotification ... [详细]
  • 函子(Functor)是函数式编程中的一个重要概念,它不仅是一个特殊的容器,还提供了一种优雅的方式来处理值和函数。本文将详细介绍函子的基本概念及其在函数式编程中的应用,包括如何通过函子控制副作用、处理异常以及进行异步操作。 ... [详细]
  • 本文详细介绍了Oracle 11g中的创建表空间的方法,以及如何设置客户端和服务端的基本配置,包括用户管理、环境变量配置等。 ... [详细]
author-avatar
着袖
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有