颠覆C#王权的“魔比斯环” ― 实现AOP框架的终极利器

时间要追溯到2005 年。那时正在做硕士论文。题目是“AOP framework for .net ”。这个AOP 框架将使用C#2.0 来实现。 这当然没什么令人惊奇的。从理论上说,任何开发语言都可以实现AOP 框架。但要按着AOP 联盟的规范实现这个AOP 框架,大多数的开发语言并不能很容易地完成这项任务。
微软公司在我们心目中是强大的,而出自于微软的C# 自然也会被认为是强大的。使用C# 几乎可以很容易地完成大多数的应用程序(包括桌面、Web 、移动等)。但要用C# 来实现标准的AOP 框架却不是那么容易,甚至有点强人所难。这到底是为什么呢?
一、             AOP 的概念和原理
AOP(Aspect Oriented Programming) 的中文意思是“面向方面编程”。它是10年前由施乐公司提出的。这种技术并不是一种新的编程技术,和OOP(Object Oriented Programming)并不是平级的。它只是OOP的一种扩展。
一个典型的软件系统由核心功能和系统功能组成。 所谓核心功能就是和这个系统相关的业务功能,如在mail服务器中的核心功能是接收和发送电子邮件。系统功能则可以看成是系统的可选功能,如日志、安全等。如果没有了这些功能,mail服务器仍可以照常工作,只是不太安全了,并且无法查找以往的记录。实现这些系统功能,一般的作法是将系统功能的代码和核心功能的代码混在一起。这样做不仅加大了系统设计和实现的难度,而且使设计系统核心功能的程序员必须要考虑这些系统的功能,分散了他们的注意力。
由于在软件开发中遇到以上问题,人们开始设想是否能将软件中的核心功能和系统功能分开,达到单独设计、实现、维护的目的。于是就有人提出在设计和实现时单独进行,当编译时将单独编写的系统功能的代码织入(weaves)到核心功能的代码中,将代码织入的工具叫织入器(weaver)。这种思想经过长期的实践,就逐渐形成了AOP的核心思想,同时也形成了AOP最早的一个实现:AspectJ。
AOP 构建在已有的技术基础之上,同时提供了自己的一套额外机制,也就是 Aspect 机制,对系统进行描述、设计和实现。 AOP 要保证这些机制在概念上简洁,执行效率高。其基本思想是通过分别描述系统的不同关注点(属性或者兴趣)及其关系,以一种松耦合的方式实现单个关注点,然后依靠 AOP 环境的支撑机制,将这些关注点组织或编排成最终的可运行程序。关注点包括普通关注点和系统的贯穿特性。通常可以使用传统的结构化方法和面向对象方法提供的机制,对普通关注点进行处理;使用 Aspect 机制,对贯穿特性进行处理。 系统的贯穿特性范围包括了从高层的关注目标,比如安全和服务质量,到低层的关注目标比如缓存处理等。贯穿特性可以是功能性的,比如事务规则,也可以是非功能的,比如同步和交易管理等。 AOP 将传统方法学中分散处理的贯穿特性实现为系统的一类元素― Aspect ,并将它们从类结构中独立了出来,成为单独的模块。
为了支持上述的系统实现和运行过程, AOP 系统首先必须提供一种声明 Aspect 的机制,包括 Aspect 如何声明,连接点( Aspect 代码与其它代码的交互点)如何定义, Aspect 代码如何定义, Aspect 的参数化程度和可定制性等。 其次,需要提供一种将 Aspect 代码和基础代码组合编排( Weaving )在一起的机制,包括定义编排语言和规则,解决 Aspect 之间潜在的冲突,为组装和执行建立外部约束等。 最后,必须有生成可运行系统的实现机制,包括系统提供什么组合机制,是编译时刻静态组装,还是运行时动态组装;对程序单元分别进行编译的模块化编译机制;对 AOP 机制和现有系统兼容性的规约;对组装结果的验证机制等。
二、C# 限制颇多,实现AOP 框架困难重重
虽然诞生于 10 多年前的 AOP 技术在近几年开始逐渐流行起来。有一些应用很广的软件或框架,如 JBoss Spring ,都使用了 AOP 技术。但这些 AOP 技术大多是基于 java 的。如 AspectJ JBoss AOP Spring AOP 。但是 AOP 技术在 .net 环境下应用得很少,其于 .net AOP 框架也不多。形成这种情况的原因很多。众所周知,实现 AOP 一般有两种方法。一种是利用动态代理或其它技术在程序运行时对方法等信息进行监控(如 JBoss AOP Spring AOP ),即动态 AOP 。另外一种是直接在编译器中支持,就象 AspectJ 。但这种做法实现的难度较大,大多数 AOP 框架的实现都是采用了第一种方法。而动态代理技术在 .net ( 不管是 VB.net C# 都一样 ) 实现是非常困难的,在 .net 中并不象 java 提供了动态代理实现机制。在 .net 中要想实现动态代理必须得直接使用 IL(Intermediate Language) 写代码,这样就必须对 IL 有非常深入的了解。
由于要在 C# 中实现这个 AOP 框架,因此,我不可能再自己做个编译 C# 编译器甚至虚拟机。所以只能使用动态代理的方式来实现。但使用动态代理需要解决两个问题。
1.       如何动态生成代理类。
2.       如何拦截构造方法。
1. 如何动态生成代理类
在阐述问题之前,先让我们看一看什么叫代理类。代理类也是普通的类。只是这个类要继承于被代理的类。而且在代理类中的方法要覆盖父类中的方法。如类 Class1 的代码如下:
class  Class1
{
    
public   virtual   void  Method1() {}
    
public   virtual  String Method2() {  return  s;}
     
    
public  Class1(String s){}    
}
上面的 Class1 有一系列的 virtual 方法,并且有一个构造方法。下面让我们来编写一个代理 Class1 的代理类 ProxyClass
class  ProxyClass : Class1
{
    
public   override   void  Method1(){  base .Method1();  }
    
public   override  String Method2(){ String s  =   base .Method2();   return  s}

    
public  Class2(String s) :  base (s) {}
}
ProxyClass 类可以看出,在使用下面代码创建 Class1 对象后,仍然会得到 Class1 类中相应方法执行后的结果:
Class1 class1 = new ProxyClass(“hello world”);
虽然上面的代码可能大多数程序员都能理解(就是多态调用),但实际上这种代码对基于动态代理技术的 AOP 框架是毫无用处的。之所以叫动态代理,就是在程序执行时自动生成代理类。而上面的代码是静态地写到程序中的,在编译后无法更改。也许有人会说,根据 Class1 可以自动生成 ProxyClass 类的源码,然后在内存或硬盘上编译再调用不就可以了!这种做法可以是可以,但效率却非常的低。大家可以想象,如果每执行一个方法,就生成一堆 C# 源代码,再编译,恐怕 C# 就比脚本语言还慢了。
看到这些,也许那些 C# .net 高手会说,直接生成 MSIL 不就行了。但你要知道,虽然 MSIL 没有汇编复杂,可对于大多数程序员来说,是可望而不可及的。因此,这个技术问题就成为实现 AOP 框架的第一个大障碍。
2. 如何拦截构造方法
AOP 最重要的特性就是对方法的拦截,这其中也包括构造方法。所谓拦截,就是在执行原方法时,要在两个位置来执行我们织入的代码,如日志代码。这两个位置是 before (在原代码执行之前)和 after (在原代码执行之后)。当然,还可以对源代码进行 around (完全取代原来的代码)。这对于普通的方法来说并不算什么,如上面的 Method1 方法。要想在 ProxyClass 类中的 Method1 方法拦截 Class1 类中的方法, before after around 的实现分别如下:
before:
public   override   void  Method1() {  base .Method1();  }

after:
public   override   void  Method1() {   base .Method1(); }

around:
public   override   void  Method1() {   }

但这对于构造方法来说却无法实现。因此,在子类中调用父类中的构造方法只能在方法后面调用,如下面代码所示:
public Class2(String s) : base(s) {...}
   从上面的代码可以看出,构造方法只能实现 before ,而不能实现 after around 。也就是在子类中无论写不写 base 关键字,必须调用父类中的构造方法(如果不写,调用父类中无参数的默认构造方法)。更不会有下面的代码形式:
public Class2(String s) {... base(s);}
因此,如何使构造方法也拥有 after around 特性,就成为实现 AOP 构架的第二个拦路虎。
三、偶遇通往MSIL 世界的“魔比斯环”,C# 王权土崩瓦解
可能每个人都向往着穿越时空。然而有一群幸运的科学家却做到了。这些科学家在一个深谷中偶然发现了一个可以通往另一个世界的大门,这就是“魔比斯环”。通过“魔比斯环”,不仅能穿越时空,同时也将拥有无穷无尽的力量,尽管这么做会有带来一定的危险。
上面所描述的只是科幻电影中的场景。然而在现实世界也确实存在着很多的“魔比斯环”。如我们使用的C# 就是这样。虽然C# 是强大的,但我们必须受到C# 语法和约定的限制。如一个重要限制是任何子类在创建其对象实例时,在子类的构造方法中必须调用父类的一个构造方法,如果不调用,C# 编译器会自动在子类的构造方法加入调用父类构造方法的语句(无参数的默认构造方法)。我们可以先看看下面两个类:

     class  ParentClass
    {
        
public  ParentClass()
        {
            System.Console.WriteLine(
" ParentClass " );
        }
    }

    
class  ChildClass : ParentClass
    {
        
public  ChildClass()
        {
            System.Console.WriteLine(
" ChildClass " );
        }
}

如果在控制台程序中创建ChildClass 对象,将输出如下的结果:
ParentClass
ChildClass
让我们用微软提供的反编译工具ildasm.exe( 位置:C:"Program Files"Microsoft Visual Studio 8"SDK"v2.0"Bin"ildasm.exe) 来反编译exe ,看看为什么会这样。用ildasm.exe 打开exe 后的界面如图1 所示:
 
method public hidebysig specialname rtspecialname 
        
instance void  .ctor() cil managed
{
  
.maxstack  8
  
IL_0000:  ldarg.0
  
IL_0001:  call       instance void ConsoleApplication1.ParentClass::.ctor()
  
IL_0006:  nop
  
IL_0007:  nop
  
IL_0008:  ldstr      "ChildClass"
  
IL_000d:  call       void [mscorlib]System.Console::WriteLine(string)
  
IL_0012:  nop
  
IL_0013:  nop
  
IL_0014:  ret
}

读者可以不必管这些代码是什么意思,只要注意上面黑体字部分。C#编译器已经自动加入了调用ParentClass的构造方法的代码(.ctor()表示构造方法),这一行是去不掉的。找到问题所在,就意味着离成功又近了一步。从中间代码可以看出,如果不让C#编译器加上这行不就行了吗?但我又不能再设计一个C#编译器,因此,只能利用C#原来的编译器。
根据上面所述,现在我只要解决一个问题即可,就是要利用C#编译器来改变C#的规则。这就要求不能使用C#的代码,而构造方法的代码必须直接使用中间语言来完成(并不是象编译器一样直接生成dll或exe)。
这可真给我出了个难题。不过C#和.net framework的强大使用坚信一定有方法解决,只是我暂时没找到而已。
由于大多数使用虚拟机的语言都支持反射功能,因此,我决定碰碰运气,看看是否能通过反射解决这个问题。在.net中有一个命名空间System.Reflection。看了一下其中的类,基本都是用来得到信息的(方法、属性、Assembly的信息),并没有写入信息的(就是写入IL)。当我快要绝望的时候,终于眼前一亮。在System.Reflection中又发现了一个Emit命名空间。Emit的中文含义是“发出”的意思,想到此,我的希望又重新燃起。于是上MSDN查了关于Emit的资料。MSDN上说Emit的主要功能就是通过C#语言绕过C#编译器直接向内存中写入IL,相当于从硬盘上读取IL一样。yeah,这正是我需要的。有了这个,就可以动态生成代理类了。而且速度和静态类是一样的。
在Emit中提高了IL指令的全部映射。如下面的代码将直接用IL生成一个方法及其实现:

 
public   delegate  String MyMethod(String str);

       
private   void  GenerateMethod()
        {
            Type[] argsType 
=  {  typeof (String) };            
            DynamicMethod dm 
=   new  DynamicMethod( " MyMethod1 " ,
                                                 
typeof (String),
            argsType,
            
typeof (Form1).Module);
              
            ILGenerator il 
=  dm.GetILGenerator();

            il.Emit(OpCodes.Ldstr, 
" hello: " );
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Call, 
typeof (String).GetMethod( " Concat " new  Type[]{ typeof (String),  typeof (String)}));
            il.Emit(OpCodes.Ret);
            MyMethod myMethod 
=  (MyMethod)dm.CreateDelegate( typeof (MyMethod));
            MessageBox.Show(myMethod(
" my friends " ));            
        }

 
上面的GenerateMethod 方法动态地生成了一个MyMethod1 方法,并显示如图2 的对话框:
 
在上面代码中主要有三个地方需要提一下:
1. OpCodes 包含了IL 的所有的指令。如 OpCodes.Ldstr 表示将一个字符串压栈(每个方法都有一个操作栈),OpCodes.Call表示调用C#中的其他方法。
2. DynamicMethod 类可以建立动态的普通方法。如果想建立构造方法,可以使用ConstructorInfo。
3 . 所有的IL代码都是通过ILGenerator的Emit方法写入内存的。
Emit 就象电影中的 “魔比斯环”一样,可以很容易地 突破C#的限制,从而穿越了C#世界,到达了另外一个MSIL世界。这将为我们实现另人振奋的功能提供了可能性。
四、在MSIL 世界建立起强大的AOP 帝国
既然能用C# 直接写MSIL ,那么就可容易编写AOP 框架了。虽然这是用C# 代码写MSIL ,但也要对MSIL 有一定的了解,感兴趣的读者可以到微软网站去下载 IL Specification
由于这个AOP 框架的代码十分庞大,在这里只给出了一些代码片段。实现AOP 框架的核心就是生成动态代理类。因此,使用IL 生成代理类的框架是第一步。下面是生成代理类框架的核心代码:
     public  TypeBuilder GenerateType()   //  返回动态代理类的Type
    {
        
string  className  =  GetNewClassName();
        TypeAttributes typeAttributes 
=  TypeAttributes.Class  |  TypeAttributes.Public  |  TypeAttributes.Sealed;
        TypeBuilder typeBuilder 
=   m_EmitClassInfo.Module.DefineType(className,
                                      typeAttributes, m_EmitClassInfo.BaseType);
        
return  typeBuilder;
    }

从上面的代码很容易猜到我要生成一个public sealed 类(不可继承)。接下来就是根据父类生成相应的方法(包括普通方法和构造方法),下面是一些代码片段:

     private   void  GenerateMethod()   //  生成普通的方法
    {
        MethodAttributes methodAttributes 
=  MethodAttributes.Public;
        MethodBuilder methodBuilder 
=  
m_TypeBuilder.DefineMethod(
" __GetMethodInvocation " , methodAttributes,  typeof (IMethodInvocation),  new  Type[] {  typeof (ICallable),  typeof (MethodInfo) });
            m_EmitClassInfo.__GetMethodInvocation 
=  methodBuilder;

       ILGenerator ilGenerator 
=  methodBuilder.GetILGenerator();     
       Label execIfLabel 
=  ilGenerator.DefineLabel();
       Label endIfLabel 
=  ilGenerator.DefineLabel();
       LocalBuilder methodInvocation 
=  ilGenerator.DeclareLocal( typeof (IMethodInvocation));
       ilGenerator.Emit(OpCodes.Ldarg_0);
         
}
    
private   void  GenerateConstructor()   //  生成构造方法
    {
        
try
        {
            MethodAttributes methodAttributes 
=  MethodAttributes.Public;
            CallingConventions callingConventions 
=  CallingConventions.Standard;
            m_BaseConstructorParams 
=  m_EmitClassInfo.ConstructorArgumentsType;
            m_ConstructorParams 
=   new  Type[m_BaseConstructorParams.Length  +   1 ];
            m_BaseConstructorParams.CopyTo(m_ConstructorParams, 
0 );
            m_ConstructorParams[m_BaseConstructorParams.Length] 
=   typeof (IInterceptor);
            m_constructorBuilder 
=  m_TypeBuilder.DefineConstructor(methodAttributes, callingConventions, m_ConstructorParams);
            m_EmitClassInfo.Constructor 
=  m_constructorBuilder;
            m_IlGenerator 
=  m_constructorBuilder.GetILGenerator();
        }
        
catch  (Exception e)
        {
            
throw  e;
        }
}

    使用Emit 不仅仅可以实现AOP 框架,还可以根据用户需要自动生成任何IL 代码。这些IL 代码不会受到VB.net C# 的限制。因此,用户可以通过Emit 来优化生成的中间语言代码,并完成一些C# 无法做到的任务。如果读者想了解Emit 的详细用法,可以参考MSDN 或其他的相关文档。

你可能感兴趣的:(职场,休闲,C#王)