用.Net的动态代码生成功能实现AOP

随着AOP的概念被越来越多的讨论,动态代码生成技术也正受到更多人的重视。动态代码生成可以分为动态生成源代码、动态生成中间代码、动态生成机器码等几个层次。动态生成源代码最为简单,各种WEB开发环境都可以理解为这一层次,例如,用ASP很容易写出如下代码:

< % Response.Write  " <script>alert('hello world');</script> "  % >

而动态生成中间代码或机器代码则要复杂得多,一般来说很难人工完成,大多数情况都有借助专门的库或工具来完成。ATL中有个小thunk的东东,就是这种想法的一个应用:它通过动态生成机器码完成了窗口消息处理之前从HWND到窗口类指针的转换,从而免去了从长长的列表中查找指定的类的过程(参见《深入解析ATL》中文版第410页)。而在JAVA和.Net这样的托管环境下,类似于C++的直接向内存中写机器码的方法变得不怎么现实,取而代之的是使用运行环境本身为我们提供的动态代码生成的方法。
.Net提供了Emit和CodeDOM两种方式用于动态生成代码,前一种方式用于生成可以即时执行、即时保存成二进制文件的机器码,后一种方式更多的适合于生成人能读懂的源代码。我在这里主要使用了Emit方式。
我的目标是这样的:仿照Spring中AOP的思想,通过动生成类代码,实现指定类的动态代理,从而可以对指定的方法实行运行时的拦截。例如:

     public   interface  MyInterface
    
{
        
void Say(String name);
    }


    
public   class  MyClass: MyInterface
    
{
        
public void Say(String name)
        
{
            Console.WriteLine(
"大家好,我是" + name);
        }

    }

类MyClass实现了接口MyInterface,在其方法Say中向屏幕输出一字符串。下面的测试程序很容易验证程序在正常工作:

    MyInterface inst  =   new  MyClass();
    inst.Say(
" Billy " );

我现在要做的是,在调用方法Say之前和之后做一些事,也就是说要拦截方法Say的调用,并且尽可能少的修改用户代码,从而,在用户看来,他的类和接口的定义是不变的,他仍然是调用MyInterface的一个实例inst上的方法,但在调用的时候我已经可以有所察觉。简单的思想就是用户调用Say的时候实际上调用的是另一个方法,而在这个方法中再去调用真正的MyClass::Say()。
若要让用户仍在MyInterface这个接口下声明变量,并且调用的是另一个方法,能完成此工作的方式就是使用多态,也就是方法的晚绑定:

     public   class  MyClass2: MyClass  { ... }

但这样做会遇到一个在JAVA世界中不存在的问题,就是除非声明了virtual,否则C#的函数是早绑定的,在子类中不能重写没有被声明为virtual的方法。而之前我说过,我要尽量少的让用户修改代码,特别是类和接口声明是不要修改的,所以这里我决定让我的类不继承于MyClass,而是让它直接实现MyInterface接口,从而它和MyClass类成为了兄弟:

     public   class  MyClass2: MyInterface
    
{
        
private MyClass c = new MyClass();
        
public void Say(String name)
        
{
            
//Do something
            c.Say(name);
            
//Do something
        }

    }

我要实现的最终结果是这样的,但问题很明显,这段代码谁来写?总不能让用户再为每个想使用AOP的类都写这样的代码吧。使用工具来自动生成源代码?也不是个好主意。那么最好的方式就是在运行时根据接口MyInterface来动态产生这样一个代理类,并且提供一个工厂,当用户想要生成新的MyClass对象时,我的程序来接管这部分工作,我先生成MyClass和MyClass2两个类的实例c1和c2,并且把c1作为c2的一个成员变量;动态为MyClass2类生成所有的MyInterface接口方法的实现,在实现中都是调用c1的相应的方法,最后把c2以接口MyInterface的身份返回给用户,用户代码变成了这样:

    MyInterface inst  =  DynamicProxyBuilder.GetProxyObject < MyInterface, MyClass > ();
    inst.Say(
" Billy " );

这里我使用了C# 2.0提供的泛型功能,从而免去了返回对象的类型转换,当然如果使用C# 1.x,只要稍做改动就可以了。
DynamicProxyBuilder是我专门用来生成动态代理的类,GetProxyObject是它唯一的公共方法:

         public   static  InterfaceType GetProxyObject < InterfaceType, BaseType > ()
        
{
            Type interfaceType 
= typeof(InterfaceType);
            Type baseType 
= typeof(BaseType);

            
//取得代理类的动态类型,如果不存在,就创建它
            Type targetType = GetProxyType(interfaceType, baseType);

            
//创建代码对象和被代理对象的实例,并且返回代理对象
            return (InterfaceType)Activator.CreateInstance(targetType, Activator.CreateInstance(baseType));
        }


其功能是返回一个实现了接口InterfaceType的能代理BaseType类型对象的对象,而这个对象类型,就是前面所说的MyClass2类型。这个MyClass2类是在编译阶段不存在的,它在第一次被请求时动态生成,生成的方式是调用DynamicProxyBuilder类的GetProxyType方法:

         private   static  Type GetProxyType(Type interfaceType, Type baseType)
        
{
            
// 这里省略了各种有效性检查

            
//动态类型的名称,也就相当于前面说的MyClass2
            String proxyClassName = "_dynamicproxy." + baseType.FullName + "Proxy";

            
if (!typeList.ContainsKey(proxyClassName)) //检查所请求类型是否已经生成了
            //如果没有生成,下面来生成

                ModuleBuilder module 
= GetDynamicModule(); //调用自定义方法,取得动态module对象

                
//定义新的类型,继承于object类,实现interfaceType接口
                TypeBuilder tb = module.DefineType(proxyClassName, TypeAttributes.Public | TypeAttributes.Class, typeof(System.Object), new Type[] { interfaceType });

                
//定义用来保存被代理对象的变量
                FieldBuilder fb = tb.DefineField("_proxyLocalObj", interfaceType, FieldAttributes.Public);

                
//调用自定义的函数,生成构造函数,构造函数要求一个参数,就是被代理对象的引用
                CreateCtor(baseType, tb, fb);

                
//遍历接口中所有的方法,为其生成代理方法
                foreach (MethodInfo method in interfaceType.GetMethods())
                
{
                    CreateMethod(baseType, tb, method, fb);
                }


                
//把刚刚生成的类型加入到动态类型列表中,供以后直接使用
                typeList.Add(proxyClassName, tb.CreateType());
            }

            
return typeList[proxyClassName];
        }


这里用到了多个自定义的函数和变量。其中typeList是一个保存了所有动态类型的键-值对:

         private   static  Dictionary < String, Type >  typeList  =   new  Dictionary < string , Type > ();

而GetDynamicModule方法用于返回一个动态创建的模块:

         private   static  ModuleBuilder GetDynamicModule()
        
{
            
if (dynamicAssembly == null)
            
{
                dynamicAssembly 
= AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("DynamicAssembly"), AssemblyBuilderAccess.Run);
                moduleBuilder 
= dynamicAssembly.DefineDynamicModule("MainModule");
            }


            
return moduleBuilder;
        }


要创建方法的实现,就不得不涉及到Emit和中间语言了,这里先看相对比较简单的构造函数:

         private   static   void  CreateCtor(Type baseType, TypeBuilder tb, FieldBuilder fb)
        
{
            
//反射出Object类的构造函数
            Type objType = Type.GetType("System.Object");
            ConstructorInfo objCtor 
= objType.GetConstructor(new Type[0]);

            ConstructorBuilder cb 
= tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { baseType });
            ILGenerator cbil 
= cb.GetILGenerator();
            cbil.Emit(OpCodes.Ldarg_0);
            cbil.Emit(OpCodes.Call, objCtor);
            cbil.Emit(OpCodes.Ldarg_0);
            cbil.Emit(OpCodes.Ldarg_1);
            cbil.Emit(OpCodes.Stfld, fb);
            cbil.Emit(OpCodes.Ret);
        }

因为我们的代理类是从System.Object类继承来的,所以这里首先反射出Object类的构造函数,稍后调用它,完成最基础的构造工作。然后调用TypeBuilder类的DefineConstructor方法,创建这个类自己的构造函数。最后一个参数“new Type[] { baseType }”表明,这个动态类的构造函数有一个构造函数,接收baseType类型,用来把这个对象赋给其_proxyLocalObj域,从而方便以后代理方法中对真正方法的调用。
后面就是一连串的IL代码,ILGenerator类用来把IL代码写到函数体里。这段IL代码相当于以下C#代码:

         public  ProxyClass(BaseType obj)
            :
base ()
        
{
            _proxyLocalObj 
= obj;
        }

看完了构造函数,再来看代理类的重头戏——代理方法的实现:

         private   static   void  CreateMethod(Type baseType, TypeBuilder tb, MethodInfo method, FieldBuilder fb)
        
{
            
//这一段用来反射出被代理方法的参数列表:
            ParameterInfo[] pis = method.GetParameters();
            Type[] paramTypes 
= new Type[pis.Length];
            
for (int i = 0; i < pis.Length; i++)
            
{
                paramTypes[i] 
= pis[i].ParameterType;
            }


            
//准备好真正要调用的方法,供稍后调用
            MethodInfo targetMethod = baseType.GetMethod(method.Name, paramTypes);

            
//创建代理方法,使用接口中相应方法的信息,并去掉其抽象方法属性
            MethodBuilder methodB = tb.DefineMethod(method.Name, method.Attributes & (~MethodAttributes.Abstract), method.CallingConvention, method.ReturnType, paramTypes);

            
//开始写入IL代码,实现代理过程
            ILGenerator il = methodB.GetILGenerator();

            
//这一行表示方法被调用前的动作
            il.EmitWriteLine("Before call");

            
//开始向栈中压参数,下面两行压入第一个参数,它是被代理对象,将来在被代理方法中被认为“this指针”
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, fb);

            
//把参数一个一个的压进去,顺序为从左向右:
            for (int i = 1; i <= pis.Length; i++)
            
{
                
switch (i)
                
{   //因参数编号在0-3时IL语言对其有优化,故分别处理
                    case 0: il.Emit(OpCodes.Ldarg_0); break;
                    
case 1: il.Emit(OpCodes.Ldarg_1); break;
                    
case 2: il.Emit(OpCodes.Ldarg_2); break;
                    
case 3: il.Emit(OpCodes.Ldarg_3); break;
                    
default:il.Emit(OpCodes.Ldarg_S, i); break;
                }

            }


            
//参数准备好以后,这里真正调用被代理方法
            il.Emit(OpCodes.Callvirt, targetMethod);

            
//这一行表示方法被调用后的动作
            il.EmitWriteLine("After call");

            il.Emit(OpCodes.Ret);
        }


这段代码比较长,但总体来说所做的就是准备好要调用的被代理方法并在IL中调用它。注意到我在构建方法时,方法的属性一项我是在接口的方法属性中去掉了Abstract(method.Attributes & (~MethodAttributes.Abstract)),因为在接口中它还是个抽象方法,但到了实现类中它已经不抽象了。
在调用被代理方法前首先要向栈中压入方法所需要的参数,参数个数是可以通过接口方法信息而知道的,但在第一个参数之前还要压入被代理对象本身,因为在面向对象语言中,所有非静态方法里都有个this的概念,这个this实际上就来源于这个最靠前的参数。也就是说,一个函数调用:

        MyClass obj;
        obj.Method(param1, param2);

实际上相当于: 

       MyClass_Method(obj, param1, param2);

因此在压入方法真正的参数之前,还要把这个对象本身先压进去。因此,整段IL代码相当于C#代码:

        Console.WriteLine( " Before call " );
        _proxyLocalObj.Method(param1, param2, ...);
        Console.WriteLine(
" After call " );

这里我用两个Console.WriteLine代理了对方法拦截前和后的处理,实际使用时可以换成任何想做的事情,甚至是提交结束这个方法的执行而不再去调用被代理的方法,一切由使用者的意愿来决定。
在使用之前,不要忘了引用这里所需要的名空间:

运行上述代码,屏幕上得到的结果就变成了:
Before call
大家好,我是Billy
After call

如果要得到更为灵活的拦截的控制,可以通过对被代理方法添加代码属性的方式,如:

        [BeforeCallInterceptor(SomeObj)]
        
public   void  Say()  { ... }

在生成动态代理时读取方法的元数据,判断方法到底需要哪种代理需求,使得程序的功能更加灵活。

总结一下,本文中我使用.Net提供给我们的动态代码生成工具——Emit通过运行时为接口生成动态代理的方法实现了.Net下的AOP。虽然我的代码基于C# 2.0,但可以稍做修改,去掉泛型和静态类等部分后就能工作在C# 1.x上。与我上一篇文章《一种基于.Net 2.0的另类AOP》相比起来,这种方法对用户更加透明,唯一所要做的就是原来new一个对象的过程变为调用DynamicProxyBuilder类的方法。它带来的限制是要求用户必须面向接口编程,而不能直接使用类自身的成员变量了,而没有《另类》一文中所提的方法灵活。如果一个类同时实现了多个接口,那么当我们需要其中某一个接口的功能时只需要生成针对这个接口的代理而已。

感觉:
1 通过动态代码生成,可以实现一些很神奇的效果,但这要求必须熟悉中间语言
2 .Net的运行时安全检查功能还是很强大的,当中间语言代码中有一些小错误时(如向栈中压入了变量但没有适当的弹出)就会抛出运行时的异常,防止执行意外的代码。
3 听说在Vista中为了防止缓冲区溢出,OS把数据内存和代码内存明确分开,禁止执行在数据段上的代码。不知道这样会不会对动态代码生成功能有所影响。


附完整的源代码如下:

using  System;
using  System.Collections.Generic;
using  System.Text;
using  System.Reflection.Emit;
using  System.Reflection;

namespace  ConsoleApplication3
{
    
public interface MyInterface
    
{
        
void Say(String name);
    }


    
public class MyClass: MyInterface
    
{
        
public void Say(String name)
        
{
            Console.WriteLine(
"大家好,我是" + name);
        }

    }

   
    
class Program
    
{
        
static void Main(string[] args)
        
{
            MyInterface inst 
= DynamicProxyBuilder.GetProxyObject<MyInterface, MyClass>();
            inst.Say(
"Billy");
        }
 
    }


    
public static class DynamicProxyBuilder
    
{
        
public static InterfaceType GetProxyObject<InterfaceType, BaseType>()
        
{
            Type interfaceType 
= typeof(InterfaceType);
            Type baseType 
= typeof(BaseType);
            Type targetType 
= GetProxyType(interfaceType, baseType);

            
return (InterfaceType)Activator.CreateInstance(targetType, Activator.CreateInstance(baseType));
        }


        
具体实现
    }

}


你可能感兴趣的:(用.Net的动态代码生成功能实现AOP)