[翻译] Virtual method interception 虚方法拦截

原文地址:http://blog.barrkel.com/2010/09/virtual-method-interception.html

注:基于本人英文水平,以下翻译只是我自己的理解,如对读者造成未知影响,一切后果自负。文章的排版对齐,行间距实在搞得头大,凑合看一下。

Delphi XE在rtti.pas单元有一个新的类型TVirtualMethodInterceptor。它最初是设计用于DataSnap的认证场景(虽然我不认为只能用在这里)。

这个类做了什么?从本质上讲,它在运行时动态地创建了一种衍生metaclass,并重写父类的虚方法,通过创建一个新的虚拟方法表和并填充到父类。用户可以拦截虚拟函数调用,在代码实现中改变参数,改变返回值,拦截和中断异常或抛出新的异常,或完全取代方法。在概念上,它有点类似于.NET和Java中的动态代理。它就像在运行时从一个类中派生出来的新类,重写虚方法(但不添加新的字段属性),然后将一个实例的运行类型更改为这个新的派生类。

为什么要这么做?两个目的:测试和远程处理。(“虚拟方法拦截”最初用在DataSnap认证部分)。

用一个例子开始:

uses SysUtils, Rtti; 
{$APPTYPE console}

type 
TFoo = class 
// 修改x 的值 
function Frob(var x: Integer): Integer; virtual; 
end;

function TFoo.Frob(var x: Integer): Integer; 
begin 
x := x * 2; 
Result := x + 10; 
end;

procedure WorkWithFoo(Foo: TFoo); 
var 
a, b: Integer; 
begin 
a := 10; 
Writeln(' a = ', a); 
try 
b := Foo.Frob(a); 
Writeln(' result = ', b); 
Writeln(' a = ', a); 
except 
on e: Exception do 
Writeln(' 异常: ', e.ClassName); 
end; 
end;

procedure P; 
var 
Foo: TFoo; 
vmi: TVirtualMethodInterceptor; 
begin 
vmi := nil; 
Foo := TFoo.Create; 
try 
Writeln('拦截以前:'); 
WorkWithFoo(Foo);

vmi := TVirtualMethodInterceptor.Create(Foo.ClassType); 
vmi.OnBefore := procedure(Instance: TObject; Method: TRttiMethod; 
const Args: TArray<TValue>; out DoInvoke: Boolean; out Result: TValue) 
var 
i: Integer; 
begin 
Write('[之前] 调用方法', Method.Name, ' 参数: '); 
for i := 0 to Length(Args) - 1 do 
Write(Args[i].ToString, ' '); 
Writeln; 
end;

// 改变 foo 实例的 metaclass pointer 
vmi.Proxify(Foo);

//所有的虚方法调用以前,都调用了OnBefore事件 
Writeln('拦截以后:'); 
WorkWithFoo(Foo); 
finally 
Foo.Free; 
vmi.Free; 
end; 
end;

begin 
P; 
Readln; 
end.

以下是输出:

你会发现它拦截所有的虚拟方法,包括那些所谓的销毁过程中,不只是我们自己声明的。

(析构函数本身是不包括在内。)

可以完全改变方法的实现,并跳过调用方法的主体:

procedure P; 
var 
Foo: TFoo; 
vmi: TVirtualMethodInterceptor; 
ctx: TRttiContext; 
m: TRttiMethod; 
begin 
vmi := nil; 
Foo := TFoo.Create; 
try 
Writeln('拦截以前:'); 
WorkWithFoo(Foo);

vmi := TVirtualMethodInterceptor.Create(Foo.ClassType);

m := ctx.GetType(TFoo).GetMethod('Frob'); 
vmi.OnBefore := procedure(Instance: TObject; Method: TRttiMethod; 
const Args: TArray<TValue>; out DoInvoke: Boolean; out Result: TValue) 
begin 
if Method = m then 
begin 
DoInvoke := False; //原方法的逻辑不调用了 
Result := 42; 
Args[0] := -Args[0].AsInteger; 
end;

end;

在这里,中断了方法的调用并把返回结果赋值为42,同时修改第一个参数的值,以下输出:

拦截前:
before: a = 10
Result = 30
after: a = 20
拦截后:
before: a = 10
Result = 42
after: a = –10

可以通过一个异常来中断方法的调用:

vmi.OnBefore := procedure(Instance: TObject; Method: TRttiMethod;
      const Args: TArray; out DoInvoke: Boolean; out Result: TValue)
    begin
      if Method = m then
        raise Exception.Create('Aborting');
    end;

输出:

拦截前:
before: a = 10
Result = 30
after: a = 20
拦截后:
before: a = 10
Exception: Exception

不局限于在方法调用前拦截,同时也可以在方法调用后拦截,并可以修改参数和返回值:

m := ctx.GetType(TFoo).GetMethod('Frob');
    vmi.OnAfter := procedure(Instance: TObject; Method: TRttiMethod;
      const Args: TArray; var Result: TValue)
    begin
      if Method = m then
        Result := Result.AsInteger + 1000000;
    end;

以下是输出:

拦截前:
before: a = 10
Result = 30
after: a = 20
拦截后:
before: a = 10
Result = 1000030
after: a = 20

 

如果虚拟方法中抛出了一个异常,可以屏蔽掉这个异常:

function TFoo.Frob(var x: Integer): Integer;
begin
  raise Exception.Create('Abort');
end;

// ...
    m := ctx.GetType(TFoo).GetMethod('Frob');
    vmi.OnException := procedure(Instance: TObject; Method: TRttiMethod;
      const Args: TArray; out RaiseException: Boolean;
      TheException: Exception; out Result: TValue)
    begin
      if Method = m then
      begin
        RaiseException := False;  //屏蔽掉原方法中的异常
        Args[0] := Args[0].AsInteger * 2;
        Result := Args[0].AsInteger + 10;
      end;
    end;

输出:

Before hackery:
before: a = 10
Exception: Exception
After interception:
before: a = 10
Result = 30
after: a = 20

有件事要知道,类TVirtualMethodInterceptor 是没有的, 它通过拦截对象工作,拦截需要一些内存开销,但这是很少的:

PPointer(foo)^ := vmi.OriginalClass;

另一个指针: 类的继承链是被挂钩过程改变了。这可以很容易地显示:

//...
    Writeln('After interception:');
    WorkWithFoo(foo);
    
    Writeln('Inheritance chain while intercepted:');
    cls := foo.ClassType;
    while cls <> nil do
    begin
      Writeln(Format('  %s (%p)', [cls.ClassName, Pointer(cls)]));
      cls := cls.ClassParent;
    end;
    
    PPointer(foo)^ := vmi.OriginalClass;
    
    Writeln('After unhooking:');
    WorkWithFoo(foo);
    
    Writeln('Inheritance chain after unhooking:');
    cls := foo.ClassType;
    while cls <> nil do
    begin
      Writeln(Format('  %s (%p)', [cls.ClassName, Pointer(cls)]));
      cls := cls.ClassParent;
    end;
// ...

输出:

Before hackery:
before: a = 10
Exception: Exception
After interception:
before: a = 10
Result = 30
after: a = 20
Inheritance chain while intercepted:
TFoo (01F34DA8)     //运行时增加的类
TFoo (0048BD84)     //vmi.OriginalClass
TObject (004014F0)
After unhooking:
before: a = 10
Exception: Exception
Inheritance chain after unhooking:
TFoo (0048BD84)
TObject (004014F0)

该功能主要是前期库的基础修补,但希望你可以看到,这不是太难。(提供一种打补丁的方式)

两个问题:

1:这种方法跟写Helper的区别?

2:当类是多层继承时,拦截的是哪个类的虚拟方法?

分享到:

1 条评论

沙发空缺中,还不快抢~