.Net語言中關于AOP 的實現詳解
文章主要和大家講解開發應用系統時在.Net語言中關于AOP 的實現。LogAspect完成的功能主要是將Advice與業務對象的方法建立映射,并將其添加到Advice集合中。由于我們在AOP實現中,利用了xml配置文件來配置PointCut,因此對于所有Aspect而言,這些操作都是相同的,只要定義了正確的配置文件,將其讀入即可。對于Aspect的SyncProcessMessage(),由于攔截和織入的方法是一樣的,不同的只是Advice的邏輯而已,因此在所有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接口:
{
#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對象:
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:
{
protected override IMessageSink CreateAspect
(IMessageSink nextSink)
{
return new MonitorAspect(nextSink);
}
protected override string GetName()
{
return "MonitorAOP";
}
}
4.4.2.3性能監測Aspect(MonitorAspect)
MonitorAspect類的實現同樣簡單:
{
public MonitorAspect(IMessageSink nextSink):base(nextSink)
{}
}
而其配置文件的定義則如下所示:
<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接口:
{
#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類的定義如下所示:
[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施加了日志方面和性能檢測方面的效果:
{
[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,分別與這兩個委托相對應:
{
void BeforeAdvice(IMethodCallMessage callMsg);
}
public interface IAfterAdvice
{
void AfterAdvice(IMethodReturnMessage returnMsg);
}
通過定義的接口,可以將Advice與Aspect分離開來,這也完全符合OO思想中的“責任分離”原則。
(注:為什么要為Advice定義兩個接口?這是考慮到有些Aspect只需要提供Before或After兩個邏輯之一,如權限控制,就只需要before Action。)
那么當類庫使用者,要定義自己的Aspect時,就可以定義具體的Advice類,來實現這兩個接口,以及具體的Advice邏輯了。例如,之前提到的日志Aspect:
{
#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接口的類對象,如:
{
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)Activator.CreateInstance("Wayfarer.AOPSample","Wayfarer.AOPSample.LogAdvice").Unwrap();
而CreateInstance()方法的參數值,是完全可以通過配置文件來配置的:
<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。它們都實現了一個累加的操作:
{
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,該接口的定義為:
{
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:
{
bool ValidateAddress();
}
如果沒有AOP,采用設計模式的方式,在不改變Mail類的前提下,可以通過Adapter模式,引入MailAdater,繼承Mail類,同時實現IValidatable接口。采用introduce技術,卻更容易實現該功能的擴展,我們只需要定義aspect:(注:java代碼,使用了AspectJ)
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類。然而通過靜態橫切技術,上面的代碼就完全可行了。