VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > temp > C#教程 >
  • AOP从静态代理到动态代理 Emit实现

【前言】

  AOP为Aspect Oriented Programming的缩写,意思是面向切面编程的技术。

  何为切面?

  一个和业务没有任何耦合相关的代码段,诸如:调用日志,发送邮件,甚至路由分发。一切能为代码所有且能和代码充分解耦的代码都可以作为一个业务代码的切面。

  我们为什么要AOP?

  那我们从一个场景举例说起:

  如果想要采集用户操作行为,我们需要掌握用户调用的每一个接口的信息。这时候的我们要怎么做?

  如果不采用AOP技术,也是最简单的,所有方法体第一句话先调用一个日志接口将方法信息传递记录。

  有何问题?

  实现业务没有任何问题,但是随之而来的是代码臃肿不堪,难以调整维护的诸多问题(可自行脑补)。

  如果我们采用了AOP技术,我们就可以在系统启动的地方将所有将要采集日志的类注入,每一次调用方法前,AOP框架会自动调用我们的日志代码。

  是不是省去了很多重复无用的劳动?代码也将变得非常好维护(有朝一日不需要了,只需将切面代码注释掉即可)

  接下来我们看看AOP框架的工作原理以及实过程。

【实现思路】

  AOP框架呢,一般通过静态代理和动态代理两种实现方式。

  

  何为静态代理? 

  静态代理,又叫编译时代理,就是在编译的时候,已经存在代理类,运行时直接调用的方式。说的通俗一点,就是自己手动写代码实现代理类的方式。

  我们通过一个例子来展现一下静态代理的实现过程:

  我们这里有一个业务类,里面有方法Test(),我们要在Test调用前和调用后分别输出日志。

  

  我们既然要将Log当作一个切面,我们肯定不能去动原有的业务代码,那样也违反了面向对象设计之开闭原则。

  那么我们要怎么做呢?我们定义一个新类 BusinessProxy 去包装一下这个类。为了便于在多个方法的时候区分和辨认,方法也叫 Test()

 

 

 

  这样,我们如果要在所有的Business类中的方法都添加Log,我们就在BusinessProxy代理类中添加对应的方法去包装。既不破坏原有逻辑,又可以实现前后日志的功能。

  当然,我们可以有更优雅的实现方式:

  

 

  我们可以定义代理类,继承自业务类。将业务类中的方法定义为虚方法。那么我们可以重写父类的方法并且在加入日志以后再调用父类的原方法。

  当然,我们还有更加优雅的实现方式:

  

  

  我们可以使用发射的技术,写一个通用的Invoke方法,所有的方法都可以通过该方法调用。

  我们这样便实现了一个静态代理。

  那我们既然有了静态代理,为什么又要有动态代理呢?

  我们仔细回顾静态代理的实现过程。我们要在所有的方法中添加切面,我们就要在代理类中重写所有的业务方法。更有甚者,我们有N个业务类,就要定义N个代理类。这是很庞大的工作量。

  

  这就是动态代理出现的背景,相比都可以猜得到,动态代理就是将这一系列繁琐的步骤自动化,让程序自动为我们生成代理类。

  何为动态代理?

  动态代理,又成为运行时代理。在程序运行的过程中,调用了生成代理类的代码,将自动生成业务类的代理类。不需要我们手共编写,极高的提高了工作效率和调整了程序员的心态

  原理不必多说,就是动态生成静态代理的代码。我们要做的,就是选用一种生成代码的方式去生成。

  今天我分享一个简单的AOP框架,代码使用Emit生成。当然,Emit 代码的写法不是今天要讲的主要内容,需要提前去学习。

  先说效果:

  定义一个Action特性类 ActionAttribute 继承自 ActionBaseAttribute,里面在Before和After方法中输出两条日志;

  

  定义一个Action特性类 InterceptorAttribute 继承自 InterceptorBaseAttribute,里面捕获了方法调用异常,以及执行前后分别输出日志;

  

  然后定义一个业务类 BusinessClass 实现了 IBusinessClass 接口,定义了各种类型的方法

  

  

  多余的方法不贴图了。

  我们把上面定义的方法调用切面标签放在业务类上,表示该类下所有的方法都执行异常过滤;

  我们把Action特性放在Test方法上,表明要在 Test() 方法的 Before 和 After 调用时记录日志;

  我们定义测试类:

  

  调用一下试试:

  

  可见,全类方法标签 Interceptor 在 Test 和 GetInt 方法调用前后都打出了对应的日志;

  Action方法标签只在 Test 方法上做了标记,那么Test 方法 Before 和 After 执行时打出了日志;

【实现过程】

  实现的思路在上面已经有详细的讲解,可以参考静态代理的实现思路。

  我们定义一个动态代理生成类 DynamicProxy,用于原业务代码的扫描和代理类代码的生成;

  定义两个过滤器标签,ActionBaseAttribute,提供 Before 和 After 切面方法;InterceptorBaseAttribute,提供 Invoke “全调用”包装的切面方法;

  Before可以获取到当前调用的方法和参数列表,After可以获取到当前方法调用以后的结果。

  Invoke 可以拿到当前调用的对象和方法名,参数列表。在这里进行反射动态调用。

复制代码
1 [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
2     public class ActionBaseAttribute : Attribute
3     {
4         public virtual void Before(string @method, object[] parameters) { }
5 
6         public virtual object After(string @method, object result) { return result; }
7     }
复制代码
复制代码
1 [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
2     public class InterceptorBaseAttribute : Attribute
3     {
4         public virtual object Invoke(object @object, string @method, object[] parameters)
5         {
6             return @object.GetType().GetMethod(@method).Invoke(@object, parameters);
7         }
8     }
复制代码

  代理生成类采用Emit的方式生成运行时IL代码。

  先把代码放在这里:

 DynamicProxy

  里面实现了两种代理方式,一种是 面向接口实现 的方式,另一种是 继承重写 的方式。

  但是继承重写的方式需要把业务类的所有方法写成virtual虚方法,动态类会重写该方法。

  我们从上一节的Demo中获取到运行时生成的代理类dll,用ILSpy反编译查看源代码:

  

  可以看到,我们的代理类分别调用了我们特性标签中的各项方法。

  核心代码分析(源代码在上面折叠部位已经贴出):

  

  解释:如果该方法存在Action标签,那么加载 action 标签实例化对象,加载参数,执行Before方法;如果该方法存在Interceptor标签,那么使用类字段this._interceptor调用该标签的Invoke方法。

  

  解释:如果面的Interceptor特性标签不存在,那么会加载当前扫描的方法对应的参数,直接调用方法;如果Action标签存在,则将刚才调用的结果包装成object对象传递到After方法中。

  这里如果目标参数是object类型,而实际参数是直接调用返回的明确的值类型,需要进行装箱操作,否则运行时报调用内存错误异常。

  

  解释:如果返回值是void类型,则直接结束并返回结果;如果返回值是值类型,则需要手动拆箱操作,如果是引用类型,那么需要类型转换操作。

  IL实现的细节,这里不做重点讨论。

【系统测试】

   1.接口实现方式,Api测试(各种标签使用方式对应的不同类型的方法调用):

  

  结论:对于上述穷举的类型,各种标签使用方式皆成功打出了日志;

  2.继承方式,Api测试(各种标签使用方式对应的不同类型的方法调用):

  

  结论:继承方式和接口实现方式的效果是一样的,只是方法上需要不同的实现调整;

  3.直接调用三个方法百万次性能结果:

  

  结论:直接调用三个方法百万次调用耗时 58ms

  4.使用实现接口方式三个方法百万次调用结果

  

  结论:结果见上图,需要注意是三个方法百万次调用,也就是300w次的方法调用

  5.使用继承方式三个方法百万次调用结果

  

  结论:结果见上图,需要注意是三个方法百万次调用,也就是300w次的方法调用

  事实证明,IL Emit的实现方式性能还是很高的。

  综合分析:

  通过各种的调用分析,可以看出使用代理以后和原生方法调用相比性能损耗在哪里。性能差距最大的,也是耗时最多的实现方式就是添加了全类方法代理而且是使用Invoke进行全方法切面方式。该方式耗时的原因是使用了反射Invoke的方法。

  直接添加Action代理类实现 Before和After的方式和原生差距不大,主要损耗在After触发时的拆装箱上。

  综上分析,我们使用的时候,尽量针对性地对某一个方法进行AOP注入,而尽量不要全类方法进行AOP注入。

【总结】

  通过自己实现一个AOP的动态注入框架,对Emit有了更加深入的了解,最重要的是,对CLR IL代码的执行过程有了一定的认知,受益匪浅。

  该方法在使用的过程中也发现了问题,比如有ref和out类型的参数时,会出现问题,需要后续继续改进

  本文的源代码已托管在GitHub上,又需要可以自行拿取(顺手Star哦~):https://github.com/sevenTiny/CodeArts (可以访问下面链接获取.Net Standard 跨平台版本代码)

  最新SevenTiny.Bantina.Aop组件代码(跨平台):https://github.com/sevenTiny/SevenTiny.Bantina

  该代码的位置在 CodeArts.CSharp 分区下

  

  VS打开后,可以在 EmitDynamicProxy 分区下找到;本博客所有的测试项目都在项目中可以找到。

  

  再次放上源代码地址,供一起学习的朋友参考,希望能帮助到你:https://github.com/sevenTiny/CodeArts (可以访问下面链接获取.Net Standard 跨平台版本代码)

  最新SevenTiny.Bantina.Aop组件代码(跨平台):https://github.com/sevenTiny/SevenTiny.Bantina



相关教程