.Net语言中关于AOP 的实现详解
文章主要和大家讲解开发应用系统时在.Net语言中关于AOP 的实现。LogAspect完成的功能主要是将Advice与业务对象的方法建立映射,并将其添加到Advice集合中。由于我们在AOP实现中,利用了xml配置文件来配置PointCut,因此对于所有Aspect而言,这些操作都是相同的,只要定义了正确的配置文件,将其读入即可。对于Aspect的SyncProcessMessage(),由于拦截和织入的方法是一样的,不同的只是Advice的逻辑而已,因此在所有Aspect的公共基类中已经提供了默认的实现:
public class LogAspect:Aspect
{
public LogAspect(IMessageSink nextSink):base(nextSink)
{}
}
然后定义正确的配置文件:
<aspect value ="LogAOP">
<advice type="before" assembly=" AOP.Advice" class="AOP.Advice.LogAdvice">
<pointcut>ADD</pointcut>
<pointcut>SUBSTRACT</pointcut>
</advice>
<advice type="after" assembly=" AOP.Advice" class="AOP.Advice.LogAdvice">
<pointcut>ADD</pointcut>
<pointcut>SUBSTRACT</pointcut>
</advice>
</aspect>
LogAdvice所属的程序集文件为AOP.Advice.dll,完整的类名为AOP.Advice.LogAdvice。
日志Advice(LogAdvice)
由于日志方面需要记录方法调用前后的相关数据,因此LogAdvice应同时实现IBeforeAdvice和IAfterAdvice接口:
public class LogAdvice:IAfterAdvice,IBeforeAdvice
{
#region IBeforeAdvice Members
public void BeforeAdvice(IMethodCallMessage callMsg)
{
if (callMsg == null)
{
return;
}
Console.WriteLine("{0}({1},{2})",
callMsg.MethodName, callMsg.GetArg(0),
callMsg.GetArg(1));
}
#endregion
#region IAfterAdvice Members
public void AfterAdvice(IMethodReturnMessage returnMsg)
{
if (returnMsg == null)
{
return;
}
Console.WriteLine("Result is {0}", returnMsg.ReturnValue);
}
#endregion
}
在BeforeAdvice()方法中,消息类型为IMethodCallMessage,通过这个接口对象,可以获取方法名和方法调用的参数值。与之相反,AfterAdvice()方法中的消息类型为IMethodReturnMessage,Advice所要获得的数据为方法的返回值ReturnValue。
性能监测方面
性能监测方面与日志方面的实现大致相同,为简便起见,我要实现的性能监测仅仅是记录方法调用前和调用后的时间。
性能监测Attribute(MonitorAOPAttribute)
与日志Attribute相同,MonitorAOPAttribute仅仅需要创建并返回对应的MonitorAOPProperty对象:
[AttributeUsage(AttributeTargets.Class)]
public class MonitorAOPAttribute:AOPAttribute
{
public MonitorAOPAttribute():base()
{}
public MonitorAOPAttribute(string aspectXml):base(aspectXml)
{}
protected override AOPProperty GetAOPProperty()
{
return new MonitorAOPProperty();
}
}
性能监测Property(MonitorAOPProperty)
MonitorAOPProperty的属性名将定义为MonitorAOP,使其与日志方面的属性区别。除定义性能监测方面的属性名外,还需要重写CreateAspect()方法,创建并返回对应的方面对象MonitorAspect:
public class MonitorAOPProperty:AOPProperty
{
protected override IMessageSink CreateAspect
(IMessageSink nextSink)
{
return new MonitorAspect(nextSink);
}
protected override string GetName()
{
return "MonitorAOP";
}
}
4.4.2.3性能监测Aspect(MonitorAspect)
MonitorAspect类的实现同样简单:
public class MonitorAspect:Aspect
{
public MonitorAspect(IMessageSink nextSink):base(nextSink)
{}
}
而其配置文件的定义则如下所示:
<aspect value ="MonitorAOP">
<advice type="before" assembly=" AOP.Advice"
class="AOP.Advice.MonitorAdvice">
<pointcut>ADD</pointcut>
<pointcut>SUBSTRACT</pointcut>
</advice>
<advice type="after" assembly=" AOP.Advice"
class="AOP.Advice.MonitorAdvice">
<pointcut>ADD</pointcut>
<pointcut>SUBSTRACT</pointcut>
</advice>
</aspect>
MonitorAdvice所属的程序集文件为AOP.Advice.dll,完整的类名为AOP.Advice.MonitorAdvice。
性能监测Advice(MonitorAdvice)
由于性能监测方面需要记录方法调用前后的具体时间,因此MonitorAdvice应同时实现IBeforeAdvice和IAfterAdvice接口:
public class MonitorAdvice : IBeforeAdvice, IAfterAdvice
{
#region IBeforeAdvice Members
public void BeforeAdvice(IMethodCallMessage callMsg)
{
if (callMsg == null)
{
return;
}
Console.WriteLine("Before {0} at {1}",
callMsg.MethodName, DateTime.Now);
}
#endregion
#region IAfterAdvice Members
public void AfterAdvice(IMethodReturnMessage returnMsg)
{
if (returnMsg == null)
{
return;
}
Console.WriteLine("After {0} at {1}",
returnMsg.MethodName, DateTime.Now);
}
#endregion
}
MonitorAdvice只需要记录方法调用前后的时间,因此只需要分别在BeforeAdvice()和AfterAdvice()方法中,记录当前的时间即可。
业务对象与应用程序
业务对象(Calculator)
通过AOP技术,我们已经将核心关注点和横切关注点完全分离,我们在定义业务对象时,并不需要关注包括日志、性能监测等方面,这也是AOP技术的优势。当然,由于要利用.Net中的Attribute及代理技术,对于施加了方面的业务对象而言,仍然需要一些小小的限制。
首先,我们应该将定义好的方面Aspect施加给业务对象。其次,由于代理技术要获取业务对象的上下文(Context),该上下文必须是指定的,而非默认的上下文。上下文的获得,是在业务对象创建和调用的时候,如果要获取指定的上下文,在.Net中,要求业务对象必须继承ContextBoundObject类。
因此,最后业务对象Calculator类的定义如下所示:
[MonitorAOP]
[LogAOP]
public class Calculator : ContextBoundObject
{
public int Add(int x,int y)
{
return x + y;
}
public int Substract(int x,int y)
{
return x - y;
}
}
[MonitorAOP]和[LogAOP]正是之前定义的方面Attribute,此外Calculator类继承了ContextBoundObject。除此之外,Calculator类的定义与普通的对象定义无异。然而,正是利用AOP技术,就可以拦截Calculator类的Add()和Substract()方法,对其进行日志记录和性能监测。而实现日志记录和性能监测的逻辑代码,则完全与Calculator类的Add()和Substract()方法分开,实现了两者之间依赖的解除,有利于模块的重用和扩展。
应用程序(Program)
我们可以实现简单的应用程序,来看看业务对象Calculator施加了日志方面和性能检测方面的效果:
class Program
{
[STAThread]
static void Main(string[] args)
{
Calculator cal = new Calculator();
cal.Add(3,5);
cal.Substract(3,5);
Console.ReadLine();
}
}
程序创建了一个Calculator对象,同时调用了Add()和Substract()方法。由于Calculator对象被施加了日志方面和性能检测方面,因此运行结果会将方法调用的详细信息和调用前后的运行当前时间打印出来。
如果要改变记录日志和性能监测结果的方式,例如将其写到文件中,则只需要改变LogAdvice和MonitorAdvice的实现,对于Calculator对象而言,则不需要作任何改变。
在《在.Net中关于AOP的实现》我通过动态代理的技术,基本上实现了AOP的几个技术要素,包括aspect,advice,pointcut。在文末我提到采用配置文件方式,来获取advice和pointcut之间的映射,从而使得构建aspect具有扩展性。
细细思考这个问题,我发现使用delegate来构建advice,似乎并非一个明智的选择。我在建立映射关系时,是将要拦截的方法名和拦截需要实现的aspect逻辑建立一个对应关系,而该aspect逻辑确实可以通过delegate,使其指向一族方法签名与该委托完全匹配的方法。这使得advice能够抽象化,以便于具体实现的扩展。然而,委托其实现毕竟是面向过程的范畴,虽然在.Net下,delegate本身仍是一个类对象,然而在创建具体的委托实例时,仍然很难通过配置文件和反射技术来获得。
考虑到委托具有的接口抽象的本质,也许采用接口的方式来取代委托更为可行。在之前的实现方案中,我为advice定义了两个委托:
public delegate void BeforeAOPHandle(IMethodCallMessage callMsg);
public delegate void AfterAOPHandle(IMethodReturnMessage replyMsg);
我可以定义两个接口IBeforeAction和IAfterAction,分别与这两个委托相对应:
public interface IBeforeAdvice
{
void BeforeAdvice(IMethodCallMessage callMsg);
}
public interface IAfterAdvice
{
void AfterAdvice(IMethodReturnMessage returnMsg);
}
通过定义的接口,可以将Advice与Aspect分离开来,这也完全符合OO思想中的“责任分离”原则。
(注:为什么要为Advice定义两个接口?这是考虑到有些Aspect只需要提供Before或After两个逻辑之一,如权限控制,就只需要before Action。)
那么当类库使用者,要定义自己的Aspect时,就可以定义具体的Advice类,来实现这两个接口,以及具体的Advice逻辑了。例如,之前提到的日志Aspect:
public class LogAdvice:IAfterAdvice,IBeforeAdvice
{
#region IBeforeAdvice Members
public void BeforeAdvice(IMethodCallMessage callMsg)
{
if (callMsg == null)
{
return;
}
Console.WriteLine("{0}({1},{2})",
callMsg.MethodName, callMsg.GetArg(0),
callMsg.GetArg(1));
}
#endregion
#region IAfterAdvice Members
public void AfterAdvice(IMethodReturnMessage returnMsg)
{
if (returnMsg == null)
{
return;
}
Console.WriteLine("Result is {0}", returnMsg.ReturnValue);
}
#endregion
}
而在AOPSink类的派生类中,添加方法名与Advice映射关系(此映射关系,我们即可理解为AOP的pointcut)时,就可以添加实现了Advice接口的类对象,如:
public override void AddAllBeforeAdvices()
{
AddBeforeAdvice("ADD",new LogAdvice());
AddBeforeAdvice("SUBSTRACT", new LogAdvice());
}
public override void AddAllAfterAdvices()
{
AddAfterAdvice("ADD",new LogAdvice());
AddAfterAdvice("SUBSTRACT", new LogAdvice());
}
由于LogAdvice类实现了接口IBeforeAdvice和IAfterAdvice,因此诸如new LogAdvice的操作均可以通过反射来创建该实例,如:
IBeforeAdvice beforeAdvice =
(IBeforeAdvice)Activator.CreateInstance("Wayfarer.AOPSample","Wayfarer.AOPSample.LogAdvice").Unwrap();
而CreateInstance()方法的参数值,是完全可以通过配置文件来配置的:
<aop>
<aspect value ="LOG">
<advice type="before" assembly="Wayfarer.AOPSample" class="Wayfarer.AOPSample.LogAdvice">
<pointcut>ADDpointcut>
<pointcut>SUBSTRACTpointcut>
advice>
<advice type="after" assembly="Wayfarer.AOPSample" class="Wayfarer.AOPSample.LogAdvice">
<pointcut>ADDpointcut>
<pointcut>SUBSTRACTpointcut>
advice>
aspect>
aop>
这无疑改善了AOP实现的扩展性。
《在.Net中关于AOP的实现》实现AOP的方案,要求包含被拦截方法的类必须继承ContextBoundObject。这是一个比较大的限制。不仅如此,ContextBoundObject对程序的性能也有极大的影响。我们可以做一个小测试。定义两个类,其中一个类继承ContextBoundObject。它们都实现了一个累加的操作:
class NormalObject
{
public void Sum(int n)
{
int sum = 0;
for (int i = 1; i <= n; i++)
{
sum += i;
}
Console.WriteLine("The result is {0}",sum);
Thread.Sleep(10);
}
}
class MarshalObject:ContextBoundObject
{
public void Sum(int n)
{
int sum = 0;
for (int i = 1; i <= n; i++)
{
sum += i;
}
Console.WriteLine("The result is {0}", sum);
Thread.Sleep(10);
}
}
然后执行这两个类的Sum()方法,测试其性能:
class Program
{
static void Main(string[] args)
{
long normalObjMs, marshalObjMs;
Stopwatch watch = new Stopwatch();
NormalObject no = new NormalObject();
MarshalObject mo = new MarshalObject();
watch.Start();
no.Sum(1000000);
watch.Stop();
normalObjMs = watch.ElapsedMilliseconds;
watch.Reset();
watch.Start();
mo.Sum(1000000);
watch.Stop();
marshalObjMs = watch.ElapsedMilliseconds;
watch.Reset();
Console.WriteLine("The normal object consume
{0} milliseconds.",normalObjMs);
Console.WriteLine("The contextbound object consume {0} milliseconds.",marshalObjMs);
Console.ReadLine();
}
}
得到的结果如下:
从性能的差异看,两者之间的差距是比较大的。如果将其应用在企业级的复杂逻辑上,这种区别就非常明显了,对系统带来的影响也是非常巨大的。
另外,在《在.Net中关于AOP的实现》文章后,有朋友发表了很多中肯的意见。其中有人提到了AOPAttribute继承ContextAttribute的问题。评论中提及微软在以后的版本中,不再提供ContextAttribute。如果真是如此,确有必要放弃继承ContextAttribute的形式。不过,在.Net中,除了ContextAttribute之外,还提供有一个接口IContextAttribute,该接口的定义为:
public interface IContextAttribute
{
void GetPropertiesForNewContext(IConstructionCallMessage msg);
bool IsContextOK(Context ctx, IConstructionCallMessage msg);
}
此时只需要将原来的AOPAttribute实现该接口即可:
public abstract class AOPAttribute:Attribute,
IContextAttribute//ContextAttribute
{
#region IContextAttribute Members
public void GetPropertiesForNewContext
(IConstructionCallMessage ctorMsg)
{
AOPProperty property = GetAOPProperty();
property.AspectXml = m_AspectXml;
property.AspectXmlFlag = m_AspectXmlFlag;
ctorMsg.ContextProperties.Add(property);
}
public bool IsContextOK(Context ctx,
IConstructionCallMessage ctorMsg)
{
return false;
}
#endregion
}
不知道,IContextAttribute似乎也会在未来的版本中被取消呢?
然而,从总体来看,这种使用ContextBoundObject的方式是不太理想的,也许它只能停留在实验室阶段,或许期待微软在未来的版本中得到更好的解决!
当然,如果采用Castle的DynamicProxy技术,可以突破必须继承CotextBoundObject的局限,但随着而来的局限却是AOP拦截的方法,要求必须是virtual的。坦白说,这样的限制,不过与前者乃“五十步笑百步”的区别而已。我还是期待有更好的解决方案。
说到AOP的几大要素,在这里可以补充说说,它主要包括:
1、Cross-cutting concern
在OO模型中,虽然大部份的类只有单一的、特定的功能,但它们通常会与其他类有着共同的第二需求。例如,当线程进入或离开某个方法时,我们可能既要在数据访问层的类中记录日志,又要在UI层的类中记录日志。虽然每个类的基本功能极然不同,但用来满足第二需求的代码却基本相同。
2、Advice
它是指想要应用到现有模型的附加代码。例如在《在.Net中关于AOP的实现》的例子中,是指关于打印日志的逻辑代码。
3、Point-cut
这个术语是指应用程序中的一个执行点,在这个执行点上需要采用前面的cross-cutting concern。如例子中,执行Add()方法时出现一个Point-cut,当方法执行完毕,离开方法时又出现另一个Point-cut。
4、Aspect
Point-cut和advice结合在一起就叫做aspect。如例子中的Log和Monitor。在对本例的重构中,我已经AOPSink更名为Aspect,相应的LogAOPSink、MonitorAOPSink也更名为LogAspect,MonitorAspect。
以上提到的PointCut和Advice在AOP技术中,通常称为动态横切技术。与之相对应的,是较少被提及的静态横切。它与动态横切的区别在于它并不修改一个给定对象的执行行为,相反,它允许通过引入附加的方法属性和字段来修改对象固有的结构。在很多AOP实现中,将静态横切称为introduce或者mixin。
在开发应用系统时,如果需要在不修改原有代码的前提下,引入第三方产品和API库,静态横切技术是有很大的用武之地的。从这一点来看,它有点类似于设计模式中提到的Adapter模式需要达到的目标。不过,看起来静态横切技术应比Adapter模式更加灵活和功能强大。
例如,一个已经实现了收发邮件的类Mail。然而它并没有实现地址验证的功能。现在第三方提供了验证功能的接口IValidatable:
public interface IValidatable
{
bool ValidateAddress();
}
如果没有AOP,采用设计模式的方式,在不改变Mail类的前提下,可以通过Adapter模式,引入MailAdater,继承Mail类,同时实现IValidatable接口。采用introduce技术,却更容易实现该功能的扩展,我们只需要定义aspect:(注:java代码,使用了AspectJ)
import com.acme.validate.Validatable;
public aspect EmailValidateAspect
{
declare parents: Email implements IValidatable;
public boolean Email.validateAddress(){
if(this.getToAddress() != null){
return true;
}else{
return false;
}
}
}
从上可以看到,通过EmailValidateAspect方面,为Email类introduce了新的方法ValidateAddress()。非常容易的就完成了Email的扩展。
我们可以比较一下,如果采用Adapter模式,原有的Email类是不能被显示转换为IValidatable接口的,也即是说如下的代码是不可行的:
Email mail = new Email();
IValidatable validate = ((IValidatable)mail).ValidateAddress();
要调用ValidateAddress()方法,必须通过EmailAdapter类。然而通过静态横切技术,上面的代码就完全可行了。