.Net中的AOP系列之《AOP实现类型》

返回《.Net中的AOP》系列学习总目录


本篇目录


Hi,guys!Long time no see! :)

本节的源码本人已托管于Coding上:点击查看

本系列的实验环境:VS 2017。


读完本章后,可能仍然不能实现自己的AOP工具,但应该对两种主要类型(PostSharp和Castle DynamicProxy)的AOP工具的运行原理有了基本的理解。PostSharp是一个在编译时编织的后期编译器,Castle DynamicProxy会在运行时生成一个代理类。虽然前面已经说了很多如何使用这些工具,但是在项目中如果用的AOP工具越多,那么准确地理解它们是如何运行的就越重要。本章的目的,充分理解编译时编织的代表PostSharp和运行时编织的代表DynamicProxy。这些工具都是一流的代表,它们的实现会让我们明白AOP是如何运行的。

AOP是如何跑起来的

先来回顾下第1章的图:


在第1章中使用这张图的目的是说明AOP能够将横切关注点划分到单独的类中,因而与业务逻辑分离并实现了自我封装。所以,作为一个开发者,不必处理相互交织在一起的代码,只需要把它交给AOP工具的切面就可以了。你作为一个开发者,只需要读、写和维护分离的类,然而,需要明白,这些代码跑起来时还是会按照交织到一起的代码进行运行。
直到目前,我们只涉及了这张图的上半部分,这节开始我们谈谈下半部分。编织(或交织,Weaving)是AOP框架将分离的类结合在一起的过程。在类被使用前,编织必须在某个时间点完成。在.Net中,这意味着可以刚好在编译完成后进行编织(编译时编织),或者可以在代码执行期间的某个时间点进行编织(运行时编织)。

下面先来看看最简单的运行时编织。

运行时编织

运行时编织,即编织发生在程序开始运行之后。在其他代码应用切面代码的同时,才会去实例化一个切面。这就是为什么Castle DynamicProxy测试友好的原因,没到运行时,什么都不会发生。


运行时编织工作的方式类似于上面的装饰者/代理模式,但是不需要手动创建装饰类,运行时编织器会在运行时创建这些类。如上图所示,我们仍然只需要创建分离的BusinessModuleLogAspect,但是在运行时,其他类BusinessModuleProxy(姑且称之为)会被创建,用于装饰BusinessModule
如果之前使用过代理模式或者装饰者模式,那么上面的图你会很熟悉。关键区别在于你不需要手动创建代理类BusinessModuleProxy。如果不熟悉也没关系,下一小节会针对这个有用的软件设计模式进行一个新手的讲解。

复习代理模式

讨论动态代理之前先来复习一下代理模式的运行原理。代理模式和装饰者模式都是设计模式,只是有稍微不同的目的和实现,但从AOP的角度看,实际上是一样的。它们都运行你将功能添加到某个类而不需要改变类本身的代码。一般情况下,代理用于另一个对象的替身,通常对实例化真正的对象负责,它和真实的对象有相同的接口。它可以控制访问或提供附加的功能,以及对真实的对象进行控制。


对外来说,所有的程序只知道它正在使用一个具体的接口调用一个对象上的Method1方法。这个对象就是一个代理,在调用真正的方法 Method1之前,它有机会运行自己的代码。一旦方法Method1执行完成,它又有机会运行自己的代码,最后才会返回原始程序的执行结果。

代理模式通常用于给程序表示外部的对象或服务(比如,web service)。在某些程序中,可能会给你一个生成的WCF代理类,它表示某个对象,你可以想象它就在你的程序中运行那样操作它,但在该接口的背后,该代理会发送HTTP调用来完成你的指令。

装饰者模式,在和真实的对象都有相同的接口方面,和代理模式是类似的。但是通常它不对实例化对象负责,因此多个装饰器可以分层在真实对象的顶部。除了LogAspect,还可以有 CacheAspect。它们都有和 BusinessModule相同的接口,以及自己的 BeginMethodEndMethod代码。

从类似AOP功能的角度讲,代理模式和装饰器模式几乎是一样的模式。下面通过一个控制台程序演示一下代理模式:

using static System.Console;
namespace ProxyPatternReview
{
    public interface IBusinessModule
    {
        void Method1();
    }

    public class BusinessModule : IBusinessModule
    {
        public void Method1()
        {
            WriteLine(nameof(Method1));//输出方法名称
        }
    }
}

using static System.Console;
namespace ProxyPatternReview
{
    class Program
    {
        static void Main(string[] args)
        {
            IBusinessModule bm = new BusinessModule();
            bm.Method1();
            ReadKey();
        }
    }
}

上面代码很简单,不多说。现在创建一个扮演BusinessModule代理的类 BusinessModuleProxy,它实现了相同的接口 IBusinessModule,这意味着我们只需要修改上面的 new语句代码即可(现实中要修改IoC配置)。

IBusinessModule bmProxy = new BusinessModuleProxy();
bmProxy.Method1();

Main方法而言,它不关心会获得该模块的任何对象,只要该对象的类实现了 IBusinessModule接口就行。下面是 BusinessModuleProxy的定义,记住它的工作是 BusinessModule的替身,因此它要实例化 BusinessModule,然后继续执行 BusinessModule的方法。

public class BusinessModuleProxy : IBusinessModule
{
    BusinessModule _bm;
    public BusinessModuleProxy()
    {
        _bm = new BusinessModule();
    }
    public void Method1()
    {
        _bm.Method1();
    }
}

这个类几乎是无用的,除了是Main和BusinessModule的中间人之外,没有其他目的。但是你可以在调用真实的方法Method1之前和之后放任何你想执行的代码,如下所示:

public void Method1()
{
    WriteLine($"{nameof(Method1)} begin!");
    _bm.Method1();
    WriteLine($"{nameof(Method1)} end!");
}

看着很熟悉吧?这个代理对象正在扮演拦截切面的角色。我们可以将它用于缓存、日志、线程以及其他任何拦截切面可以实现的东西。只要Main方法获得了一个IBusiness对象(很可能通过IoC容器),无论是否使用了代理类对象,它都会工作。而且,无需改变BusinessModule的任何代码就可以将横切关注点加入真实的BusinessModule。

但等一下,既然代理类能做AOP工具的事情,那么要AOP干什么?在一个有限的环境中,单独地使用代理模式是有效的。但是如果要写一个用于具有不同接口的多个类,那就需要为每个接口都要写代理类了,是不是浪费生命?

如果你只有很少数量的类并且每个类有很少数量的方法,那么使用代理类没多大问题。对于像日志和缓存这样的横切关注点,编写大量的相似的功能性代理类会变得很重复。比如,为两个具有两个方法的接口编写代理类不困难,但想一下,如果有12个接口呢,每个接口又有12个方法,那么就要编写接近144个一样的代理类方法了。
也想想,在一种不确定数量的类需要横切关注点时,比如日志项目可能本身会复用到多个解决方案。通过使用动态代理,就不需要自己手动写所有的这些代理了,只需要让动态代理生成器帮助你工作即可。

动态代理

虽然代理模式不依赖第三方工具就可以实现关注点分离,但是在某些时候需要确定下代理模式本身会变得太重复和模板化。如果你发现自己经常在写一些几乎一样的代理类,只是名字和接口稍微不同而已,那么是时候让工具来为你完成这个工作了。Castle DynamicProxy (以及其他的使用了运行时编织的AOP工具)会通过Reflection, 特别是 Reflection.Emit来生成这些类。不用再在一个代码文件中定义类了,代理生成器会使用Reflection.Emit API来创建类的。

来看一个类似于之前的代理模式的场景,以发微博为例。定义一个简单的接口ISinaService,它有一个发送微博的方法,然后创建该接口的实现类 MySinaService,为了演示需要,只将发送内容输出到控制台:

using static System.Console;
namespace DynamicProxyPractice
{
    public interface ISinaService
    {
        void SendMsg(string msg);
    }

    public class MySinaService:ISinaService
    {
        public void SendMsg(string msg)
        {
            WriteLine($"[{msg}] has been sent!");
        }
    }
}

要使用代理模式或装饰者模式,需要创建一个实现了该接口的类,暂且称之为MySinaServiceProxy,它要对创建真实的对象负责,并且可以实现自己的任何的代码,下面只会在运行真实的对象方法之前和之后输出相应信息,但在真实程序中,你可以实现日志,缓存等等功能:

public class MySinaServiceProxy : ISinaService
{
    private MySinaService _service;
    public MySinaServiceProxy()
    {
        _service = new MySinaService();
    }
    public void SendMsg(string msg)
    {
        WriteLine("Before");
        _service.SendMsg(msg);
        WriteLine("After");
    }
}

问题来了,如果这个服务类有十几个方法,那么意味着我们这个代理类也要有十几个方法。或者,当前这个代理类只适合发微博,那么发微信呢,其他社交媒体呢?所以,每次真实的服务对象要添加或修改方法,你的代理类也必须做相应修改。相反,我们可以在运行时生成那些类。

个人代理生成器

这一小节我们会使用最原始的方法Reflection.Emit生成代理类。它不是动态的,因为它只给MySinaService生成了代理,做个形象的比喻,如果DynamicProxy是趟快车,那我们这个工具只是个石头轮子(还比不上木头轮子)。
这个例子不是打算教大家从头开始写自己的代理生成器,而是让大家明白像Castle DynamicProxy这样高级的工具是如何运作的。

因为Reflection.Emit会生成MySinaServiceProxy,所以不需要在源码中编写了。相反,下面创建了一个返回类型为 MySinaServiceProxy的方法,通过 Activator.CreateInstance和该这个返回类型我们可以创建一个新实例。下面就在Mian方法中完成这个代理生成器:

 static void Main(string[] args)
 {
     //生成一个动态代理类型并返回
     var type = CreateDynamicProxyType();
     //使用Activator和上面的动态代理类型实例化它的一个对象
     var dynamicProxy = Activator.CreateInstance(type,new object[] { new MySinaService()}) as ISinaService;
     //调用真实对象的方法
     dynamicProxy.SendMsg("test msg");
     ReadLine();
 }
 private static Type CreateDynamicProxyType()
 {
     //所有的Reflection.Emit方法都在这里
 }

在运行时构建新类型和运行时构建新类型是相似的:

  1. 创建一个程序集;
  2. 在程序集中创建一个模块;
  3. 使用Reflection.Emit API,创建一个AssemblyName,然后用它在当前域中定义一个AssemblyBuilder,然后使用该AssemblyBuilder创建一个ModuleBuilder。如下所示:
        private static Type CreateDynamicProxyType()
        {
            //所有的Reflection.Emit方法都在这里
            //1 定义AssemblyName
            var assemblyName = new AssemblyName("MyProxies");
            //2 DefineDynamicAssembly为你指定的程序集返回一个AssemblyBuilder
            AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            //3 使用AssemblyBuilder创建ModuleBuilder
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MyProxies");
        }

模块名称和程序集名称可以不一样,这里只是为了简单。

程序集和模块

  • 程序集是编译后的代码类库,包括exe和dll文件。
  • 程序集包含了一些元数据,并且可能包含一个或多个模块,但在实践中很少包含多个模块。
  • 模块包含类。
  • 类包含成员(方法或字段)。

一旦有了ModuleBuilder,就可以用它来构建一个代理类。要构建一个类型Type,需要一个类型名称,特性(public和class),基类(所有类都有基类,即使是object)以及该类型实现的任何接口(这里我们要实现ISinaService)。明白了这些,然后使用ModuleBuilder的DefineType方法,它会返回一个TypeBuilder对象,如下代码:

        private static Type CreateDynamicProxyType()
        {
            //... 省略上面的代码
            TypeBuilder typeBuilder = moduleBuilder.DefineType(
                "MySinaServiceProxy",//要创建的类型的名称
                TypeAttributes.Public|TypeAttributes.Class,//类型的特性
                typeof(object),//基类
                new[] {typeof(ISinaService)}//实现的接口
                );
        }

现在定义了一个类,但它是空的,我们还需要定义字段,构造函数和方法。先从字段开始,这个字段是用来存储真实对象的,以便在代理想调用它时使用。要创建字段,需要字段名称,类型(MySinaService)和特性(这里private)。将这些信息在TypeBuilder的DefineField方法中进行设置,就会返回一个FieldBuilder对象,如下:


FieldBuilder fieldBuilder = typeBuilder.DefineField(
    "_realObject",
    typeof(MySinaService),
    FieldAttributes.Private
    );

此时,这个方法会生成相应的下面的C#代码,只是这种方式更冗长,因为我们做了和编译器通常会为我们做的相似的工作。

   public class MySinaServiceProxy : ISinaService
    {
        private MySinaService _realObject;
    }

下一步,需要构建构造函数,它有一个形参,构造函数体会把形参赋值给字段。我们可以再使用TypeBuilder定义构造函数。要定义它,需要特性(只能是public),调用约定(实例构造函数还是静态构造函数)以及每个形参是参数类型(这里只有一个类型为SinaService的参数)。然后使用DefineConstructor方法来定义构造函数,一旦定义了构造函数,我们需要一种方法将代码放入构造函数中,这里使用GetILGenerator获得构造函数的 ILGenerator对象:

            ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(
                MethodAttributes.Public,
                CallingConventions.HasThis,
                new[] {typeof(MySinaService)}
                );
            ILGenerator ilgenerator = constructorBuilder.GetILGenerator();

在构造函数中,我们只需要一个语句来将参数分配给该字段(如果你计数return的话,就是两个语句,return在C#中是隐含的)。 在调用DefineConstructor时,我们创建了一个指定类型数组的参数,但请注意参数没有名称。 就. NET而言,这只有参数argument 1。(为什么是参数1而不是参数0?因为参数0是this - -当前的实例)。

要将代码放在构造函数中,我们需要使用constuctorBuilder来发出公共中间语言(CIL)操作码。 可能你认为到这里的一切都很复杂,其实这里真的很难。 没有多少人是精通Reflection.Emit的专家,但是因为这是一个简单的操作,我还是能够正确分配OpCodes的。 它包含三个部分:参数0(this),参数1(参数的输入值)和将被赋值的字段。 它们被发送到计算堆栈,所以排序可能看起来很别扭。

//将this加载到计算栈
ilgenerator.Emit(OpCodes.Ldarg_0);
//将构造函数的形参加载到栈
ilgenerator.Emit(OpCodes.Ldarg_1);
//将计算结果保存到字段
ilgenerator.Emit(OpCodes.Stfld, fieldBuilder);
//从构造函数返回
ilgenerator.Emit(OpCodes.Ret);

现在我们已经生成了一个有名称,有一个命名的私有字段和在构造函数中设置私有字段的类型。 为确保此类型实现ISinaService接口,我们需要定义一个名为SendMsg的void方法,它有一个字符串参数,如下列表所示。 使用TypeBuilder的这个信息以及DefineMethod和DefineMethodOverride,我们还需要另一个ILGenerator将代码发送到此方法的方法体中。

            MethodBuilder methodBuilder = typeBuilder.DefineMethod(
                "SendMsg",//方法名称
                MethodAttributes.Public | MethodAttributes.Virtual,//方法修饰符
                typeof(void),//无返回值
                new[] { typeof(string) }//有个字符串参数
                );
            //指定要构建的方法实现了ISinaService接口的SendMsg方法
            typeBuilder.DefineMethodOverride(
                methodBuilder,
                typeof(ISinaService).GetMethod("SendMsg")
                );
            //获取一个ILGenerator将代码添加到SendMsg方法
            ILGenerator sendMsgIlGenerator = methodBuilder.GetILGenerator();

现在我们有一个SendMsg方法,我们需要填写代码。 在MySinaServiceProxy中,SendMsg方法将“Before”输出到Console,然后调用真实的SendMsg方法,随后将“After”写入控制台。 我们需要通过发射OpCodes来处理所有这些事情,如该列表所示。


            //加载字符串变量到计算栈
            sendMsgIlGenerator.Emit(OpCodes.Ldstr, "Before");
            //调用Console类的静态WriteLine方法
            sendMsgIlGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }));
            //将参数argument0(this)加载到栈
            sendMsgIlGenerator.Emit(OpCodes.Ldarg_0);
            //将字段_realObject加载到栈
            sendMsgIlGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
            //加载SendMsg的参数到栈
            sendMsgIlGenerator.Emit(OpCodes.Ldarg_1);
            //调用字段上的SendMsg方法
            sendMsgIlGenerator.Emit(OpCodes.Call, fieldBuilder.FieldType.GetMethod("SendMsg"));
            //加载字符串After到栈
            sendMsgIlGenerator.Emit(OpCodes.Ldstr, "After");
            //调用Console类的静态WriteLine方法
            sendMsgIlGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }));
            //返回
            sendMsgIlGenerator.Emit(OpCodes.Ret);

就这样 ,TypeBuilder对象具有构建我们想要的代理所需的所有信息。最后一步是使用构建器创建类型(并返回它):

return typeBuilder.CreateType();

Opcodes MSDN 文档
点击查看

运行结果见下图,是不是有种既满足(按照预期)又失望(太费力了)的感觉?

正如我之前所说,我们本章远远还不能构建一个完整的动态代理生成器。 要做这个小演示成为有点像样的动态代理生成器将需要大量工作,包括(但不限于):

  • 使其能够代理任何类型,而不是只有MySinaService对象。
  • 使其能够处理这些对象中的任何方法,而不仅仅是SendMsg方法。
  • 使其能够执行任意的切面代码,而不是仅仅在控制台输出点东西。
  • 将其全部包装在一个漂亮的,封装的,易于使用的API中。

幸运的是,诸如DynamicProxy这样的工具为我们打开了这条路,所以我们没有必要做所有这些繁琐的管道。 通过向你展示这个过于简单的动态代理版本,我希望能够完成两件事情:可以看到专门知识和复杂的工作,已经进入这些工具的制作之中,并给予你机会看看动态代理生成的底层原理。 当实现一个IInterceptor并将其提供给DynamicProxy ProxyGenerator时,其实你正在使用Reflection.Emit在运行时开始一系列复杂的程序集,模块,类型,领域和方法构建来创建一个新的类型,但这些不存在于源代码中。

编译时编织工具的工作原理类似于运行时编织,除了它不会在运行时创建一个新的类型,我们在本章中一直在讨论。它会在执行代码之前修改由正常的.NET编译器创建的程序集中的类型。

编译时编织

当你在C#中创建一个.NET项目时,它被编译成CIL(也称为MSIL,IL和字节码)然后成为程序集(DLL或EXE文件)。 下图说明了流程的这个过程。 公共语言运行时(CLR)然后将CIL转换成实际的机器指令(通过称为即时编译的过程,或JIT)。 作为.NET开发人员,这个过程应该是你熟悉的。

使用编译时编织的AOP工具为此过程提供了另一个步骤称为后期编译(因此名称PostSharp)。 完成编译后,PostSharp(或其他编译时的AOP工具)然后为已经创建的切面以及你已经指出切面用在了什么地方去检查程序集。 然后它直接修改程序集中的CIL来执行编织,如图所示。

这种方法的一个很好的副作用是PostSharp可以检测到的任何错误也可以在Visual Studio中显示,就好像它们是来自编译器的错误(关于更多详细见下一章)

在编译器完成创建CIL代码之后,后期编译器进程将立即根据你编写的切面以及在哪里应用了那些切面运行和修改CIL代码。 修改CIL的这个过程是任何编译时AOP工具通用基础,但在本节的其余部分,你将看到一些PostSharp具体运行的细节,以及最终修改后的CIL是什么样子。

后期编译(PostCompiling)

为了帮助你理解PostSharp,我们先来一步一步看看PostSharp是如何工作的。

第一步是在编译之前,当然你会使用PostSharp.dll库编写切面,并指出那些方面应该用在什么地方(例如,指定具有特性的切入点)。所有PostSharp切面都是特性,通常不会自己执行(它们只是元数据)。下一步是在编译项目后立即进行。

编译器会查看你的源代码并将其转换成一个包含CIL的程序集。 之后,PostSharp后期编译程序接管。 它会检查你编写的切面,指定的切面用在了什么地方,以及程序集的CIL。 然后PostSharp会做几件事情:实例化切面,序列化切面,并修改CIL,以适当调用该切面。

当PostSharp完成工作后,序列化切面将被存储为汇编中的二进制流(作为资源)。 此流将在运行时加载用于执行(并且还将执行其RuntimeInitialize方法)。为了帮助可视化此过程,下是使用伪代码来表示项目的三个主要状态的图:你编写的源代码,由编译器与PostSharp合作创建的程序集,以及由CLR执行的执行程序。

所有这些都可能听起来有点复杂,所以为了进一步演示,让我们回顾一下PostSharp来龙去脉的工作原理。 我们将通过使用反编译器比较写入编译后的程序集中的源代码。

来龙去脉

反编译器是一个可以分析.NET程序集(如DLL或EXE文件)的工具,并将其从CIL转换回C#代码。 它反过来编译(CIL到C#而不是C#到CIL)。 你可以使用各种反编译工具来实现反编译,而且它们都倾向于有一组共同的功能,但我在这里使用的工具叫做ILSpy。官网http://ilspy.net 。

为了演示,我要编写一个简单的程序,编译它,并使用ILSpy反编译。 起初,我不会使用任何AOP,这意味着我希望看到我的C#和我的反编译的C#是相同的。 这是一个只有一个方法简单的类,在Visual Studio中:

namespace BeforeAndAfter
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

然后我将其编译(在我的项目的bin文件夹中的DLL或EXE文件中)。 如果我使用ILSpy打开它与导航到Program,然后ILSpy将显示我下图:

如果你使用其他工具,你可能看不到完全相同的东西。 可能会出现一个默认的无参数构造函数。 每个类都需要一个构造函数,并且由于我没有明确定义一个构造函数,所以编译器假定一个public,空方法体,无参数的构造函数。 反编译时,ILSpy也会做出相同的假设。 除此之外,反编译的C#应该看起来和原来的C#相似,不管是你使用哪一个工具。

现在让我们使用PostSharp在这个项目的代码中添加一个切面。 PostSharp会修改CIL,这意味着我不会指望反编译的C#与Visual Studio中看到的C#看起来相同。 以下列表再次显示Program,这次用一个简单的PostSharp方法应用到Main方法:

class Program
    {
        [MyAspect]
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }

using PostSharp.Aspects;
namespace BeforeAndAfter
{
    [Serializable]
    public class MyAspect:OnMethodBoundaryAspect
    {
        public override void OnEntry(MethodExecutionArgs args)
        {
            Console.WriteLine("Before");
        }

        public override void OnExit(MethodExecutionArgs args)
        {
            Console.WriteLine("After");
        }
    }
}

编译完后,我再次使用ILSpy打开程序集,看看反编译Main代码 如果你使用免费的PostSharp Express,你会看到代码很长的方法(与完整商业版相比)。

PostSharp 功能:Aspect切面优化器
PostSharp Express不包括Aspect优化器。 Aspect优化器会检查你编写的切面,并修改IL只能完成任务你想做的事情。 例如,如果你根本不使用args对象,那么这个切面优化器将会发现这一点,当OnEntry和OnExit被调用时,一个空值将是传递给args参数。 另一个例子:因为我们没有重写OnSuccess或OnException,所以aspect优化器会看到这一点,并且在编织创建代码时不会调用那些空的基本方法。
PostSharp Express没有这个优化器 - 它假设你需要所有的东西,这就是为什么反编译版本的Main方法太长了。

从上图可以看到,编译后的程序集包含两个命名空间,一个是我们定义的命名空间BeforeAndAfter,另一个是PostSharp生成的随机命名空间PostSharp.ImplementationDetails_f1559a2f,里面包含了PostSharp的详细实现代码,大概过程就是将元数据通过二进制序列化器反序列化为对应的我们定义的切面对象,然后再在Program程序中引入 PostSharp.ImplementationDetails_f1559a2f命名空间,调用我们的切面的方法。Program类编织后的代码和预想的差不多,但是PostSharp随机生成的命名空间的代码是AOP实现的关键。

命名可能看起来很奇怪。 这些都是怪异的名字,但它们只是名称,像任何其他类或方法名称一样。 OnEntry是一个名为a0对象的方法,它是MyAspect类型。 对象a0是一个内部隐藏类<>z__a_1的内部静态只读成员。

PostSharp在编译时通过添加和操作CIL,创建了这段代码中的几乎所有内容,这些都是根据你写的切面而生成的。 一些生成的CIL不直接对应C#。 名称<>z__a_1在C#中无效。 这些都是ILSpy尽力解读的表现。

这部分和前一段可能似乎是深入.NET的底层,但现实中我们很少接触到Reflection.Emit和CIL的操纵。 幸运的是,我们作为AOP工具的用户 - 大多数时候不需要关心这样的复杂性。 但重要的是要有一些对这些AOP实现的内部工作的理解,因为我们要对下决定使用哪种类型的AOP负责。 我们应该使用运行时编织,还是应该使用编译时编织?

运行时编织 VS. 编译时编织

开发人员似乎担心的一个因素是性能,所以让我们从通过比较两种方法的性能入手。 根据我的经验,现实是,程序中的性能瓶颈很少由使用AOP工具引起,而与在开发人员的生产力和可维护的代码受益方面相比,AOP造成的任何性能问题都不重要。

如你所见,运行时AOP工具如DynamicProxy使用Reflection.Emit,这可能是用户注意到的慢操作,但一旦类型创建,它不需要再次创建,所以这个性能点相对可以忽略不计。 编译时工具不会使用缓慢的Reflection.Emit操作,因为它在编译时执行其工作。 可以看得到,开发人员在解决方案中拥有大量使用了PostSharp项目时,这会增加构建时间。这是最常见的关于后期编译工具的抱怨。 但随着PostSharp新版本的性能不断提高,你可以配置大型多项目解决方案,以使PostSharp不执行那些不使用切面的项目。 如果性能是你的主要关注点,这两种类型的工具都会以某种方式降低性能,尽管可能在实践中注意到不是足够慢。

因此,你如何决定哪个AOP实现更好:运行时编织或编译时编织,只基于性能考虑? 你应该使用哪一个? 虽然很讨厌这种回答,但它是真实的:这视情况而定。

如果你没有使用很多切面,或者你没有在许多class上使用它们,就可以用写代理或装饰器类,根本不用任何第三方的AOP工具。

但是,如果你的项目使用了很多横切关注点,AOP肯定会对你有好处。 也许在运行时动态生成的类型,也许在编译时修改CIL。 也许两者都行。 让我们看下每种方法的好处。

运行时编织优点

使用你已经看过的DynamicProxy等工具的主要优点之一是它很容易测试(参见单元测试章节)。 一个DynamicProxy拦截器可以在运行时轻松注入依赖关系,方便编写切面独立的测试。

第二,与PostSharp这样的工具相比,像DynamicProxy这样的运行时工具不需要后编译过程。 你不需要单独的EXE,使其在每个团队成员的计算机和构建服务器上正确编译。 因此可能更容易将AOP引入项目团队和/或项目的构建服务器。

第三,因为方面在运行时才被实例化,你也可以保留在构建完成后配置切面的能力。 运行时你拥有一定的灵活性 - 比如,可以使用XML文件更改切面配置。

最后,虽然许可和成本是复杂的问题,但是DynamicProxy是一个世界一流的AOP框架,是一个免费的开源工具,所以我一定会将它作为运行时编织阵营的头牌。 这些是运行时优于编译时编织的关键领域。

编译时编织优点

编译时编织有一些不同的好处。 由于PostSharp这样的工具运行的本质(通过在程序集文件中直接操作CIL),它们可以更强大。

首先,通过运行时编织,拦截器通常被应用于类的每个方法,即使你只对一个类感兴趣。 使用PostSharp等工具可以使用更细粒度的控制来应用切面。

其次,使用运行时编织,你通常需要使用IoC容器来使用拦截方面。 但是,程序中的每个对象并不总是这样通过IoC工具实例化。 例如,UI对象和域对象可能不适合或不能用容器实例化。 因此,PostSharp等工具具有运行时AOP工具不具备的附加功能。

如果你正在开发的项目没有使用IoC工具,那么为了使用运行时AOP,你需要重新构建代码才能使用IoC工具,然后才能开始使用AOP。 通过编译时AOP工具,你可以立即开始获得AOP的优势。 我不是说你不应该使用IoC或其他依赖注入工具。无论你是否使用AOP, 依赖注入是一个非常有用的工具,可以让你创建松散耦合,易于测试的代码。 但不是你从事的每个代码库都有使用DI来构建,而且重构过程可能会慢而昂贵。

编译时工具更强大的最后一点是,它允许任何代码使用AOP:包括静态方法,私有方法和字段(在第5章中见位置拦截)。

小结

最终,我不能单方面决定使用一种方法:只有你可以做出这个决定。 除了我提到的技术利弊之外,还要考虑整个非技术因素,包括许可,价格和支持。 我用了很多篇幅描述两种主要的方法:运行时编织和编译时编织。 在实践中,你评估的各个工具可能有所不同。 这个工具有多成熟? 它的API可能会改变吗? 它的API是否有意义? 你的团队其余成员最适合什么? 你是从旧版代码库开始还是从头开始创建一个新项目?

这些都是在你下决定时必须考虑的关键属性。 但是,这些都在技术之外,因为每个团队,每个公司,每一个AOP工具,每个项目都不同。 现在你熟悉使用AOP和AOP工具如何工作,你对于项目或代码库的架构就会有整体的决定。

除了我一直在描述的常见横切关注点的切面,AOP具有架构师感兴趣的功能。由于PostSharp在编译之后会对代码进行检查,因此它可以提供许多其他AOP工具无法提供的额外功能。 在下一章,我们来看看如何将PostSharp引入到你的架构。

posted @ 2017-06-12 07:25 tkbSimplest 阅读(...) 评论(...) 编辑 收藏