使用 Emit 实现动态代理

最近使用Emit实现了动态代理,也就是说我们可以根据某个接口的实现类(委托类),重新定义一个新的实现类(代理类),并向这个代理类中添加我们自定义的代码而不改变原本的委托类的代码。例如,我们有一个接口ICalcService接口和它的实现类CalcService

public interface ICalcService
{
Decimal Add(Decimal d1, Decimal d2);
Decimal Minus(Decimal d1, Decimal d2);
}

public class CalcService : ICalcService
{
public Decimal Add(decimal d1, decimal d2)
{
Console.WriteLine();
Console.WriteLine("----------调用中-------------------------------------------");
Console.WriteLine("{0} + {1} = {2}", d1, d2, (d1 + d2));
Console.WriteLine();
return d1 + d2;
}

public decimal Minus(decimal d1, decimal d2)
{
return d1 - d2;
}
}

最基本的,我们可以这样调用:

ICalcService calc = new CalcService();
calc.Add(100, 250);
输出结果如下:
图 1

那么,如果我想记录一下ICalcService里面的Add方法的执行时间,以及调用次数呢?于是有以下几种实现方案:

  1. 1、直接在CalcService类的Add方法里面添加相关代码
  2. 2、在调用Add方法的前后添加相关代码
  3. 3、动态代理

对于第 1 种方案,CalcService类的代码应该是这个样子:

private Stopwatch _watch;

public CalcService()
{
_watch = new Stopwatch();
}

public Decimal Add(decimal d1, decimal d2)
{
Console.WriteLine();
Console.WriteLine("----------调用中-------------------------------------------");
_watch.Reset();
_watch.Start();
Decimal result = d1 + d2;
_watch.Stop();
Console.WriteLine("{0} + {1} = {2}", d1, d2, result);
Console.WriteLine();
Console.WriteLine("耗时:{0}ms", _watch.ElapsedMilliseconds);
return result;
}

很明显,这个是可以实现我们需要的功能的,但是如果这个接口里面有 20 个方法,难不成每个方法里面都写相同的代码?很明显不能这样,所以方案 1 排除掉。

对于第 2 种方案,我们在调用的时候可以这么写:

Stopwatch watch = new Stopwatch();
ICalcService calc = new CalcService();

watch.Reset();
watch.Start();
calc.Add(100, 250);
watch.Stop();
Console.WriteLine("耗时:{0}ms", watch.ElapsedMilliseconds);

方案 2 同样可以实现功能,但问题依旧存在,如果我需要调用Add方法 20 次,那我就需要写 20 次多余的代码,所以方案 2 同样排除。

那么如果使用动态代理之后,调用代码应该是这个样子:

var filter = new DefaultFilter();
var instance = new CalcService();
var proxy = ProxyActivator.CreateProxyInstance<ICalcService>(instance, filter);
var rnd = new Random();

Int32 iteration = 20;

for (Int32 i = 0; i < iteration; i++) {
proxy.Add(rnd.Next(1, 9999), rnd.Next(1, 9999));
}

其中,ProxyActivator是创建动态代理类的核心类,CreateProxyInstance (instance,filter)方法的含义就是为接口ICalcService创建一个代理类。其中,DefaultFilter类的实现如下:

public class DefaultFilter : IMethodFilter
{
private Stopwatch _watch;
private static Int32 s_callCount;

public DefaultFilter()
{
_watch = new Stopwatch();
}

public void OnMethodExecuting(MethodExecutingContext context)
{
_watch.Reset();
s_callCount++;
Console.WriteLine("----------调用前-------------------------------------------");
Console.WriteLine("开始调用的时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:fff"));
Console.WriteLine("调用次数:" + s_callCount.ToString());
Console.WriteLine("开始执行参数过滤...");
_watch.Start();
}

public void OnMethodExecuted(MethodExecutedContext context)
{
_watch.Stop();

Console.WriteLine("----------调用后-------------------------------------------");

if (context.Exception != null) {
String[] lines = context.Exception.StackTrace.Split(new String[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) {
Console.WriteLine(line);
}
return;
}

Console.WriteLine("结束调用的时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:fff"));
Console.WriteLine("执行方法:{0} 共计耗时 {1} ms.", context.MethodFullName, _watch.ElapsedMilliseconds);

}
}

这个filterOnMethodExecuting方法会在委托类的方法调用前调用,OnMethodExecuted方法会在委托类的方法调用后调用。所以执行了20Add方法的调用,输入结果如下:

图 2

使用方案 3 的好处不言而喻,不仅省了很多累赘的代码,而且还能实现方法拦截机制。所以,方案 3 是比较好的。

然后重点来说说动态代理的具体实现,动态代理的目的其实就是生成我们需要的代码,那么生成代码比较好的方式就是使用Emit。下面来看看使用EmitICalcService接口生成的代理类是什么样子:

public class CalcServiceProxy : ICalcService
{
private ICalcService _service;
private IMethodFilter[] _filters;

public CalcServiceProxy(ICalcService service, IMethodFilter[] filters)
{
this._service = service;
this._filters = filters;
}

public override decimal Add(decimal num, decimal num2)
{
if (this._filters != null && this._filters.Length > 0) {
IMethodFilter[] filters = this._filters;
for (int i = 0; i < filters.Length; i++) {
IMethodFilter methodFilter = filters[i];
methodFilter.OnMethodExecuting(new MethodExecutingContext(this, " SuziWare.Test.CalcService.Add ", new object[]
{
num,
num2
}));
}
}
decimal num3 = 0m;
Exception exception = null;
try {
num3 = (decimal)this._service.GetType().GetMethod("Add", new Type[]
{
typeof(decimal),
typeof(decimal)
}).Invoke(this._service, new object[]
{
num,
num2
});
}
catch (Exception ex) {
exception = ex;
}
finally {
if (this._filters != null && this._filters.Length > 0) {
foreach (IMethodFilter methodFilter in this._filters.Reverse<IMethodFilter>()) {
methodFilter.OnMethodExecuted(new MethodExecutedContext(this, "SuziWare.Test.CalcService.Add", new object[]
{
num,
num2
}, num3, exception));
}
}
}
return num3;
}
}

所以,我们接下来的目标就是用Emit生成上述代码。具体如何生成,MSDN 上有官方文档: https://msdn.microsoft.com/zh-cn/library/system.reflection.emit.typebuilder(v = vs.110).aspx

最后,我说说我在使用Emit时遇到的唯一的一个大坑。 后缀为_S的指令要慎用,不然就会莫名其妙的报非法单字节分支之类的错误。