PostSharp是一个非常优秀的AOP框架,使用上非常方便,功能强大,对目标拦截的方法不需要做什么修改,但现在已经商业化运作
从 PostSharp官方网站下载一个试用版,安装
简单示例
PostSharp采用Post-Compile的方式实现AOP,即对已经生成的程序集,按照拦截规则进行修改,对需要拦截的方法注入拦截代码。这种方式与基于动态代理的方式相比,没有过多限制,比如不需要目标方法为virtual类型或者实现了接口等
1. 新建一个PostSharp.Test的Console测试项目添加引用: PostSharp、PostSharp.Laos
2. 程序引用的命名空间
using System;
using System.Reflection;
using PostSharp.Laos;
3. 测试用的代码如下:
01 |
private static void SayHello( string user, string title) |
03 |
Console.WriteLine( string .Format( "Hello {0} {1}" , title, user)); |
05 |
private static void ThrowException() |
07 |
throw new Exception( "I'm a test message." ); |
09 |
static void Main( string [] args) |
11 |
SayHello( "Richie" , "Mr." ); |
4. 然后在项目中实现一个aspect:
02 |
public class OnMyExceptionAspect : OnMethodBoundaryAspect |
04 |
public override void OnEntry(MethodExecutionEventArgs eventArgs) |
06 |
base .OnEntry(eventArgs); |
07 |
Console.WriteLine( string .Format( "Entering method: {0}" , eventArgs.Method.Name)); |
08 |
object [] arguments = eventArgs.GetReadOnlyArgumentArray(); |
09 |
ParameterInfo[] parameters = eventArgs.Method.GetParameters(); |
10 |
for ( int i = 0; arguments != null && i < arguments.Length; i++) |
11 |
Console.WriteLine( string .Format( " arg{0} {1}: {2}" , i + 1, parameters[i].Name, arguments[i])); |
13 |
public override void OnExit(MethodExecutionEventArgs eventArgs) |
15 |
base .OnExit(eventArgs); |
16 |
Console.WriteLine( string .Format( "Exiting method: {0}" , eventArgs.Method.Name)); |
18 |
public override void OnException(MethodExecutionEventArgs eventArgs) |
20 |
Console.WriteLine( "There's an error occured:" + eventArgs.Exception.Message); |
21 |
base .OnException(eventArgs); |
我们使用这个aspect对测试代码进行拦截处理,输出方法调用的日志信息
5. 接下来的2个步骤就是设置项目,使得PostSharp在编译结束后能够对生成的程序集进行修改,假如拦截处理机制
a). 在项目中添加一个assembly指令,告诉PostSharp对哪些目标实施拦截
[assembly: PostSharp.Test.OnMyExceptionAspect(
AttributeTargetAssemblies = "PostSharp.Test",
AttributeTargetTypes = "PostSharp.Test.Program")]
b). 修改项目文件,让PostSharp在编译完成后能够注入拦截代码:
先通过VS的右键菜单卸载项目
然后通过VS邮件菜单编辑项目文件,添加一个import指令:
在<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />后面添加一条import指令如下:
<Import Project="E:\Program Files\PostSharp 2.0\PostSharp.targets" />
6. 重新加载项目,编译,PostSharp就会在我们指定的位置上使用我们的OnMyExceptionAspect对Program的方法调用注入拦截代码,运行结果如下:
使用Attribute方式
上面使用一个assembly指令告诉PostSharp应该对哪些方法实施拦截,OnMethodBoundaryAspect类已经继承了Attribute,因此我们可以通过给类或者方法添加Attribute的方式来代替assembly指令
下面示例需要引用的命名空间:
using System;
using System.ComponentModel;
using System.Reflection;
using PostSharp.Extensibility;
using PostSharp.Laos;
我们用下面的PropertyChangedNotificationAttribute来实现对象属性更改通知:
02 |
[MulticastAttributeUsage(MulticastTargets.Method)] |
03 |
public class PropertyChangedNotificationAttribute : OnMethodBoundaryAspect |
05 |
private object _preValue; |
06 |
public override void OnExit(MethodExecutionEventArgs eventArgs) |
08 |
base .OnExit(eventArgs); |
09 |
if (!eventArgs.Method.IsSpecialName) return ; |
10 |
if (!eventArgs.Method.Name.StartsWith( "get_" ) |
11 |
&& !eventArgs.Method.Name.StartsWith( "set_" )) return ; |
12 |
bool isSetter = eventArgs.Method.Name.StartsWith( "set_" ); |
13 |
string property = eventArgs.Method.Name.Substring(4); |
15 |
Console.WriteLine( string .Format( "Property \"{0}\" was changed from \"{1}\" to \"{2}\"." |
18 |
, this .GetPropertyValue(eventArgs.Instance, property))); |
20 |
Console.WriteLine( string .Format( "Property \"{0}\" was read." , property)); |
22 |
public override void OnEntry(MethodExecutionEventArgs eventArgs) |
24 |
base .OnEntry(eventArgs); |
26 |
if (!eventArgs.Method.IsSpecialName) return ; |
27 |
if (!eventArgs.Method.Name.StartsWith( "set_" )) return ; |
28 |
string property = eventArgs.Method.Name.Substring(4); |
29 |
this ._preValue = this .GetPropertyValue(eventArgs.Instance, property); |
31 |
private object GetPropertyValue( object instance, string property) |
33 |
PropertyInfo getter = instance.GetType().GetProperty(property); |
34 |
return getter.GetValue(instance, null ); |
然后给测试类添加PropertyChangedNotification的attribute就能通过AOP实现对象属性更改通知的效果:
01 |
[PropertyChangedNotification] |
04 |
private string firstName; |
05 |
private string lastName; |
06 |
public Person( string first, string last) |
08 |
this .firstName = first; |
11 |
public string FirstName |
13 |
get { return firstName; } |
14 |
set { firstName = value; } |
16 |
public string LastName |
18 |
get { return lastName; } |
19 |
set { lastName = value; } |
23 |
get { return this .FirstName + " " + this .LastName; } |
27 |
static void Main( string [] args) |
29 |
Person user = new Person( "Richie" , "Liu" ); |
30 |
user.FirstName = "RicCC" ; |
31 |
Console.WriteLine( string .Format( "{{ {0} {1} }}" , user.FirstName, user.LastName)); |
编译生成,运行结果如下:
因为我们在Property的setter方法进入(OnEntry)时用反射读取了属性的值,记录修改之前的属性值;然后在setter方法结束(OnExit)时又使用反射去读取了修改之后的属性值,因此在***was changed from *** to ***这条消息之前,有2条属性的读取日志
Cache示例
PostSharp中有个缓存方法调用的示例:第一次调用方法的时候,执行这个方法,并把执行结果缓存起来;后面再调用这个方法时,就直接从缓存中取结果,让方法返回,而不执行方法体
实现这一功能的aspect类如下:
02 |
public sealed class CacheAttribute : OnMethodBoundaryAspect |
05 |
private MethodFormatStrings formatStrings; |
07 |
private static readonly Dictionary< string , object = "" > cache = new Dictionary< string , object = "" >(); |
11 |
public override bool CompileTimeValidate(MethodBase method) |
14 |
if (method is ConstructorInfo) |
16 |
Message.Write(SeverityType.Error, "CX0001" , "Cannot cache constructors." ); |
19 |
MethodInfo methodInfo = (MethodInfo)method; |
21 |
if (methodInfo.ReturnType.Name == "Void" ) |
23 |
Message.Write(SeverityType.Error, "CX0002" , "Cannot cache void methods." ); |
27 |
ParameterInfo[] parameters = method.GetParameters(); |
28 |
for ( int i = 0; i < parameters.Length; i++) |
30 |
if (parameters[i].IsOut) |
32 |
Message.Write(SeverityType.Error, "CX0003" , "Cannot cache methods with return values." ); |
39 |
public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo) |
41 |
this .formatStrings = Formatter.GetMethodFormatStrings(method); |
44 |
public override void OnEntry(MethodExecutionArgs eventArgs) |
47 |
string key = this .formatStrings.Format( |
48 |
eventArgs.Instance, eventArgs.Method, eventArgs.Arguments.ToArray()); |
53 |
if (!cache.TryGetValue(key, out value)) |
56 |
eventArgs.MethodExecutionTag = key; |
60 |
eventArgs.ReturnValue = value; |
61 |
eventArgs.FlowBehavior = FlowBehavior.Return; |
67 |
public override void OnSuccess(MethodExecutionArgs eventArgs) |
70 |
string key = ( string )eventArgs.MethodExecutionTag; |
74 |
cache[key] = eventArgs.ReturnValue; |
测试代码如下:
01 |
public static void Main( string [] args ) |
03 |
Console.WriteLine( "1 ->" + GetDifficultResult( 1 ) ); |
04 |
Console.WriteLine( "2 ->" + GetDifficultResult( 2 ) ); |
05 |
Console.WriteLine( "1 ->" + GetDifficultResult( 1 ) ); |
06 |
Console.WriteLine( "2 ->" + GetDifficultResult( 2 ) ); |
10 |
private static int GetDifficultResult( int arg ) |
13 |
Console.WriteLine( "Some difficult work!" ); |
参数为1和2的时候分别执行以下方法体,后面2次调用则都是从缓存中直接取返回值了
DbInvoke示例
这也是PostSharp中一个比较有意思的示例,其工作方式大致如下,先使用下面代码声明一些方法:
01 |
[DbInvoke( "ConnectionString" )] |
02 |
internal static class DataLayer |
04 |
#pragma warning disable 626 |
05 |
extern static public void CreateCustomer( string customerName, out int customerId); |
06 |
extern static public void ModifyCustomer( int customerId, string customerName); |
07 |
extern static public void DeleteCustomer( int customerId); |
08 |
extern static public void ReadCustomer( int customerId, out string customerName); |
09 |
#pragma warning restore 626 |
这里每个方法代表一个数据库的存储过程,通过DbInvoke这个attribute实施拦截(DbInvoke继承自PostSharp.Aspects.ImplementMethodAspect,PostSharp直接使用这个类的OnExecution方法代替对原方法的调用,他用于实现对extern方法、abstract类的方法提供拦截实现),然后将方法名作为存储过程名字,参数名字则作为存储过程参数名,将.NET数据类型转化为数据库的数据类型,执行存储过程,并处理out、ref类型的参数,将存储过程执行结果的参数值设置到方法的相关参数上
通过这样的方式,将数据库的存储过程声明为.NET中的extern方法,简化对存储过程的调用方式
Aspect的类型
1. OnMethodBoundaryAspect
可以override OnEntry、OnExit、OnSuccess、OnException等方法实施拦截,可以读取入参,修改ref、out类型的入参,决定是否调用被拦截的方法体,以及读取、修改方法的返回值等,一般可以用于自己编写的assembly,进行日志记录等操作
PostSharp对原方法注入一个try{}catch(){}语句,在适当的位置注入各个拦截事件的调用代码
当对同一个方法使用多个该类型的aspect时,可以通过设置或者实现AspectPriority来确定各个拦截器的执行顺序
类似于前面的Cache示例中使用到的,在aspect之间或者拦截的各个方法之间,可以通过MethodExecutionEventArgs的MethodExecutionTag属性来传递必要的状态信息
2. OnExceptionAspect
用于实现异常捕获,可以运用于第三方开发的,没有源代码的assembly上
3. OnFieldAccessAspect
方便对field的读取、设置进行拦截处理,override OnGetValue、OnSetValue方法实施拦截。测试过程中无法读取到FieldInfo属性,不知道是不是只有商业版注册后才可以使用这个功能
对field访问的拦截只能适用于当前程序集,如果其他程序集直接诶访问field无法实现拦截,所以对于public、protected类型的field,PostSharp会将field重命名,然后自动生成一个原field名字的property,这会导致依赖的程序集二进制兼容性被破坏,需要重新编译。这个行为也可以通过选项进行配置,阻止PostSharp这样做
4. OnMethodInvocationAspect
override OnInvocation方法实施拦截,PostSharp不是直接修改注入目标程序集,而是为目标方法生成一个委托,修改当前程序集中的调用,改为调用委托,从而实现拦截(这种方式叫做Call-site weaving,调用方织入,对应的另一种方式叫做Target-site weaving,目标织入)。这种方式可以实现对第三方程序集方法实施拦截
5. ImplementMethodAspect
如前面的DbInvoke示例,这个aspect用于extern方法、abstract类的方法进行拦截,不要求目标方法有具体的实现
Aspect的生命周期
aspect在编译期实例化,PostSharp将aspect的实例序列化存到assembly中,在运行时再反序列化回来
对multicast类型的attribute,PostSharp会为每个匹配到的类型、方法等单独创建一个该attribute的实例对象应用于目标上
multicast概念:本来我们写一个custom attribute,必须在每个需要运用的方法、类型上面使用这个attribute,PostSharp中的multicast指可以指定比较宽的一个范围,或者使用正则表达式以及一些filter等,将这个attribute应用到匹配到的多个目标对象上面去,类似于多播这样的效果。这就是在前面使用过的assembly指令
这个概念导致的一个结果,对于理解PostSharp的行为比较重要,如果某个multicast类型的attribute指定为作用于field,但又将这个attribute设置在了type上面,则PostSharp会为该type的所有field运用这个attribute,从而运用拦截处理
另外,PostSharp的示例Advanced\AssemblyExplorer项目演示了如何使用PostSharp的CodeModel。PostSharp没有使用Mono.Cecil和CCI等开源项目,而是建立了自己的CodeModel来分析和修改assembly的元数据以及IL代码等,通过这个示例可以大致了解如何使用PostSharp的CodeModel来实现某些元数据、IL层面的操作
参考:
Aspect Oriented Programming 101
AOP Implementation of INotifyPropertyChanged