AOP in .NET
最近公司分配了项任务,让我调查一下.NET世界中的AOP。需求有四
- 什么是AOP?有什么用途?
- AOP的编程模型
- .NET世界中的AOP框架介绍极其评比
- 最佳实践
本文将假设读者对AOP的相关原理概念有了一定的了解,直接从第三部分开始。
实现方式
From Ayende Rahien’s blog post
Approach | Advantages | Disadvantages |
Remoting Proxies | Easy to implement, because of the .Net framework support | Somewhat heavy weight |
Deriving from ContextBoundObject | Easiest to implement | Very costly in terms of performance |
Compile-time subclassing | Easiest to understand | Interfaces or virtual methods only |
Runtime subclassing | Easiest to understand | Complex implementation (but already exists) |
Hooking into the profiler API | Extremely powerful | Performance? |
Compile time IL-weaving | Very powerful | Very hard to implement |
Runtime IL-weaving | Very powerful | Very hard to implement |
来看看具体的缺点
- 使用remoting proxies / ContextBoundObject
- 无法对this的调用进行intercept,因为CLR保证this不会是代理
- 性能影响很大,比如使用继承ContextBoundObject的方式会造成运行时context切换
- SubClassing
- 需要类型不能为sealed且只能override virtual方法。
- 不能改写constructor
- Weaving
- 如果是source code层级的织入,需要对每种.Net支持的语言提供分别的支持
- 无论是source code层级还是IL层级的织入,都有可能在类型变更的时候遭到破坏/给类型造成破坏。
现有的一些.Net的AOP框架
Project | Status | License | Approach | Document & Support | More Description |
GPL 2+ Look here for detail | Perhaps Compile time IL injection? | This project is not supported any more. | Relies on Cecil. | ||
Compile time IL injection | Commercial support. Well documented. | ||||
Dynamic proxy | Out of date documents | Multipurpose framework | |||
Custom Complier | Only tested with .Net 1.x | ||||
Relies on Phoenix | |||||
? | |||||
? | |||||
? | Runtime IL injection | ||||
? | Compile time IL injection | Relies on Rapier-LOOM.NET | |||
Runtime proxy | Well documented. | Multipurpose framework | |||
Runtime proxy | Out of date documents. | Relies on Cecil. Multipurpose framework | |||
? | Static mixins | Relies on Cecil. | |||
Runtime proxy | Well documented. | Multipurpose framework |
来看几个框架的一些具体介绍
关于几个IL织入的框架
技术上来说,能够支持IL织入的框架是一定能实现AOP的,比如Mono Cecil, MS CCI, MS Phoenix, and Rail。但是由于他们不是专门做AOP的框架,在之后的评比中没有考虑他们。
PIAB
PIAB (The Policy Injection Application Block) 是MS Enterprise Library的一部分。已经内嵌的支持如下aspects
- Authorization
- Exception Handling
- Logging
- Validation
- Performance Counter
- Caching (Caching handler is removed from EntLib 5.0 due to some issues)
Spring.Net
已经内嵌的支持如下aspects。
- Caching
- Exception Handling
- Logging
- Retry
- Transaction Management
- Parameter Validation
Post Sharp
注意Post Sharp对商业应用已不再开源和免费。
以插件的形式支持如下aspects
以插件的形式集成了其他一些著名的框架,但是貌似是Post Sharp 1.5时的事儿了?
最重要的一点:只能inject本assembly中的东西,不能修改其他assembly,这就是为什么我的sample中其他都在调用Models.Tester,唯独Post'Sharp.Interception把Tester的实现copy了一份:要intercept的Buy方法的调用在Tester里。
LinFu
由于PostSharp已经收费,所以就想找一款同样是IL weaving方式的开源AOP框架。目前比较活跃的貌似就属LinFu了,但是实践起来却问题多多(单指AOP方面,这个框架还支持IoC、动态代理等)。比如
文档老旧,很多和最新版(2.3)对不上。
配置上(编辑csproj文件)不支持对指定方法的interception,这是最让人抓狂的,来看
1: <PostWeaveTask TargetFile="$(MSBuildProjectDirectory)\$(OutputPath)$(MSBuildProjectName)$(TargetExt)"
2: InterceptAllExceptions="false"
3: InterceptAllFields="false"
4: InterceptAllNewInstances="false"
5: InterceptAllMethodCalls="true"
6: InterceptAllMethodBodies="false" />
要想inject指定的方法怎么办?实现自己的Task去解析!
另外,我的sample程序也跑不起来,原因不明。。。用MethodBodyReplacement的方法JIT时报错,用AroundMethodCall方法报CLR遇到一个invalid program。。。
评比
前人的一些工作
mono-cecil-vs-postsharp-core-vs-microsoft-cci-for-implementing-aop-framework
5 Sep 2009
Rating of Open Source AOP Frameworks in .NET
5 Aug 2008
31 Aug 2005
其中第二篇参考意义比较大,感兴趣的不妨去看看,其结论是三甲为PostSharp、PIAB、Spring.Net
本文的评比标准
- 能力上,至少能够inject public方法,不论其是否virtual
- project本身至少要保持更新,且有release版
- 性能与raw的调用不能差太多
参评框架
根据以上标准,选取PostSharp、PIAB、Spring.Net。
性能测试
参评的三个框架每种都用有源代码的情况下和无源代码的情况下两种方式实现,与raw调用进行对比。
测试case为IOder ICustomer.Buy(IProduct product)
每组都实现两个advice
- 输出trace
- 若Customer.WorkingAt = Product.ProducedBy,打八折
raw的实现是这样的
1: public IOrder Buy(IProduct product, int count = 1)
2: {
3: var order = new Order
4: {
5: Count = count,
6: Product = product,
7: Customer = this,
8: UnitPrice = product.UnitPrice
9: };
10:
11: if (WorkingAt.Equals(product.ProduceBy))
12: {
13: order.UnitPrice = product.UnitPrice*0.8;
14: }
15: Console.WriteLine(string.Format("{0} bought {1} at {2}. Count={3}, Unit Price={4}",
16: order.Customer.Name, order.Product.Name, DateTime.Now, order.Count,
17: order.UnitPrice));
18: return order;
19: }
效果如下
测试调用100,200,…1500次,测算每次调用耗费的毫秒数,列表如下
100 |
200 |
300 |
400 |
500 |
600 |
700 |
800 |
900 |
1000 |
1100 |
1200 |
1300 |
1400 |
1500 |
|
Raw |
2.33 |
0.465 |
0.56 |
0.55 |
0.548 |
0.558333 |
0.56 |
0.57125 |
0.564444 |
0.548 |
0.551818 |
0.55 |
0.548462 |
0.552857 |
0.548 |
PIAB |
2.5 |
0.665 |
0.636667 |
0.6725 |
0.656 |
0.651667 |
0.65 |
0.6525 |
0.658889 |
0.654 |
0.645455 |
0.655 |
0.653077 |
0.655714 |
0.652 |
PIAB.Interception |
2.5 |
0.67 |
0.66 |
0.6575 |
0.652 |
0.655 |
0.655714 |
0.64875 |
0.655556 |
0.654 |
0.654545 |
0.655833 |
0.653846 |
0.655 |
0.655333 |
PostSharp |
3 |
0.55 |
0.553333 |
0.54 |
0.55 |
0.546667 |
0.548571 |
0.5575 |
0.553333 |
0.553 |
0.55 |
0.5525 |
0.552308 |
0.555 |
0.55 |
PostSharp.Interception |
3.15 |
0.525 |
0.53 |
0.5725 |
0.554 |
0.561667 |
0.567143 |
0.555 |
0.565556 |
0.555 |
0.563636 |
0.558333 |
0.56 |
0.560714 |
0.558667 |
Spring |
2.44 |
0.53 |
0.586667 |
0.5825 |
0.578 |
0.583333 |
0.574286 |
0.56375 |
0.56 |
0.564 |
0.553636 |
0.565 |
0.563846 |
0.564286 |
0.562667 |
Spring.Interception |
2.59 |
0.52 |
0.523333 |
0.5375 |
0.538 |
0.548333 |
0.548571 |
0.54875 |
0.547778 |
0.544 |
0.553636 |
0.5475 |
0.546154 |
0.547143 |
0.549333 |
直观起见,直接上图(去掉了100的那一组数据)
可见PostSharp和Spring.Net都达到了接近Raw的性能,而PIAB仍需努力。
结论
如果喜欢IL织入,选择PostSharp
如果喜欢runtime proxy,选择Spring.Net
最佳实践
最佳实践这个题目对于我来说太大了。只能说是一些网上看来和实践得到的注意事项吧
通用
使用功能上最低限度的advice。例如只在after invoke做一些事的话就尽量不用around invoke的advice。
filed的interception是对OO的侵犯(直接无视了OO的第一条:封装),不要使用(三者中只有PostSharp支持)。
如果你的advice实现了某些特殊功能(而不是logging这种普通的功能),在你的方法上做出说明。
如果你的advice准备对全局起作用,就把它放在一个全局的地方,并且详加说明。
使用AOP时总是会造成一些与预期不同的结果,给你造成很多困扰。如果今后别人接手会给他造成更多的麻烦。所以有时还是用传统的方式吧。
PIAB
我使用EntLibConfig.NET4-32.exe编辑我的app.config,结果怎么都不行,之后删除了parameter节点中的typeName的assembly信息部分就OK了,原因不明。
PostSharp
已经不需要手动修改csproj文件,至少我的例子中没有。
Spring.Net
由于Spring.Net没有像PIAB一样提供一个Wrap方法,所以为了能从Spring容器中取出inject过的对象,还是需要在配置文件中进行配置,然后从ContextRegistry中取。例如本例中的
1: <object id="customer" type="AopInvestigate.Implements.Spring.Models.Customer, AopInvestigate.Implements.Spring" singleton="false"/>
代码下载
最后给出示例代码。
关于其中使用的各框架的license,上面那个表格里都有,这里就不再声明一次了。。。