.Net平台AOP技术概览

4.1.Net平台AOP技术概览

.Net平台与Java平台相比,由于它至今在服务端仍不具备与unix系统的兼容性,也不具备类似于Java平台下J2EE这样的企业级容器,使得.Net平台在大型的企业级应用上,常常为人所诟病。就目前而言,.Net平台并没有提供AOP技术的直接实现,而微软在未来对于.Net的发展战略目标,我们仍未可知。但我相信微软对于目前炙手可热的AOP技术应该不会视而不见。也许在未来的.Net平台下,会出现类似于Spring那样的轻量级IoC容器,加上O/R Mapping的进一步实现与完善,随着Windows Server操作系统的逐步推新,.Net平台对于企业级系统开发的支持会越来越多。

AOP技术在.Net平台中的应用,相较于Java平台而言,还远不够成熟,功能也相对较弱,目前能够投入商用的AOP工具几乎没有。借鉴Java开源社区的成功,.Net平台下AOP工具的开发也都依托于开源社区的力量。众多开源爱好者,仍然在坚持不懈对AOP技术进行研究和实践,试图找到AOP技术与.Net之间的完美结合点,从而开发出真正能够商用的功能强大的AOP工具。就目前而言,大部分在.Net平台下的AOP工具,大部分均脱胎于Java平台下的AOP工具,例如Spring.Net之于SpringEos之于AspectJ。由于Java平台和.Net平台在语言机制上的相似性,使得它们在实现AOP的技术机制上,大体相似,无非是利用静态织入动态织入的方式,完成对aspect的实现。

目前在.Net平台下的AOP大部分仍然处于最初的开发阶段,各自发布的版本基本都是beta版。其中较有代表性的AOP工具包括AspectC#Spring.NetEos等。

AspectC#是基于Castle动态代理技术实现的。Castle动态代理技术利用了.NetEmit技术,生成一个新的类去实现特定的接口,或者扩展一个已有的类,并将其委托指向IInterceptor接口的实现类。通过Castle动态代理技术,就可以拦截方法的调用,并将Aspect的业务逻辑织入到方法中。利用Castle动态代理技术,最大的缺陷是它只对虚方法有效,这限制了AspectC#的一部分应用。

Spring.Net从根本意义上来说,是对Spring工具从Java平台向.Net平台的完全移植。它在AOP的实现上与Spring几乎完全相似,仍然利用了AOP联盟提供的拦截器、Advice等实现AOPSpring.Net的配置文件也与Spring相同。

Eos采用的是静态织入的技术。它提供了独有的编译器,同时还扩展了C#语法,以类似于AspectJ的结构,规定了一套完整的AOP语法,诸如aspectadvicebeforeafterpointcut等。Eos充分的利用了.Net中元数据的特点,以IL级的代码对"方面(aspect)"进行织入,这也使得它的性能与其他AOP工具比较有较大的提高。

4.2 .Net平台下实现AOP的技术基础

如前所述,在.Net平台下实现AOP,采用的方式主要是静态织入动态织入的方式。在本文中,我将充分利用.Net的技术特性,包括元数据、Attribute.Net Remoting的代理技术,将其综合运用,最终以动态织入的方式实现AOP公共类库。本节将介绍实现AOP所必需的.Net知识。

4.2.1元数据(metadata
4.2.1.1
元数据概述

元数据是一种二进制信息,用以对存储在公共语言运行库(CLR)中可移植可执行文件 (PE) 或存储在内存中的程序进行描述。在.Net中,如果将代码编译为 PE 文件时,便会将元数据插入到该文件的一部分中,而该代码被编译成的Microsoft 中间语言 (MSIL),则被插入到该文件的另一部分中。在模块或程序集中定义和引用的每个类型和成员都将在元数据中进行说明。执行代码时,运行库将元数据加载到内存中,并引用它来发现有关代码的类、成员、继承等信息。

元数据以非特定语言的方式描述在代码中定义的每一类型和成员。它存储的信息包括程序集的信息,如程序集的版本、名称、区域性和公钥,以及该程序集所依赖的其他程序集;此外,它还包括类型的说明,包括类型的基类和实现的接口,类型成员(方法、字段、属性、事件、嵌套的类型)。

.Net Framework中,元数据是关键,该模型不再需要接口定义语言 (IDL) 文件、头文件或任何外部组件引用方法。元数据允许 .NET 语言自动以非特定语言的方式对其自身进行描述,此外,通过使用Attribute,可以对元数据进行扩展。元数据具有以下主要优点:

1. 自描述文件

公共语言运行库(CLR)模块和程序集是自描述的。模块的元数据包含与另一个模块进行交互所需的全部信息。元数据自动提供COMIDL的功能,允许将一个文件同时用于定义和实现。运行库模块和程序集甚至不需要向操作系统注册。运行库使用的说明始终反映编译文件中的实际代码,从而提高应用程序的可靠性。

2.语言互用性和更简单的基于组件的设计

元数据提供所有必需的有关已编译代码的信息,以供您从用不同语言编写的 PE 文件中继承类。您可以创建用任何托管语言(任何面向公共语言运行库的语言)编写的任何类的实例,而不用担心显式封送处理或使用自定义的互用代码。

3Attribute

.NET Framework允许在编译文件中声明特定种类的元数据(称为Attribute)。在整个 .NET Framework 中到处都可以发现Attribute的存在,Attribute用于更精确地控制运行时程序如何工作。另外,用户可以通过自定义属性向 .NET Framework 文件发出用户自己的自定义元数据。

4.2.1.2元数据的结构

PE文件中与元数据有关的主要包括两部分。一部分是元数据,它包含一系列的表和堆数据结构。每个元数据表都保留有关程序元素的信息。例如,一个元数据表说明代码中的类,另一个元数据表说明字段等。如果您的代码中有10个类,类表将有10行,每行为1个类。元数据表引用其他的表和堆。例如,类的元数据表引用方法表。元数据以四种堆结构存储信息:字符串、Blob、用户字符串和 GUID。所有用于对类型和成员进行命名的字符串都存储在字符串堆中。例如,方法表不直接存储特定方法的名称,而是指向存储在字符串堆中的方法的名称。

另一部分是MSIL指令,许多MSIL指令都带有元数据标记。元数据标记在 PE 文件的 MSIL 部分中唯一确定每个元数据表的每一行。元数据标记在概念上和指针相似,永久驻留在MSIL中,引用特定的元数据表。元数据标记是一个四个字节的数字。最高位字节表示特定标记(方法、类型等)引用的元数据表。剩下的三个字节指定与所说明的编程元素对应的元数据表中的。如果用C#定义一个方法并将其编译到PE文件中,下面的元数据标记可能存在于PE文件的MSIL部分:
0x06000004

最高位字节 (0x06) 表示这是一个MethodDef标记。低位的三个字节 (000004) 指示公共语言运行库在 MethodDef 表的第四行查找对该方法定义进行描述的信息。

1.1 描述了PE文件中元数据的结构及其每部分的内容:

PE部分 

 

 

PE部分的内容 

 

 

1.1  PE文件中的元数据

4.2.1.3元数据在运行时的作用
由于在MSIL指令中包含了元数据标记,因此,当公共语言运行库(CLR)将代码加载到内存时,将向元数据咨询该代码模块中包含的信息。运行库对Microsoft 中间语言 (MSIL) 流执行广泛的分析,将其转换为快速本机指令。运行库根据需要使用实时 (JIT) 编译器将 MSIL 指令转换为本机代码,每次转换一个方法。例如,有一个类APP,其中包含了Main()方法和Add()方法:
using System; 

public class App
{
   public static int Main()
   {
      int ValueOne = 10;
      int ValueTwo = 20;       
      Console.WriteLine("The Value is: {0}", Add(ValueOne, ValueTwo));
      return 0;
   }
   public static int Add(int One, int Two)
   {
      return (One + Two);
   }
}

通过运行库,这段代码被加载到内存中,并被转化为MSIL
.entrypoint
.maxstack  3
.locals ([0] int32 ValueOne,
         [1] int32 ValueTwo,
         [2] int32 V_2,
         [3] int32 V_3)
IL_0000:  ldc.i4.s   10
IL_0002:  stloc.0
IL_0003:  ldc.i4.s   20
IL_0005:  stloc.1
IL_0006:  ldstr      "The Value is: {0}"
IL_000b:  ldloc.0
IL_000c:  ldloc.1
IL_000d:  call int32 ConsoleApplication.MyApp::Add(int32,int32) /* 06000003 */

JIT 编译器读取整个方法的 MSIL,对其进行彻底地分析,然后为该方法生成有效的本机指令。在 IL_000d 遇到 Add 方法 (/* 06000003 */) 的元数据标记,运行库使用该标记参考 MethodDef 表的第三行。

1.2显示了说明 Add 方法的元数据标记所引用的 MethodDef 表的一部分:

 

 

 

 

相对虚拟地址 (RVA)  

 

 

ImplFlags  

 

 

Flags  

 

 

Name  

 

 

(指向字符串堆) 

 

Signature  

 

 

(指向 Blob 堆) 

 

1.2 元数据标记

该表的每一列都包含有关代码的重要信息。RVA 列允许运行库计算定义该方法的 MSIL 的起始内存地址。ImplFlags Flags 列包含说明该方法的位屏蔽(例如,该方法是公共的还是私有的)。Name 列对来自字符串堆的方法的名称进行了索引。Signature 列对在 Blob 堆中的方法签名的定义进行了索引。

通过利用元数据,我们就可以获得类的相关信息。如上所述,在类APPMethodDef表中,可以获得类APP的三个方法,以及方法的Flags和方法签名。而在.Net中,则提供了反射技术,来支持这种对元数据信息的获取。可以说,正是因为有了元数据,才使得AOP的拦截与织入功能的实现成为可能。

4.2.2 Attribute
4.2.2.1 Attribute
概述

通过对.Net元数据的分析,我们知道可以通过Attribute来扩展元数据。那么什么是Attribute?在MSDN中,Attribute被定义为是被指定给某一声明的一则附加的声明性信息 我们可以通过Attribute来定义设计层面的信息以及运行时(run-time)信息,也可以利用Attribute建立自描述(self-describing)组件。

Attribute可应用于任何目标元素,我们可以通过AttributeTargets枚举指定其施加的目标,AttributeTargets枚举在.Net中的定义如下:
public enum AttributeTargets
{
   All=16383,
   Assembly=1,
   Module=2,
   Class=4,
   Struct=8,
   Enum=16,
   Constructor=32,
   Method=64,
   Property=128,
   Field=256,
   Event=512,
   Interface=1024,
   Parameter=2048,
   Delegate=4096,
   ReturnValue=8192
}

作为参数的AttributeTarges的值允许通过操作来进行多个值的组合,如果你没有指定参数,那么默认参数就是All

不管是.Net Framework提供的Attribute,还是用户自定义Attribute,都是通过[]施加到目标元素上。虽然Attribute的用法与通常的类型不一样,但在.Net内部,Attribute本质上还是一个类。但是,Attribute类的实例化发生在编译时,而非运行时,因而达到了扩展元数据的目的。一个Attribute的多个实例可应用于同一个目标元素;并且Attribute可由从目标元素派生的元素继承。

4.2.2.2自定义Attribute
.Net Framework
支持用户自定义Attribute。自定义Attribute的方法与定义类一样,唯一不同之处是自定义的Attribute必须继承Attribute类。Attribute类包含用于访问和测试自定义Attribute的简便方法。其中,Attribute类的构造函数为protected,只能被Attribute的派生类调用。Attribute类包含的方法主要为:

1.三个静态方法
static Attribute GetCustomAttribute():
这个方法有8种重载的版本,它被用来取出施加在类成员上指定类型的Attribute
static Attribute[] GetCustomAttributes():
这个方法有16种重载版本,用来取出施加在类成员上指定类型的Attribute数组。
static bool IsDefined()
:有八种重载版本,看是否指定类型的定制attribute被施加到类的成员上面。

2.两个实例方法
bool IsDefaultAttribute():
如果Attribute的值是默认的值,那么返回true
bool Match():
表明这个Attribute实例是否等于一个指定的对象。

3.公共属性
TypeId:
得到一个唯一的标识,这个标识被用来区分同一个Attribute的不同实例。

通过自定义Attribute,可使得用户自定义的信息与Attribute施加的类本身相关联。例如,给定一个自定义的 .NET 属性,我们就可以轻松地将调用跟踪Attribute与类的方法相关联:
public class Bar
{
    [CallTracingAttribute("In Bar ctor")]
    public Bar() {}
    [CallTracingAttribute("In Bar.Calculate method")]
    public int Calculate(int x, int y){ return x + y; }
}

请注意,方括号中包含 CallTracingAttribute 和访问方法时输出的字符串。这是将自定义元数据与 Bar 的两个方法相关联的Attribute语法。该自定义的Attribute实现,如下所示:
using System;
using System.Reflection;

[AttributeUsage( AttributeTargets.ClassMembers, AllowMultiple = false )]
public class CallTracingAttribute : Attribute
{    
    private string m_TracingInfo;
    public CallTracingAttribute(string info)
    {
        m_TracingInfo = info;
    }
    public string TracingInfo
    {
        get {return tracingInfo;}
    }
}

通过自定义的CallTracingAttribute,将一段Tracing信息施加到类Bar的构造函数和方法Calculate上。我们可以利用反射技术与Attribute类提供的方法,来获得Bar类的元数据中包含的Attribute信息,如:
public class Test
{
    public static void Main(string[] args)
    {
        System.Reflection.MemberInfo info = typeof(Bar);
        CallTracingAttribute attribute = (CallTracingAttribute) Attribute.GetCustomAttribute(info,typeof(CallTracingAttribute));
        if (attribute != null)
        {
             Console.WriteLine(“Tracing Information:{0}”,attribute.TracingInfo);
        }
    }
}

4.2.2.3上下文(Context)和Attribute
所谓上下文(Context),是指一个逻辑上的执行环境。每一个应用程序域都有一个或多个Context.Net中的所有对象都会在相应的Context中创建和运行。如图1.1所示,它显示了一个安全地存在于Context的对象:

1.1 安全地存在于Context的对象

在图1.1中,上下文(Context)提供了错误传播、事务管理和同步功能,而对象的创建和运行就存在于该Context中。在.Net中,提供了ContextBoundObject类,它代表的含义就是该对象应存在于指定的Context边界中(Object that will be bound with a context)。凡是继承了ContextBoundObject类的类类型,就自动具备了对象与Context之间的关系。事实上,如果一个类对象没有继承自ContextBoundObject,则该对象默认会创建和运行在应用程序域的default context中,而继承自ContextBoundObject的类对象,在其对象实例被激活时,CLR将自动创建一个单独的Context供其生存。

如果需要判定ContextBoundObject类型对象所认定的Context,只需要为该类型对象施加ContextAttribute即可。ContextAttribute类继承了Attribute类,它是一个特殊的Attribute,通过它,可以获得对象需要的合适的执行环境,即Context(上下文)。同时,ContextAttribute还实现了IContextAttributeIContextProperty接口。

由于在施加Attribute时,只需要获取ContextBoundObject类型的Context属性,因此,我们也可以自定义Attribute,只需要该自定义的Attribute实现IContextAttribute即可。IContextAttribute接口的定义如下:
public interface IContextAttribute
{
    bool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg);
    void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg);
}

每个context attributecontext的构造阶段(通常是由ContextBoundObject对象构造动作引发的)会被首先问到IsContextOK,就是说新创建的这个ContextBoundObjec(通过ctorMsg可以知道是哪个对象的哪个构造方法被用来构造ContextBoundObjec对象的)能不能在给定的ctx中存在?这个目的主要是减少应用程序域中潜在的context的数量,如果某些ContextBoundObjec类型可以共用一个有所需特性的执行环境的话,就可以不用再创建新的环境,而只要在已有的环境中构造并执行就好了。

如果ContextBoundObjec类型上设置的所有context attributes都认同给定的context(也即调用代码所处的context)是正确地的(此时IsContextOK均返回true),那么新的ContextBoundObjec就会被绑定到这个context上。否则,只有有一个attribute返回false,就会立即创建一个新的context。然后,CLR会再一次询问每一个context attribute新构造的context是否正确,由于Context已经被重新创建,通常此时返回的结果应为true。那么,Context构造程序就会调用其GetPropertiesForNewContext()方法,context attribute可以用这个方法传入的构造器方法调用信息(ctorMsg)中的context properties列表(ContextProperties)来为新建的context增加所需的context properties

AOP的角度来看,Context类似于前面分析的横切关注点,那么利用我们自定义的Context Attribute,就可以获得对象它所存在的上下文,从而建立业务对象与横切关注点之间的关系。

4.2.3代理(Proxy
在程序设计中使用代理(Proxy),最重要的目的是可以通过利用代理对象,实现代理所指向的真实对象的访问。在GOF的《设计模式》中,将代理(Proxy)模式分为四种:
1
、远程代理(Remote Proxy)。它为一个位于不同的地址空间的对象提供一个局域代表对象。这个不同的地址空间可以是在本机器中,亦可是在另一台机器中。
2
、虚代理(Virtual Proxy)。它能够根据需要创建一个资源消耗较大的对象,使得此对象只在需要时才会被真正创建。
3
、保护代理(Protection Proxy)。它控制对原始对象的访问,如果需要可以给不同的用户提供不同级别的使用权限。
4
、智能引用代理(Smart Reference Proxy)。它取代了简单的指针,在访问一个对象时,提供一些额外的操作。例如,对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它。当第一次引用一个持久对象时,智能引用可以将该对象装入内存。在访问一个实际对象前,检查该对象是否被锁定,以确保其他对象不能改变它。

.Net Remoting中,采用了远程代理(Remote Proxy)模式。采用代理技术,使得对象可以在两个不同的应用程序域(甚至可以是两台不同的机器)之间传递。代理在.Net中被分为透明代理(Transparent Proxy)和真实代理(Real Proxy)。Transparent Proxy的目标是在 CLR 中在 IL 层面最大程度扮演被代理的远端对象,从类型转换到类型获取,从字段访问到方法调用。对 CLR 的使用者来说,Transparent Proxy和被其代理的对象完全没有任何区别,只有通过 RemotingServices.IsTransparentProxy 才能区分两者的区别。Real Proxy则是提供给 CLR 使用者扩展代理机制的切入点,通过从Real Proxy继承并实现 Invoke 方法,用户自定义代理实现可以自由的处理已经被从栈调用转换为消息调用的目标对象方法调用,如实现缓存、身份验证、安全检测、延迟加载等等。

如果我们希望自己定义的代理类能够模仿真实对象的能力,首先就需要实现透明代理。然而,CLR中虽然提供了这样一个透明代理类(_TransparentProxy),我们却不能让自己的代理类从透明代理类派生,也不能通过自定义Attribute、实现标志性接口等方式将代理类标识为透明代理,从而让CLR能够认识。要获取透明代理,必须要提供一个真实代理。一个真实代理是一个从System.Runtime.Remoting.Proxies.RealProxy派生而来的类。这个RealProxy类的首要功能就是帮我们在运行期动态生成一个可以透明兼容于某一个指定类的透明代理类实例。从RealProxy的源代码,可以看出透明代理和真实代理之间的关系:
namespace System.Runtime.Remoting.Proxies
{
  abstract public class RealProxy
  {
    protected RealProxy(Type classToProxy) : this(classToProxy, (IntPtr)0, null){}
    protected RealProxy(Type classToProxy, IntPtr stub, Object stubData)
    {
      if(!classToProxy.IsMarshalByRef && !classToProxy.IsInterface)
        throw new ArgumentException(...);

      if((IntPtr)0 == stub)
      {
        stub = _defaultStub;
        stubData = _defaultStubData;
      }

      _tp = null;

      if (stubData == null)
        throw new ArgumentNullException("stubdata");

      _tp = RemotingServices.CreateTransparentProxy(this, classToProxy, stub, stubData);
    }
    public virtual Object GetTransparentProxy()
    {
      return _tp;
    }
  }
}

很明显,透明代理(Transparent Proxy)是在RealProxy类的构造函数中,调用RemotingServices.CreateTransparentProxy()方法动态创建的。CreateTransparentProxy()方法将把被代理的类型强制转换为统一的由 CLR 在运行时创建的 RuntimeType 类型,进而调用 Internal 方法完成TransparentProxy的创建。通过GetTransparentProxy()方法,就可以获得创建的这个透明代理对象。因此,要定义自己的真实代理对象,只需要继承RealProxy类即可:
using System.Runtime.Remoting.Proxies;

public class MyRealProxy: RealProxy
{
  public MyRealProxy(Type classToProxy): base(classToProxy)
  {
    …
  }
}

透明代理和真实代理在上下文(Context)中,会起到一个侦听器的作用。首先,透明代理将调用堆栈序列化为一个称为消息(Message)的对象,然后再将消息传递给真实代理。真实代理接收消息,并将其发送给第一个消息接收进行处理。第一个消息接收对消息进行预处理,将其继续发送给位于客户端和对象之间的消息接收堆栈中的下一个消息接收,然后对消息进行后处理。下一个消息接收也如此照办,以此类推,直到到达堆栈构建器接收,它将消息反序列化回调用堆栈,调用对象,序列化出站参数和返回值,并返回到前面的消息接收。这个调用链如图1.2所示。

 

1.2 代理(Proxy)侦听消息的顺序

由于透明代理完全等同于其代理的对象,因此,当我们侦听到代理对象被调用的消息时,就可以截取该消息,并织入需要执行的方面逻辑,完成横切关注逻辑与核心逻辑的动态代码织入。

4.3 .Net平台下AOP技术实现
4.3.1
实现原理
根据对.Net中元数据(Metadata)、Attribute、上下文(Context)、代理(Proxy)等技术要素的分析,要在.Net中实现AOP,首先需要获得一个类对象的上下文(Context),则其前提就是这个类必须从System.ContextBoundObject类派生。这个类对象就相当于AOP中的核心关注点,而类对象的上下文则属于AOP的横切关注点。很显然,只需要利用上下文,就可以方便的实现核心关注点和横切关注点的分离。

正如图1.1所示,对象是存在于上下文中的。利用自定义Attribute,可以建立对象与上下文之间的关联。Attribute可以扩展对象的元数据,从而标识出该对象属于其中的一个或多个Aspect。一旦该对象实例被创建或调用时,就可以利用反射技术获得该对象的自定义Attribute。为使得对象的元数据与上下文关联起来,就要求这个自定义的Attribute必须实现接口IContextAttribute

获得了对象的上下文之后,透明代理与真实代理就能够对该对象的方法调用(包括构造函数)进行侦听,并完成消息的传递。传递的消息可以被Aspect截取,同时利用真实代理,也可以完成对业务对象的Decorate,将Aspect逻辑注入到业务对象中。由于在大型的企业系统设计中,横切关注点会包括事务管理、日志管理、权限控制等多方面,但由于方面(Aspect)在技术上的共同特性,我们可以利用.Net的相关技术实现方面(Aspect)的核心类库,所有的横切关注点逻辑,都可以定义为派生这些类库的类型,从而真正在.Net中实现AOP技术。

4.3.2 AOP公共类库
4.3.2.1 AOP Attribute
如上所述,要实现AOP技术,首先需要自定义一个Attribute。该自定义Attribute必须实现IContextAttribute,因此其定义如下所示:
using System;
using System.Runtime.Remoting.Contexts;
using System.Runtime.Remoting.Activation;

[AttributeUsage(AttributeTargets.Class)]
public abstract class AOPAttribute:Attribute,IContextAttribute
{
    private string m_AspectXml;
    private const string CONFIGFILE = @"configuration/aspect.xml";
    public AOPAttribute()                    
    {
        m_AspectXml = CONFIGFILE;
    }   
    public AOPAttribute(string aspectXml)
    {
        this.m_AspectXml = aspectXml;
    }  
    protected abstract AOPProperty GetAOPProperty();

    #region IContextAttribute Members
    public sealed void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
    {
        AOPProperty property = GetAOPProperty();    
        property.AspectXml = m_AspectXml;     
        ctorMsg.ContextProperties.Add(property);
    }
    public bool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg)
    {
        return false;
    }
}

AOPAttribute除了继承System.Attribute类之外,关键之处在于实现了接口IContextAttribute接口。接口方法GetPropertiesForNewContext()其功能是向Context添加属性(Property)集合,这个集合是IConstructionCallMessage对象的ContextProperties属性。而接口方法IsContextOK(),则用于判断Context中是否存在指定的属性。这个方法会在Context的构造阶段(通常是由被施加了AOPAttribute的业务对象在创建时引发的)被调用,如果返回false,会创建一个新的Context

GetAOPProperty()方法是一个受保护的抽象方法,继承AOPAttribute的子类将重写该方法,返回一个AOPProperty对象。在这里,我们利用了Template Method模式,通过该方法创建符合条件的AOPProperty对象,并被GetPropertiesForNewContext()方法添加到属性集合中。

抽象类AOPAttribute是所有与方面有关的Attribute的公共基类。所有方面的相关Attribute均继承自它,同时实现GetAOPProperty()方法,创建并返回与之对应的AOPProperty对象。

4.3.2.2 AOP Property
ContextProperties
是一个特殊的集合对象,它存放的是对象被称为Context Property,是一个实现了IContextProperty接口的对象,这个对象可以为相关的Context提供一些属性。IContextProperty接口的定义如下:
public interface IContextProperty
{
    string Name { get; }
    bool IsNewContextOK(Context newCtx);
    void Freeze(Context newCtx);
}

IContextProperty接口的Name属性,表示Context Property的名字,Name属性值要求在整个Context中必须是唯一的。IsNewContextOK()方法用于确认Context是否存在冲突的情况。而Freeze()方法则是通知Context Property,当新的Context构造完成时,则进入Freeze状态(通常情况下,Freeze方法仅提供一个空的实现)。

由于IContextProperty接口仅仅是为Context提供一些基本信息,它并不能完成对方法调用消息的截取。根据对代理技术的分析,要实现AOP,必须在方法调用截取消息传递,并形成一个消息链Message Sink。因此,如果需要向所在的ContextTransparent Proxy/Real Proxy中植入Message SinkContext Property还需要提供Sink的功能。所幸的是,.Net已经提供了实现MessageSink功能的相关接口,这些接口的命名规则为IContributeXXXSinkXXX代表了四种不同的SinkEnvoyClientContextServerContextObject。这四种接口有其相似之处,都只具有一个方法用于返回一个IMessageSink对象。由于我们需要获取的透明代理对象,是能够穿越不同的应用程序域的。在一个应用程序域收到其他应用程序域的对象,则该对象在.Net中被称为Server Object,该对象所处的Context也被称为Server Context。我们在.Net中实现AOP,其本质正是要获得对象的Server Context,并截取该Context中的方法调用消息,因而Context Property对象应该实现IContributeServerContextSink接口。事实上,也只有IContributeServerContextSink接口的GetServerContextSink()方法,才能拦截包括构造函数在内的所有方法的调用。

因此,AOP Property最终的定义如下:
using System;
using System.Runtime.Remoting.Activation;
using System.Runtime.Remoting.Contexts;
using System.Runtime.Remoting.Messaging;

public abstract class AOPProperty : IContextProperty, IContributeServerContextSink
{
    private string m_AspectXml;
    public AOPProperty()
    {
        m_AspectXml = string.Empty;          
    }
    public string AspectXml
    {
        set { m_AspectXml = value; }
    }
    protected abstract IMessageSink CreateAspect(IMessageSink nextSink);
    protected virtual string GetName()
    {
        return "AOP";
    }
    protected virtual void FreezeImpl(Context newContext)
    {
        return;
    }
    protected virtual bool CheckNewContext(Context newCtx)
    {
        return true;
    }

    #region IContextProperty Members
    public void Freeze(Context newContext)
    {
        FreezeImpl(newContext);
    }
    public bool IsNewContextOK(Context newCtx)
    {
        return CheckNewContext(newCtx);
    }
    public string Name
    {
        get { return GetName(); }
    }
    #endregion

    #region IContributeServerContextSink Members
    public IMessageSink GetServerContextSink(IMessageSink nextSink)
    {
        Aspect aspect = (Aspect)CreateAspect(nextSink);           
        aspect.ReadAspect(m_AspectXml,Name);           
        return (IMessageSink)aspect;
    }
    #endregion
}

在抽象类AOPProperty中,同样利用了Template Method模式,将接口IContextProperty的方法的实现利用受保护的虚方法延迟到继承AOPProperty的子类中。同时,对于接口IContributeServerContextSink方法GetServerContextSink(),则创建并返回了一个Aspect类型的对象,Aspect类型实现了IMessageSink接口,它即为AOP中的方面,是所有方面(Aspect)的公共基类。

AOPProperty类作为抽象类,是所有与上下文有关的Property的公共基类。作为Context Property应与Aspect相对应,且具体的AOPProperty类对象应在AOPAttribute的子类中创建并获得。

4.3.2.3 AspectPointCut
Aspect
类是AOP的核心,它的本质是一个Message Sink,代理正是通过它进行消息的传递,并截获方法间传递的消息。Aspect类实现了IMessageSink接口,其定义如下:
public interface IMessageSink
{
    IMessage SyncProcessMessage(IMessage msg);
    IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink);
    IMessageSink NextSink { get; }    
}

IMessageSink接口利用NextSink将多个Message Sink连接起来,以形成一个消息接收器链;而SyncProcessMessage()AsyncProcessMessage()方法则分别用于同步和异步操作,它们在消息传递的时候被调用。

注意方法SyncProcessMessage()中的参数,是一个IMessage接口类型的对象。在.Net中,IMethodCallMessageIMethodReturnMessage接口均继承自IMessage接口,前者是调用方法的消息,而后者则是方法被调用后的返回消息。利用这两个接口对象,就可以获得一个对象方法的切入点。因此,一个最简单的Aspect实现应该如下:
    public abstract class Aspect : IMessageSink
    {     
        private IMessageSink m_NextSink;

        public AOPSink(IMessageSink nextSink)
        {
            m_NextSink = nextSink;        
        }
        public IMessageSink NextSink
        {
            get { return m_NextSink; }
        }
        public IMessage SyncProcessMessage(IMessage msg)
        {
            IMethodCallMessage call = msg as IMethodCallMessage;
            if (call == null)
            {
                 return null;
            }

            IMessage retMsg = null;
            BeforeProcess();
            retMsg = m_NextSink.SyncProcessMessage(msg);
            AfterProcess();
            return retMsg;
        }
        public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
        {
             return null;
        }
        private void BeforeProcess()
        {
             //
方法调用前的实现逻辑;
        }
        private void AfterProcess()
        {
             //
方法调用后的实现逻辑;
        }
    }

注意在方法SyncProcessMessage()中,IMessageSink对象m_NextSink通过Aspect构造函数赋值为业务对象的透明代理,在调用m_NextSinkSyncProcessMessage()方法时,此时调用的是与该透明代理对应的真实代理。如果在消息接收链中还存在代理,在方法调用将会沿着消息链不断的向后执行。而对于一个业务对象而言,此时的IMessage即为该对象中被调用的方法,SyncProcessMessage(msg)就相当于执行了该方法。而在m_NextSink.SyncProcessMessage(msg)方法前后执行BeforeProcess()AfterProcess(),就完成了对方法的截取,并将自己的Aspect逻辑织入到业务对象的方法调用中,从而实现了AOP

然而对于AOP技术的实际应用而言,并非业务对象的所有方法都需要被截取进而进行方面的织入。也即是说,切入点(PointCut)必须实现可被用户定义。而所谓切入点,实际上是业务对象方法与Advice之间的映射关系。在.Net中,我们可以通过集合对象来管理这个映射关系。由于Advice包括Before AdviceAfter Advice,因此,在Aspect类中应该定义两个集合对象:
private SortedList m_BeforeAdvices;
private SortedList m_AfterAdvices;

在添加PointCut时,是将方法名和具体的Advice对象建立映射,根据SortedList集合的特性,我们将方法名作为SortedListKey,而Advice则作为SortedListValue
        protected virtual void AddBeforeAdvice(string methodName, IBeforeAdvice before)
        {
            lock (this.m_BeforeAdvices)
            {
                if (!m_BeforeAdvices.Contains(methodName))
                {
                    m_BeforeAdvices.Add(methodName, before);
                }
            }
        }
        protected virtual void AddAfterAdvice(string methodName, IAfterAdvice after)
        {
            lock (this.m_AfterAdvices)
            {
                if (!m_AfterAdvices.Contains(methodName))
                {
                    m_AfterAdvices.Add(methodName, after);
                }
            }
        }

在向SortedList添加PointCut时,需要先判断集合中是否已经存在该PointCut。同时考虑到可能存在并发处理的情况,在添加PointCut时,利用lock对该操作进行了加锁,避免并发处理时可能会出现的错误。

建立了方法名和Advice的映射关系,在执行SyncProcessMessage()方法,就可以根据IMessage的值,获得业务对象被调用方法的相关属性,然后根据方法名,找到其对应的Advice,从而执行相关的Advice代码:
       public IMessage SyncProcessMessage(IMessage msg)
       {           
             IMethodCallMessage call = msg as IMethodCallMessage;
             string methodName = call.MethodName.ToUpper();
             IBeforeAdvice before = FindBeforeAdvice(methodName);
             if (before != null)
             {
                  before.BeforeAdvice(call);
             }           
             IMessage retMsg = m_NextSink.SyncProcessMessage(msg);
             IMethodReturnMessage reply = retMsg as IMethodReturnMessage;
             IAfterAdvice after = FindAfterAdvice(methodName);
             if (after != null)
             {
                   after.AfterAdvice(reply);
             }
             return retMsg;
        }

其中FindBeforeAdvice()FindAfterAdvice()方法完成keyvalue的查找工作,分别的定义如下:
        public IBeforeAdvice FindBeforeAdvice(string methodName)
        {
            IBeforeAdvice before;
            lock (this.m_BeforeAdvices)
            {
                before = (IBeforeAdvice)m_BeforeAdvices[methodName];
            }
            return before;
        }
        public IAfterAdvice FindAfterAdvice(string methodName)
        {
            IAfterAdvice after;
            lock (this.m_AfterAdvices)
            {
                after = (IAfterAdvice)m_AfterAdvices[methodName];
            }
            return after;
        }

在找到对应的Advice对象后,就可以调用Advice对象的相关方法,完成方面逻辑代码的织入。

那么,PointCut是在什么时候添加的呢?我们可以在AOP的配置文件(Aspect.xml)中配置PointCut,然后在Aspect类中,通过ReadAspect()方法,读入配置文件,获取PointCut以及Aspect需要的信息,包括方法名和Advice对象(通过反射动态创建),在执行AddBeforeAdvice()AddAfterAdvice()方法将PointCut添加到各自的集合对象中:
public void ReadAspect(string aspectXml,string aspectName)
{
    IBeforeAdvice before = (IBeforeAdvice)Configuration.GetAdvice(aspectXml,aspectName,Advice.Before);
    string[] methodNames = Configuration.GetNames(aspectXml,aspectName,Advice.Before);
    foreach (string name in methodNames)
    {
         AddBeforeAdvice(name,before);
    }
    IAfterAdvice after = (IAfterAdvice)Configuration.GetAdvice(aspectXml,aspectName,Advice.After);
    string[] methodNames = Configuration.GetNames(aspectXml,aspectName,Advice.After);
    foreach (string name in methodNames)
    {
         AddAfterAdvice(name,after);
    }   
}

一个Aspect的配置文件示例如下:
<aop>
    <aspect value ="LogAOP">
        <advice type="before" assembly="AOP.Advice" class="AOP.Advice.LogAdvice">
     <pointcut>ADD</pointcut>
     <pointcut>SUBSTRACT</pointcut>
 </advice>
 <advice type="after" assembly="AOP.Advice" class="AOP.Advice.LogAdvice">
     <pointcut>ADD</pointcut>
     <pointcut>SUBSTRACT</pointcut>
 </advice>
    </aspect> 
</aop>

配置文件中,元素Adviceassembly属性和class属性值,是利用反射创建Advice对象所需要的信息。另外,Aspect的名字应与方面的Property名保持一致,因为ReadAspect()方法是通过AOPProperty名字来定位配置文件的Aspect

4.3.2.4 Advice
Aspect类中,已经使用了Advice对象。根据类别不同,这些Advice对象分别实现IBeforeAdvice接口和IAfterAdvice接口:
using System;
using System.Runtime.Remoting.Messaging;

public interface IBeforeAdvice
{
    void BeforeAdvice(IMethodCallMessage callMsg);
}
public interface IAfterAdvice
{
    void AfterAdvice(IMethodReturnMessage returnMsg);
}

接口方法应该实现具体的方面逻辑,同时可以通过IMethodCallMessage对象获得业务对象的调用方法信息,通过IMethodReturnMessage对象获得方法的返回信息。

4.4 .Net平台AOP技术应用案例
1.3.2节,我们已基本实现了AOP的公共类库,这其中包括AOPAttributeAOPPropertyAspectIBeforeAdviceIAfterAdvice。根据这些公共基类或接口,我们就可以定义具体的方面,分别继承或实现这些类与接口。为了展示AOP.Net中的应用,在本节,我将以一个简单的实例来说明。

假定我们要设计一个计算器,它能提供加法和减法功能。我们希望,在计算过程中,能够通过日志记录整个计算过程及其结果,同时需要监测其运算性能。该例中,核心业务是加法和减法,而公共的业务则是日志与监测功能。根据前面对AOP的分析,这两个功能作为横切关注点,将是整个系统需要剥离出来的方面

4.4.1日志方面
    
作为日志方面,其功能就是要截取业务对象方法的调用,并获取之间传递的消息内容。从上节的分析我们知道,方法间的消息可以从IMethodCallMessageIMethodReturnMessage接口对象获得。因此,实现日志方面,最重要的是实现Aspect类中的SyncProcessMessage()方法。此外,也应定义与之对应的AttributeProperty,以及实现日志逻辑的Advice

4.4.1.1日志AttributeLogAOPAttribute
LogAOPAttribute
类继承AOPAttribute,由于AOPAttribute类主要是创建并获得对应的AOPProperty,因此,其子类也仅需要重写父类的受保护抽象方法GetAOPProperty()即可:
       [AttributeUsage(AttributeTargets.Class)]
       public class LogAOPAttribute:AOPAttribute
       {
              public LogAOPAttribute():base()
              {}
              public LogAOPAttribute(string aspectXml):base(aspectXml)
              {}

              protected override AOPProperty GetAOPProperty()
              {
                     return new LogAOPProperty();
              }   
       }

通过对GetAOPProperty()方法的重写,创建并获得了与LogAOPAttribute类相对应的LogAOPProperty,此时在LogAOPAttribute所施加的业务对象的上下文中,所存在的AOP Property就应该是具体的LogAOPProperty对象。

4.4.1.2日志PropertyLogAOPProperty
由于Context Property的名字在上下文中必须是唯一的,因此每个方面的Property的名字也必须是唯一的。因此在继承AOPProperty的子类LogAOPProperty中,必须重写父类的虚方法GetName(),同时在LogAOPProperty中,还应该创建与之对应的Aspect,也即是Message Sink,而这个工作是由抽象方法CreateAspect()来完成的。因此,LogAOPProperty类的定义如下:
       public class LogAOPProperty:AOPProperty
       {
              protected override IMessageSink CreateAspect(IMessageSink nextSink)
              {
                     return new LogAspect(nextSink);
              }
              protected override string GetName()
              {
                     return "LogAOP";
              }
       }

为避免Property的名字出现重复,约定成俗以方面的Attribute名为Property的名字,以本例而言,其Property名为LogAOP

4.4.1.3日志AspectLogAspect
LogAspect
完成的功能主要是将Advice与业务对象的方法建立映射,并将其添加到Advice集合中。由于我们在AOP实现中,利用了xml配置文件来配置PointCut,因此对于所有Aspect而言,这些操作都是相同的,只要定义了正确的配置文件,将其读入即可。对于AspectSyncProcessMessage(),由于拦截和织入的方法是一样的,不同的只是Advice的逻辑而已,因此在所有Aspect的公共基类中已经提供了默认的实现:
       public class LogAspect:Aspect
       {
              public LogAspect(IMessageSink nextSink):base(nextSink)
              {}           
       }

然后定义正确的配置文件:
<aspect value ="LogAOP">
    <advice type="before" assembly=" AOP.Advice" class="AOP.Advice.LogAdvice">
        <pointcut>ADD</pointcut>
 <pointcut>SUBSTRACT</pointcut>
    </advice>
    <advice type="after" assembly=" AOP.Advice" class="AOP.Advice.LogAdvice">
 <pointcut>ADD</pointcut>
 <pointcut>SUBSTRACT</pointcut>
    </advice>
</aspect>

LogAdvice所属的程序集文件为AOP.Advice.dll,完整的类名为AOP.Advice.LogAdvice

4.4.1.4日志AdviceLogAdvice
由于日志方面需要记录方法调用前后的相关数据,因此LogAdvice应同时实现IBeforeAdviceIAfterAdvice接口:
    public class LogAdvice:IAfterAdvice,IBeforeAdvice
    {
        #region IBeforeAdvice Members
        public void BeforeAdvice(IMethodCallMessage callMsg)
        {
            if (callMsg == null)
            {
                return;
            }
            Console.WriteLine("{0}({1},{2})", callMsg.MethodName, callMsg.GetArg(0), callMsg.GetArg(1));
        }
        #endregion

        #region IAfterAdvice Members
        public void AfterAdvice(IMethodReturnMessage returnMsg)
        {
            if (returnMsg == null)
            {
                return;
            }
            Console.WriteLine("Result is {0}", returnMsg.ReturnValue);
        }
        #endregion
    }

BeforeAdvice()方法中,消息类型为IMethodCallMessage,通过这个接口对象,可以获取方法名和方法调用的参数值。与之相反,AfterAdvice()方法中的消息类型为IMethodReturnMessageAdvice所要获得的数据为方法的返回值ReturnValue

4.4.2性能监测方面
性能监测方面与日志方面的实现大致相同,为简便起见,我要实现的性能监测仅仅是记录方法调用前和调用后的时间。

4.4.2.1性能监测AttributeMonitorAOPAttribute
与日志Attribute相同,MonitorAOPAttribute仅仅需要创建并返回对应的MonitorAOPProperty对象:
      [AttributeUsage(AttributeTargets.Class)]
      public class MonitorAOPAttribute:AOPAttribute
      {
             public MonitorAOPAttribute():base()
             {}
             public MonitorAOPAttribute(string aspectXml):base(aspectXml)
             {}
             protected override AOPProperty GetAOPProperty()
             {
                    return new MonitorAOPProperty();
             } 
      }

4.4.2.2性能监测PropertyMonitorAOPProperty
MonitorAOPProperty
的属性名将定义为MonitorAOP,使其与日志方面的属性区别。除定义性能监测方面的属性名外,还需要重写CreateAspect()方法,创建并返回对应的方面对象MonitorAspect
      public class MonitorAOPProperty:AOPProperty
      {
             protected override IMessageSink CreateAspect(IMessageSink nextSink)
             {
                    return new MonitorAspect(nextSink);
             }
             protected override string GetName()
             {
                    return "MonitorAOP";
             }
      }

4.4.2.3性能监测AspectMonitorAspect
MonitorAspect
类的实现同样简单:
    public class MonitorAspect:Aspect
    {
             public MonitorAspect(IMessageSink nextSink):base(nextSink)
             {}
    }

而其配置文件的定义则如下所示:
<aspect value ="MonitorAOP">
    <advice type="before" assembly=" AOP.Advice" class="AOP.Advice.MonitorAdvice">
        <pointcut>ADD</pointcut>
 <pointcut>SUBSTRACT</pointcut>
    </advice>
    <advice type="after" assembly=" AOP.Advice" class="AOP.Advice.MonitorAdvice">
 <pointcut>ADD</pointcut>
 <pointcut>SUBSTRACT</pointcut>
    </advice>
</aspect> 

MonitorAdvice所属的程序集文件为AOP.Advice.dll,完整的类名为AOP.Advice.MonitorAdvice

4.4.2.4性能监测AdviceMonitorAdvice
由于性能监测方面需要记录方法调用前后的具体时间,因此MonitorAdvice应同时实现IBeforeAdviceIAfterAdvice接口:
    public class MonitorAdvice : IBeforeAdvice, IAfterAdvice
    {
        #region IBeforeAdvice Members
        public void BeforeAdvice(IMethodCallMessage callMsg)
        {
            if (callMsg == null)
            {
                return;
            }
            Console.WriteLine("Before {0} at {1}", callMsg.MethodName, DateTime.Now);
        }
        #endregion

        #region IAfterAdvice Members
        public void AfterAdvice(IMethodReturnMessage returnMsg)
        {
            if (returnMsg == null)
            {
                return;
            }
            Console.WriteLine("After {0} at {1}", returnMsg.MethodName, DateTime.Now);
        }
        #endregion
    }

MonitorAdvice只需要记录方法调用前后的时间,因此只需要分别在BeforeAdvice()AfterAdvice()方法中,记录当前的时间即可。

4.4.3业务对象与应用程序
4.4.3.1
业务对象(Calculator
通过AOP技术,我们已经将核心关注点和横切关注点完全分离,我们在定义业务对象时,并不需要关注包括日志、性能监测等方面,这也是AOP技术的优势。当然,由于要利用.Net中的Attribute及代理技术,对于施加了方面的业务对象而言,仍然需要一些小小的限制。

首先,我们应该将定义好的方面Aspect施加给业务对象。其次,由于代理技术要获取业务对象的上下文(Context),该上下文必须是指定的,而非默认的上下文。上下文的获得,是在业务对象创建和调用的时候,如果要获取指定的上下文,在.Net中,要求业务对象必须继承ContextBoundObject类。因此,最后业务对象Calculator类的定义如下所示:
       [MonitorAOP]
       [LogAOP]
       public class Calculator : ContextBoundObject
       {
              public int Add(int x,int y)
              {
                     return x + y;
              }
              public int Substract(int x,int y)
              {
                     return x - y;
              }
       }

[MonitorAOP][LogAOP]正是之前定义的方面Attribute,此外Calculator类继承了ContextBoundObject。除此之外,Calculator类的定义与普通的对象定义无异。然而,正是利用AOP技术,就可以拦截Calculator类的Add()Substract()方法,对其进行日志记录和性能监测。而实现日志记录和性能监测的逻辑代码,则完全与Calculator类的Add()Substract()方法分开,实现了两者之间依赖的解除,有利于模块的重用和扩展。

4.4.3.2应用程序(Program
我们可以实现简单的应用程序,来看看业务对象Calculator施加了日志方面和性能检测方面的效果:
      class Program
      {          
             [STAThread]
             static void Main(string[] args)
             {
                    Calculator cal = new Calculator();
                    cal.Add(3,5);
                    cal.Substract(3,5);
                    Console.ReadLine();
             }
      }

程序创建了一个Calculator对象,同时调用了Add()Substract()方法。由于Calculator对象被施加了日志方面和性能检测方面,因此运行结果会将方法调用的详细信息和调用前后的运行当前时间打印出来,如图4.3所示:

4.3 施加了方面的业务对象调用结果

如果要改变记录日志和性能监测结果的方式,例如将其写到文件中,则只需要改变LogAdviceMonitorAdvice的实现,对于Calculator对象而言,则不需要作任何改变。

在《在.Net中关于AOP的实现》我通过动态代理的技术,基本上实现了AOP的几个技术要素,包括aspectadvicepointcut。在文末我提到采用配置文件方式,来获取advicepointcut之间的映射,从而使得构建aspect具有扩展性。 

细细思考这个问题,我发现使用delegate来构建advice,似乎并非一个明智的选择。我在建立映射关系时,是将要拦截的方法名和拦截需要实现的aspect逻辑建立一个对应关系,而该aspect逻辑确实可以通过delegate,使其指向一族方法签名与该委托完全匹配的方法。这使得advice能够抽象化,以便于具体实现的扩展。然而,委托其实现毕竟是面向过程的范畴,虽然在.Net下,delegate本身仍是一个类对象,然而在创建具体的委托实例时,仍然很难通过配置文件和反射技术来获得。 

考虑到委托具有的接口抽象的本质,也许采用接口的方式来取代委托更为可行。在之前的实现方案中,我为advice定义了两个委托:

public delegate void BeforeAOPHandle(IMethodCallMessage callMsg);

public delegate void AfterAOPHandle(IMethodReturnMessage replyMsg);

 

我可以定义两个接口IBeforeActionIAfterAction,分别与这两个委托相对应:

    public interface IBeforeAdvice

    {

        void BeforeAdvice(IMethodCallMessage callMsg);

}

    public interface IAfterAdvice

    {

        void AfterAdvice(IMethodReturnMessage returnMsg);

}

通过定义的接口,可以将AdviceAspect分离开来,这也完全符合OO思想中的责任分离原则。

(注:为什么要为Advice定义两个接口?这是考虑到有些Aspect只需要提供BeforeAfter两个逻辑之一,如权限控制,就只需要before Action。)

 

那么当类库使用者,要定义自己的Aspect时,就可以定义具体的Advice类,来实现这两个接口,以及具体的Advice逻辑了。例如,之前提到的日志Aspect

    public class LogAdvice:IAfterAdvice,IBeforeAdvice

    {

        #region IBeforeAdvice Members

 

        public void BeforeAdvice(IMethodCallMessage callMsg)

        {

            if (callMsg == null)

            {

                return;

            }

            Console.WriteLine("{0}({1},{2})", callMsg.MethodName, callMsg.GetArg(0), callMsg.GetArg(1));

        }

 

        #endregion

 

        #region IAfterAdvice Members

 

        public void AfterAdvice(IMethodReturnMessage returnMsg)

        {

            if (returnMsg == null)

            {

                return;

            }

            Console.WriteLine("Result is {0}", returnMsg.ReturnValue);

        }

 

        #endregion

}

 

而在AOPSink类的派生类中,添加方法名与Advice映射关系(此映射关系,我们即可理解为AOPpointcut)时,就可以添加实现了Advice接口的类对象,如:

         public override void AddAllBeforeAdvices()

         {

            AddBeforeAdvice("ADD",new LogAdvice());

            AddBeforeAdvice("SUBSTRACT", new LogAdvice());

         }

         public override void AddAllAfterAdvices()

         {

             AddAfterAdvice("ADD",new LogAdvice());

            AddAfterAdvice("SUBSTRACT", new LogAdvice());

         }

由于LogAdvice类实现了接口IBeforeAdviceIAfterAdvice,因此诸如new LogAdvice的操作均可以通过反射来创建该实例,如:

IBeforeAdvice beforeAdvice =

(IBeforeAdvice)Activator.CreateInstance("Wayfarer.AOPSample","Wayfarer.AOPSample.LogAdvice").Unwrap();

CreateInstance()方法的参数值,是完全可以通过配置文件来配置的:

<aop>

    <aspect value ="LOG">

         <advice type="before" assembly="Wayfarer.AOPSample" class="Wayfarer.AOPSample.LogAdvice">

             <pointcut>ADDpointcut>

             <pointcut>SUBSTRACTpointcut>

         advice>

         <advice type="after" assembly="Wayfarer.AOPSample" class="Wayfarer.AOPSample.LogAdvice">

             <pointcut>ADDpointcut>

             <pointcut>SUBSTRACTpointcut>

         advice>

    aspect>   

aop>

这无疑改善了AOP实现的扩展性。

 

《在.Net中关于AOP的实现》实现AOP的方案,要求包含被拦截方法的类必须继承ContextBoundObject。这是一个比较大的限制。不仅如此,ContextBoundObject对程序的性能也有极大的影响。我们可以做一个小测试。定义两个类,其中一个类继承ContextBoundObject。它们都实现了一个累加的操作:

class NormalObject

    {

        public void Sum(int n)

        {

            int sum = 0;

            for (int i = 1; i <= n; i++)

            {

                sum += i;

            }

            Console.WriteLine("The result is {0}",sum);

            Thread.Sleep(10);

        }

    }

 

    class MarshalObject:ContextBoundObject

    {

        public void Sum(int n)

        {

            int sum = 0;

            for (int i = 1; i <= n; i++)

            {

                sum += i;

            }

            Console.WriteLine("The result is {0}", sum);

            Thread.Sleep(10);

        }

    }

然后执行这两个类的Sum()方法,测试其性能:

    class Program

    {

        static void Main(string[] args)

        {

            long normalObjMs, marshalObjMs;

            Stopwatch watch = new Stopwatch();

            NormalObject no = new NormalObject();

            MarshalObject mo = new MarshalObject();

 

            watch.Start();

            no.Sum(1000000);

            watch.Stop();

            normalObjMs = watch.ElapsedMilliseconds;

            watch.Reset();

 

            watch.Start();

            mo.Sum(1000000);

            watch.Stop();

            marshalObjMs = watch.ElapsedMilliseconds;

            watch.Reset();

 

            Console.WriteLine("The normal object consume {0} milliseconds.",normalObjMs);

            Console.WriteLine("The contextbound object consume {0} milliseconds.",marshalObjMs);           

            Console.ReadLine();

        }

    }

得到的结果如下:


 

从性能的差异看,两者之间的差距是比较大的。如果将其应用在企业级的复杂逻辑上,这种区别就非常明显了,对系统带来的影响也是非常巨大的。

 

另外,在《在.Net中关于AOP的实现》文章后,有朋友发表了很多中肯的意见。其中有人提到了AOPAttribute继承ContextAttribute的问题。评论中提及微软在以后的版本中,不再提供ContextAttribute。如果真是如此,确有必要放弃继承ContextAttribute的形式。不过,在.Net中,除了ContextAttribute之外,还提供有一个接口IContextAttribute,该接口的定义为:

public interface IContextAttribute

{

        void GetPropertiesForNewContext(IConstructionCallMessage msg);

        bool IsContextOK(Context ctx, IConstructionCallMessage msg);       

}

此时只需要将原来的AOPAttribute实现该接口即可:

    public abstract class AOPAttribute:Attribute,IContextAttribute//ContextAttribute

    {

        #region IContextAttribute Members

        public void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)

        {

            AOPProperty property = GetAOPProperty();

            property.AspectXml = m_AspectXml;

            property.AspectXmlFlag = m_AspectXmlFlag;

            ctorMsg.ContextProperties.Add(property);

        } 

        public bool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg)

        {

            return false;

        }

        #endregion

}

不知道,IContextAttribute似乎也会在未来的版本中被取消呢? 

然而,从总体来看,这种使用ContextBoundObject的方式是不太理想的,也许它只能停留在实验室阶段,或许期待微软在未来的版本中得到更好的解决! 

当然,如果采用CastleDynamicProxy技术,可以突破必须继承CotextBoundObject的局限,但随着而来的局限却是AOP拦截的方法,要求必须是virtual的。坦白说,这样的限制,不过与前者乃五十步笑百步的区别而已。我还是期待有更好的解决方案。 

说到AOP的几大要素,在这里可以补充说说,它主要包括:

1Cross-cutting concern  

  在OO模型中,虽然大部份的类只有单一的、特定的功能,但它们通常会与其他类有着共同的第二需求。例如,当线程进入或离开某个方法时,我们可能既要在数据访问层的类中记录日志,又要在UI层的类中记录日志。虽然每个类的基本功能极然不同,但用来满足第二需求的代码却基本相同。 

2Advice  

  它是指想要应用到现有模型的附加代码。例如在《在.Net中关于AOP的实现》的例子中,是指关于打印日志的逻辑代码。

 3Point-cut

   这个术语是指应用程序中的一个执行点,在这个执行点上需要采用前面的cross-cutting concern。如例子中,执行Add()方法时出现一个Point-cut,当方法执行完毕,离开方法时又出现另一个Point-cut。 

4Aspect  

Point-cutadvice结合在一起就叫做aspect。如例子中的LogMonitor。在对本例的重构中,我已经AOPSink更名为Aspect,相应的LogAOPSinkMonitorAOPSink也更名为LogAspectMonitorAspect。 

以上提到的PointCutAdviceAOP技术中,通常称为动态横切技术。与之相对应的,是较少被提及的静态横切。它与动态横切的区别在于它并不修改一个给定对象的执行行为,相反,它允许通过引入附加的方法属性和字段来修改对象固有的结构。在很多AOP实现中,将静态横切称为introduce或者mixin。 

在开发应用系统时,如果需要在不修改原有代码的前提下,引入第三方产品和API库,静态横切技术是有很大的用武之地的。从这一点来看,它有点类似于设计模式中提到的Adapter模式需要达到的目标。不过,看起来静态横切技术应比Adapter模式更加灵活和功能强大。 

例如,一个已经实现了收发邮件的类Mail。然而它并没有实现地址验证的功能。现在第三方提供了验证功能的接口IValidatable

public interface IValidatable

{

    bool ValidateAddress();

}

如果没有AOP,采用设计模式的方式,在不改变Mail类的前提下,可以通过Adapter模式,引入MailAdater,继承Mail类,同时实现IValidatable接口。采用introduce技术,却更容易实现该功能的扩展,我们只需要定义aspect:(注:java代码,使用了AspectJ

import com.acme.validate.Validatable; 

public aspect EmailValidateAspect

{

   declare parents: Email implements IValidatable; 

   public boolean Email.validateAddress(){

     if(this.getToAddress() != null){

          return true;

     }else{

          return false;

     }

   }

}

 从上可以看到,通过EmailValidateAspect方面,为Emailintroduce了新的方法ValidateAddress()。非常容易的就完成了Email的扩展。

 我们可以比较一下,如果采用Adapter模式,原有的Email类是不能被显示转换为IValidatable接口的,也即是说如下的代码是不可行的:

Email mail = new Email();

IValidatable validate = ((IValidatable)mail).ValidateAddress();

要调用ValidateAddress()方法,必须通过EmailAdapter类。然而通过静态横切技术,上面的代码就完全可行了。

原文地址:https://www.cnblogs.com/mz121star/p/2690666.html