.Net“.NET研究”语言中关于AOP 的实现详解

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 文章主要和大家讲解开发应用系统时在.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 > 上海网站建设tyle="color: #000000;">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只需要记录方法调用前后的时间,因此只需要分别在BeforeAd上海企业网站设计与制作vice()和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_AspectX上海徐汇企业网站设计与制作mlFlag;
ctorMsg.ContextProperties.Add(property);
}
public bool IsContextOK(Context ctx,
IConstructionCallMessage ctorMsg)
{
return false ;
}
#endregion
}

不知道,IContextAttribute似乎也会在未来的版本中被取消呢?

然而,从总体来看,这种使用ContextBoundObject的方式是不太理想的,也许它只能停留在实验室阶段,或许期待微软在未来的版本中得到更好的解决!

当然,如果采用Castle的DynamicProxy技术,可以突破必须继承CotextBoundObject的局限,但随着而来的局限却是AOP拦截的方法,要求必须是virtual的。坦白说,这样的限制,不过与前者乃“五十步笑百步”的区别而已。我还是期待有更好的解决方案。

上海徐汇企业网站制作p style="text-indent: 2em;">说到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日志并进行多维度分析。
目录
相关文章
|
编解码 JSON 网络协议
Golang 语言使用标准库 net/rpc/jsonrpc 包跨语言远程调用
Golang 语言使用标准库 net/rpc/jsonrpc 包跨语言远程调用
132 0
|
1月前
|
XML 监控 安全
深入调查研究Spring AOP
【11月更文挑战第15天】
43 5
|
1月前
|
缓存 监控 Java
|
2月前
|
JSON C# 开发者
C#语言新特性深度剖析:提升你的.NET开发效率
【10月更文挑战第15天】C#语言凭借其强大的功能和易用性深受开发者喜爱。随着.NET平台的演进,C#不断引入新特性,如C# 7.0的模式匹配和C# 8.0的异步流,显著提升了开发效率和代码可维护性。本文将深入探讨这些新特性,助力开发者在.NET开发中更高效地利用它们。
44 1
|
5月前
|
人工智能 物联网 开发工具
.NET技术:多元语言、丰富库与跨平台能力引领软件开发新纪元。
`【7月更文挑战第4天】.NET技术:多元语言、丰富库与跨平台能力引领软件开发新纪元。从企业应用、云服务到游戏开发,其角色日益凸显。随着微软的持续创新与社区合作,未来.NET将在物联网、AI等领域拓宽应用,开发者应把握趋势,共创未来。`
48 0
|
4月前
|
JSON C# 开发者
💡探索C#语言进化论:揭秘.NET开发效率飙升的秘密武器💼
【8月更文挑战第28天】C#语言凭借其强大的功能与易用性深受开发者喜爱。伴随.NET平台演进,C#持续引入新特性,如C# 7.0的模式匹配,让处理复杂数据结构更直观简洁;C# 8.0的异步流则使异步编程更灵活高效,无需一次性加载全部数据至内存。通过示例展示了模式匹配简化JSON解析及异步流实现文件逐行读取的应用。此外,C# 8.0还提供了默认接口成员和可空引用类型等特性,进一步提高.NET开发效率与代码可维护性。随着C#的发展,未来的.NET开发将更加高效便捷。
67 1
|
4月前
分享一份 .NET Core 简单的自带日志系统配置,平时做一些测试或个人代码研究,用它就可以了
分享一份 .NET Core 简单的自带日志系统配置,平时做一些测试或个人代码研究,用它就可以了
|
6月前
|
机器学习/深度学习 JSON 测试技术
CNN依旧能战:nnU-Net团队新研究揭示医学图像分割的验证误区,设定先进的验证标准与基线模型
在3D医学图像分割领域,尽管出现了多种新架构和方法,但大多未能超越2018年nnU-Net基准。研究发现,许多新方法的优越性未经严格验证,揭示了验证方法的不严谨性。作者通过系统基准测试评估了CNN、Transformer和Mamba等方法,强调了配置和硬件资源的重要性,并更新了nnU-Net基线以适应不同条件。论文呼吁加强科学验证,以确保真实性能提升。通过nnU-Net的变体和新方法的比较,显示经典CNN方法在某些情况下仍优于理论上的先进方法。研究提供了新的标准化基线模型,以促进更严谨的性能评估。
177 0
|
7月前
|
中间件 Go API
Golang深入浅出之-Go语言标准库net/http:构建Web服务器
【4月更文挑战第25天】Go语言的`net/http`包是构建高性能Web服务器的核心,提供创建服务器和发起请求的功能。本文讨论了使用中的常见问题和解决方案,包括:使用第三方路由库改进路由设计、引入中间件处理通用逻辑、设置合适的超时和连接管理以防止资源泄露。通过基础服务器和中间件的代码示例,展示了如何有效运用`net/http`包。掌握这些最佳实践,有助于开发出高效、易维护的Web服务。
93 1
|
Go
Golang 语言怎么使用 net/http 标准库开发 http 应用?
Golang 语言怎么使用 net/http 标准库开发 http 应用?
59 0