一起谈.NET技术,.Net语言中关于AOP 的实现详解

简介: 文章主要和大家讲解开发应用系统时在.Net语言中关于AOP 的实现。LogAspect完成的功能主要是将Advice与业务对象的方法建立映射,并将其添加到Advice集合中。由于我们在AOP实现中,利用了xml配置文件来配置PointCut,因此对于所有Aspect而言,这些操作都是相同的,只要定义了正确的配置文件,将其读入即可。

文章主要和大家讲解开发应用系统时在.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类。然而通过静态横切技术,上面的代码就完全可行了。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
14天前
|
人工智能 开发框架 量子技术
【专栏】.NET 技术:驱动创新的力量
【4月更文挑战第29天】.NET技术,作为微软的开发框架,以其跨平台、开源和语言多样性驱动软件创新。它在云计算、AI/ML、混合现实等领域发挥关键作用,通过Azure、ML.NET等工具促进新兴技术发展。未来,.NET将涉足量子计算、微服务和无服务器计算,持续拓宽软件开发边界,成为创新的重要推动力。掌握.NET技术,对于开发者而言,意味着握有开启创新的钥匙。
|
14天前
|
开发框架 .NET C#
【专栏】理解.NET 技术,提升开发水平
【4月更文挑战第29天】本文介绍了.NET技术的核心概念和应用,包括其跨平台能力、性能优化、现代编程语言支持及Web开发等特性。文章强调了深入学习.NET技术、关注社区动态、实践经验及学习现代编程理念对提升开发水平的重要性。通过这些,开发者能更好地利用.NET构建高效、可维护的多平台应用。
|
14天前
|
机器学习/深度学习 vr&ar 开发者
【专栏】.NET 技术:引领开发新方向
【4月更文挑战第29天】本文探讨了.NET技术如何引领软件开发新方向,主要体现在三方面:1) 作为跨平台开发的先锋,.NET Core支持多操作系统和移动设备,借助.NET MAUI创建统一UI,适应物联网需求;2) 提升性能和开发者生产力,采用先进技术和优化策略,同时更新C#语言特性,提高代码效率和可维护性;3) 支持现代化应用架构,包括微服务、容器化,集成Kubernetes和ASP.NET Core,保障安全性。此外,.NET还不断探索AI、ML和AR/VR技术,为软件开发带来更多创新可能。
|
14天前
|
开发框架 Cloud Native 开发者
【专栏】剖析.NET 技术的核心竞争力
【4月更文挑战第29天】本文探讨了.NET框架在软件开发中的核心竞争力:1) .NET Core实现跨平台与云原生技术的融合,支持多操作系统和容器化;2) 提升性能和开发者生产力,采用JIT、AOT优化,提供C#新特性和Roslyn编译器平台;3) 支持现代化应用架构,包括微服务和容器化,内置安全机制;4) 丰富的生态系统和社区支持,拥有庞大的开发者社区和微软的持续投入。这些优势使.NET在竞争激烈的市场中保持领先地位。
|
14天前
|
开发框架 .NET 开发者
【专栏】领略.NET 技术的创新力量
【4月更文挑战第29天】.NET技术自ASP.NET起历经创新,现以.NET Core为核心,展现跨平台能力,提升性能与生产力,支持现代化应用架构。.NET Core使开发者能用同一代码库在不同操作系统上构建应用,扩展至移动和物联网领域。性能提升,C#新特性简化编程,Roslyn编译器优化代码。拥抱微服务、容器化,内置安全机制,支持OAuth等标准。未来.NET 6将引入更快性能、Hot Reload等功能,预示着.NET将持续引领软件开发潮流,为开发者创造更多机会。
|
14天前
|
物联网 vr&ar 开发者
【专栏】.NET 技术:为开发注入活力
【4月更文挑战第29天】本文探讨了.NET技术的创新,主要体现在三个方面:1) .NET Core实现跨平台开发革命,支持多种操作系统和硬件,如.NET MAUI用于多平台UI;2) 性能提升与生产力飞跃,C#新特性简化编程,JIT和AOT优化提升性能,Roslyn提供代码分析工具;3) 引领现代化应用架构,支持微服务、容器化,内置安全机制。未来,.NET 7将带来更多新特性和前沿技术整合,如量子计算、AI,持续推动软件开发创新。开发者掌握.NET技术将赢得竞争优势。
|
14天前
|
人工智能 前端开发 Cloud Native
【专栏】洞察.NET 技术的开发趋势
【4月更文挑战第29天】本文探讨了.NET技术的三大发展趋势:1) 跨平台与云原生技术融合,通过.NET Core支持轻量级、高性能应用,适应云计算和微服务;2) 人工智能与机器学习的集成,如ML.NET框架,使开发者能用C#构建AI模型;3) 引入现代化前端开发技术,如Blazor,实现前后端一致性。随着.NET 8等新版本的发布,期待更多创新技术如量子计算、AR/VR的融合,.NET将持续推动软件开发的创新与进步。
|
3月前
|
Java 数据库连接 应用服务中间件
Spring5源码(39)-Aop事物管理简介及编程式事物实现
Spring5源码(39)-Aop事物管理简介及编程式事物实现
26 0
|
4月前
AOP&面向切面编程
AOP&面向切面编程
56 0
|
4月前
|
Java 程序员 Maven
Spring AOP入门指南:轻松掌握面向切面编程的基础知识
Spring AOP入门指南:轻松掌握面向切面编程的基础知识