在上一篇文章中简单介绍了没有使用AOP情况下如何来实现延迟加载的,并给出了一个使用了AOP实现延迟加载后的代码效果。这篇文章就来介绍如何用PostSharp来达到这种效果。
PostSharp是在编译的时候将代码织入到你的代码中的,它是一个VisualStudio的插件,所以必须安装才能使用,去官方网站下载后安装,就可以开始你的PostSharp之旅了。
PostSharp可以对类、类的方法、类的字段等进行拦截,而延迟加载主要是对类的属性进行拦截,C#类的属性编译后就是set和get方法,我们可以对这些方法进行拦截达到拦截属性的目的,还有一种方法就是对属性对应的字段进行拦截来达到对属性的拦截,我们就是采用的对字段进行拦截的方法:
首先我们要引用PostSharp.Laos和PostSharp.Public,并添加相应的using语句到代码中:
using PostSharp.Laos;
在字段上进行拦截,我们的Attribute需要继承自OnFieldAccessAspect这个抽象类(我们的Attribute类必须是可序列化的):
[Serializable] public class LazyLoadFieldAttribute : OnFieldAccessAspect { .... }
仔细观察上一篇文章中以前的延迟加载的实现方式,最主要的东西就是那个委托,委托最终会被赋值为一个获取某个属性值的方法,在AOP的延迟加载中也需要这样一个方法,这个方法(名)需要通过Attribute的参数传入,光有方法名还不够,需要知道这个方法在哪个程序集的哪个类中,所以还需要传入方法所在类的FullName(包含程序集的名称),于是构造函数就如下:
[Serializable] public class LazyLoadFieldAttribute : OnFieldAccessAspect { string lazyLoadMethodName; string typeFullName; public LazyLoadFieldAttribut(string lazyLoadMethodName, string typeFullName) { this.lazyLoadMethodName = lazyLoadMethodName; this.typeFullName = typeFullName; } }
看到这里大家已经猜出来了吧?是的,我们要通过反射来调用获取属性值的延迟加载方法(在这里我们不讨论反射的性能问题,我在这里只是介绍使用PostSharp解决延迟加载的方法)。
OnFieldAccessAspect提供了2个操作字段值的方法:OnGetValue和OnSetValue,通过对这2个方法的重载我们就可以对字段值进行拦截了,这2个方法接收一个声明为FieldAccessEventArgs的参数,FieldAccessEventArgs有如下的属性:
我们将通过这些属性来获取需要延迟加载的类的信息以及实例等。
先来看看比较简单一点的OnSetValue的实现:
public override void OnSetValue(FieldAccessEventArgs eventArgs) { eventArgs.StoredFieldValue = eventArgs.ExposedFieldValue; }
在这里ExposedFieldValue可以理解为我们要赋给字段的值,而StoredFieldValue就是当前字段保存的值,OnSetValue只是简单地把ExposedFieldValue赋给了StoredFieldValue。OnGetValue的实现:
public override void OnGetValue(FieldAccessEventArgs eventArgs) { if (eventArgs.StoredFieldValue == null) { eventArgs.StoredFieldValue = LazyLoadValue(lazyLoadMethodName, typeFullName, eventArgs.Instance); } eventArgs.ExposedFieldValue = eventArgs.StoredFieldValue; }
也很简单,判断字段保存的值是否为null(这里主要是为了讲原理,简单化了,实际上如果是集合或者数组,还要判断他们是否为空),如果为null则通过LazyLoadValue去获取值,而LazyLoadValue就是通过反射用当前的实例(eventArgs.Instance)作为参数去调用lazyLoadMethodName指定的方法。
看起来很不错吧,但是等等,我们好像漏掉了什么?是的,被你发现了,当我们从数据源获取出来的值本来就是null,那不是每次都要去调用LazyLoadValue这个方法?对照以前的延迟加载的实现,我们发现少了一个isLoaded的属性,用来表明已经加载过了,不管是否为null,都不需要去数据源加载了。如何解决呢?不可能在类的定义里面去为每一个需要延迟加载的属性都声明一个isLoaded的字段吧,那代码看起来就不优雅了。
我首先想到的是不能在类里面在直接声明,那么是否可以用织入的方式为类增加一些isLoaded的字段呢,可以我找了很久都没有找到PostSharp在类里面织入字段的方法。后来考虑到如果在类里面声明一系列的isLoaded字段不雅观,那就用一个List来保存字段名表明那些字段已经加载过了(List<string> IsLoadedFieldList)列表中有的就是加载过了,没有的就没有加载,这样就只有一个字段,比许多isLoaded看起来要舒服点。最终我也放弃了这种做法,因为CompositionAspect出场了:
通过它,你可以给你的类增加一个接口实现,我不能在类里面直接织入一个IsLoadedFieldList字段,可以定义一个接口包含IsLoadedFieldList,然后你的类“实现”这个接口,当然不是你“显示”实现,而是PostSharp织入:
首先定义一个接口:
public interface ILazyLoadable { List<string> IsLoadedList { get; } }
为这个接口提供一个默认实现:
internal class LazyLoadImplementation : ILazyLoadable { List<string> isLoadedList = new List<string>(); public List<string> IsLoadedList { get { return isLoadedList; } } }
然后就是LazyLoadClassAttribute,这个Attribute是用在类上的:
[Serializable] public class LazyLoadClassAttribute : CompositionAspect { public LazyLoadClassAttribute() { } public override object CreateImplementationObject(InstanceBoundLaosEventArgs eventArgs) { return new LazyLoadImplementation(); } public override Type GetPublicInterface(Type containerType) { return typeof(ILazyLoadable); } }
这样就需要对LazyLoadFieldAttribute的OnGetValue和OnSetValue做一点点修改:
public override void OnGetValue(FieldAccessEventArgs eventArgs) { string fieldName = eventArgs.FieldInfo.Name; ILazyLoadable isLoaded = eventArgs.Instance as ILazyLoadable; if (isLoaded != null) { if (eventArgs.StoredFieldValue == null && !isLoaded.IsLoadedFieldList.Contains(fieldName)) { eventArgs.StoredFieldValue = LazyLoadValue(lazyLoadMethodName, typeFullName, eventArgs.Instance); isLoaded.IsLoadedFieldList.Add(fieldName); } } eventArgs.ExposedFieldValue = eventArgs.StoredFieldValue; } public override void OnSetValue(FieldAccessEventArgs eventArgs) { string fieldName = eventArgs.FieldInfo.Name; ILazyLoadable isLoaded = eventArgs.Instance as ILazyLoadable; if (isLoaded != null) { if (!isLoaded.IsLoadedFieldList.Contains(fieldName)) { isLoaded.IsLoadedFieldList.Add(fieldName); } } eventArgs.StoredFieldValue = eventArgs.ExposedFieldValue; }
一个简单的延迟加载完成了。
ps:写文章真累,有点虎头蛇尾了,太困了。