Emit Proxy






4.79/5 (15投票s)
.NET Emit 动态生成的代理
引言
有时候我们希望在不修改方法本身代码的情况下,改变方法的调用方式。这可能是因为方法属于第三方库,或者在方法执行中添加额外代码显得不合时宜。我们可能想要记录方法调用、检查权限,甚至缓存结果,这些行为并非针对单个方法,而是希望在多个方法上应用相同的行为。代理提供了一个在调用方法和方法被调用之间的中间层,这个中间层提供了拦截方法调用并根据需要改变其行为的机会。
EmitProxy
是一个动态代理,允许您拦截实现接口的任何对象的全部方法调用。一旦方法调用被拦截,您就可以在调用之前或之后执行操作、修改方法参数、修改返回值,甚至完全绕过执行。EmitProxy 可用于提供日志记录、缓存、身份验证、负载均衡或适用于多个方法且不属于每个单独方法的任何其他通用方面。
EmitProxy
专注于减少动态代理通常带来的开销。为了实现高性能,代理是在运行时使用 .NET Emit 生成的。Emit 框架能够在运行时编写中间语言(编译后的 .NET 代码),从而创建与预编译代码一样快的动态代码。这提供了使用更慢的反射方法在运行时生成代理的动态代理的显著优势。
背景
代理的创建是一个常见且定义明确的设计模式,通常通过编写中间包装对象来执行所需的附加操作。然而,在运行时创建动态代理才是一个有趣的挑战,尤其对于 C# 这样的强类型语言。
.NET 框架在远程处理服务中提供了 RealProxy
类,以帮助创建动态透明代理。RealProxy
的原始用途是创建远程代理,尽管它们可以改编用于创建本地对象的通用代理,但这些代理很难使用,并且性能开销很大。
John Mikhail 开发了 动态 Emit 代理,作为使用 RealProxy 创建动态代理的替代方案。Mikhail 的动态 Emit 代理与本文讨论的 EmitProxy
类似,都使用 .NET Emit 框架在运行时创建代理以提高性能。不幸的是,Mikhail 的代理仍然使用反射来调用被代理对象,从而降低了代理的性能。本文介绍的 EmitProxy
在调用过程中不使用任何反射,因此比 Mikhail 的 Emit 代理性能显著提高。
Using the Code
要使用 EmitProxy
,您需要一个实现您打算代理的接口的对象。
在这个例子中,我们有一个简单的接口,其中有一个 Echo
方法,以及一个实现该接口的对象,它只是返回输入参数。
public interface ITestService
{
string Echo(string echo);
}
public class TestService : ITestService
{
public string Echo(string echo)
{
return echo;
}
}
要创建 EmitProxy
,我们调用 TestService
上的 Proxy
扩展方法。作为参数,我们指定要代理的接口和一个 EmitProxyInterceptor
委托方法。作为返回值,我们得到一个实现该接口的代理对象。
Proxy
方法是 object 的一个扩展方法,允许它被所有类型调用。但是,调用 Proxy
方法的对象必须实现传入的接口(这源于泛型在扩展方法中的有趣用法)。此外,我们传入的类必须是接口,否则会生成运行时错误。
public class EmitProxyExample
{
static void Main(string[] args)
{
ITestService service = new TestService();
service = service.Proxy<ITestService>
(new EmitProxyInterceptor<ITestService>(MyInterceptorMethod));
Console.Out.WriteLine(service.Echo("Hello")); //Prints "Hello World!";
}
public static object MyInterceptorMethod(ITestService proxiedObject,
string methodName, object[] parameters, EmitProxyExecute<ITestService> execute)
{
return execute(objectToInvoke, parameters[0]+" World!");
}
}
现在,我们代理的每个方法调用都会首先通过 EmitProxyInterceptor
,使其有机会查看和修改参数、执行额外行为,甚至跳过方法本身的执行。
传递给拦截器方法的参数是:
objectToInvoke
- 被代理的对象methodName
- 被调用的方法的名称parameters
- 调用方法时使用的参数execute
- 用于执行被代理对象的实际调用的委托
传递的 execute 委托是一个调用我们刚刚代理的方法的方法。它接受一个实现该接口的对象和一个参数对象数组,并返回执行我们刚刚代理的方法的结果。
要求 execute 方法传递一个对象可能看起来是一个奇怪的决定,尤其因为在大多数情况下它只是被代理的对象。这是一个有意的设计决策,允许您将代理用作负载均衡器。每次调用都可以在不同的远程代理上执行,将执行负载分散到一组机器上。
选择性代理
在大多数情况下,处理每个方法调用可能有点过度,您可能只想代理少数特定方法。Proxy 方法提供了一个重载来处理这种情况。您可以选择传递一个谓词委托,它可以过滤掉任何不需要的方法。下面的例子展示了一种只代理以“Debug”开头的方法的情况。
service.Proxy<ITestService>((methodInfo) => {
return methodInfo.Name.StartsWith("Debug");
}, new EmitProxyInterceptor<ITestService>(MyInterceptorMethod));
在大多数情况下,您会根据特定属性进行过滤。为了方便起见,存在一个重载来实现这一点。通过将属性作为泛型参数传递,接口或被代理对象中的任何具有该属性的方法都将调用拦截器。
service.Proxy<ITestService, MyAttribute>
(new EmitProxyInterceptor<ITestService>(MyInterceptorMethod));
属性复制
属性是指示其他框架如何处理特定类或方法的常用方式。将代理传递给其中一个框架时可能会遇到问题,因为代理对象通常不会拥有框架期望的属性。例如,在使用 WCF 时。创建单例服务时,类必须在类上具有 [ServiceBehaviour(InstanceContextMode=InstanceContextMode.Single]
属性,以指示其单例托管行为。如果我们想托管一个代理而不是我们的实际服务,那么我们生成的代理也需要具有相同的 ServiceBehaviour
属性。
为了避免这些问题,Emit Proxy 会从被代理对象复制属性,并将它们附加到代理的类和相应的方法上。这将使 Emit Proxy 在外部框架看来,它拥有与它所代理的对象相同的属性。
幕后
大部分内部代码都是使用 .NET Emit 编写的,用于创建中间语言。发出的中间语言超出了本文的范围,但可以看看 C# 等效的代码,以充分理解 Emit Proxy 的内部工作原理。
当调用 proxy 方法时,代码会在运行时创建一个实现 proxy 接口的新类。对于接口中的每个方法,它都会在类中创建两个方法。一个方法是接口的实现,它只会调用 EmitProxyInterceptor
委托。另一个方法是一个 static
的 execute 方法,它被传递给 EmitProxyInterceptor
,并且将简单地调用适当的底层方法,并在需要时进行类型转换。
代理上面示例中使用的 TestService
将会生成一个等同于以下 C# 类的类。
public class ManualEmitProxy : ITestService
{
public EmitProxyInterceptor<ITestService> Interceptor;
ITestService ProxiedObject;
public string Echo(string echo)
{
return (string)Interceptor
(ProxiedObject, "Echo", new object[] { echo }, ExecuteEcho);
}
public static object ExecuteEcho(ITestService service, params object[] parameters)
{
return service.Echo((string)parameters[0]);
}
}
一旦创建了这个类,它就会被缓存。任何进一步对此类代理的请求都将使用同一个类的新实例。这可能会使创建第一个代理在计算上非常昂贵,但后续代理的创建速度会快得多。
限制
为了让 EmitProxy
工作,它需要一个具有接口的对象。接口向其提供应被代理的方法列表,并提供代理可以赋值的一种变量类型。可以设想开发一个代理来拦截虚方法调用并动态扩展给定的类。然而,虚方法代理会为本已功能强大且简单的代理增加很大的复杂性,因此被排除在此项目范围之外。
不幸的是,EmitProxy
与按引用传递和 out 参数不完全兼容。Emit Proxy 仍然可以使用这些值,但拦截器无法正确查看或设置这些值。
性能
Emit Proxy 的速度被认为是 Emit Proxy 开发中的一个重要方面。进行了测试,使用了一个简单的直通代理,即一个不执行任何操作,只是将操作传递给被代理对象的代理。这样做的目的是不将代理的速度与执行代理逻辑的速度混淆。
执行了两个测试。
- 一个 Echo 方法,如本文前面的示例所示,这是一个传递对象作为参数(在本例中是字符串)并返回对象的示例。
- 第二个测试是一个 Add 方法,它将接受两个整数并返回它们的总和。使用它是因为它需要代理装箱两个参数并返回结果。装箱应该会减慢动态代理的速度,显示其在非理想情况下的性能。
结果显示了 10,000,000 次调用的平均调用时间。
----Echo test----
No Proxy: 9ns
EmitProxy: 119ns
Manual Proxy: 19ns
Manual Emit Proxy: 122ns
Transparent Proxy: 31441ns
Mikhail's Dynamic Emit Proxy: 8384ns
----Add Test (contains boxing)----
No Proxy: 8ns
EmitProxy: 180ns
Manual Proxy: 17ns
Manual Emit Proxy: 183ns
Transparent Proxy: 36498ns
Mikhail's Dynamic Emit Proxy: 10091ns
- 无代理 - 未使用代理,可以视为一次方法调用的时间。
- Emit Proxy - 本文讨论的动态代理。
- 手动代理 - 手动创建实现接口以调用被代理对象的代理。
- 手动 Emit Proxy - 手动编写与 Emit Proxy 生成内容等效的 C# 代码(实际上是为了确认生成的中间语言与编译器生成的代码一样好)。
- 透明代理 - 使用 .NET Remoting RealProxy 实现的动态代理。
- Mikhail 的动态 Emit 代理 - John Mikhail 在 CodeProject 上发布的 Emit 代理。
在动态代理(运行时生成的代理,EmitProxy
、Transparent Proxy 和 Mikhail 的 Dynamic Emit Proxy)中,EmitProxy
的性能是其他两者的 50 倍以上,表明其在性能方面有显著提升。
正如预期的那样,装箱操作确实减慢了动态代理的 Add 测试,但性能下降幅度不足以降低其处理值类型的实用性。
重要的是要记住,这些速度都非常快,在大多数应用程序中,8ns、120ns 和 31,000ns 之间的差异除非这些代理被大量使用,否则是察觉不到的。EmitProxy
的开发还旨在使其体积小(只有一个 CS 文件)、易于使用且速度快,从而即使在非高性能环境中,EmitProxy
也能保持其作为有价值的代码。
历史
- 2009 年 11 月 4 日 - 首次发布
- 2010 年 12 月 26 日 - 修复了接口多重继承问题。代理现在可以从被代理对象复制属性。允许使用谓词委托过滤代理方法。