(適用於.NET/.NET Core/.NET Framework) 【目錄】0.前言1.第一個AOP程式2.Aspect橫切麵編程3.一個橫切麵程式攔截多個主程式4.多個橫切麵程式攔截一個主程式5.AOP的泛型處理(擴充)6.AOP的非同步處理(擴充)7.優勢總結8.展望 0.前言 AOP(Aspe ...
(適用於.NET/.NET Core/.NET Framework)
【目錄】
0.前言
1.第一個AOP程式
2.Aspect橫切麵編程
3.一個橫切麵程式攔截多個主程式
4.多個橫切麵程式攔截一個主程式
5.AOP的泛型處理(擴充)
6.AOP的非同步處理(擴充)
7.優勢總結
8.展望
0.前言
AOP(Aspect Oriented Programming)是“面向橫切麵編程”,主要是用來對程式/模塊進行解耦。怎麼理解??
我們可以把一般的編程理解為“縱向編程”(主程式),比如如下的一個示例代碼:
public string GetInfo(int i) { string s = ""; if (i == 1) s = "A"; else if (i == 2) s = "B"; else if (i == 3) s = "C"; else s = "Z"; return s; }
試想一下,上述軟體實際使用後,
- 如果條件變數i有更多的判斷值,我們是不是要在GetInfo()方法內部修改代碼+重新編譯?
- 如果後續需要加個日誌記錄功能,我們是不是也要在GetInfo()方法內部加上日誌函數+重新編譯?
- 如果...
- 更多如果...
為了避免上述的這些麻煩並增加軟體的靈活性,“橫向編程”,也就是AOP被創造了出來,它就像是“橫切一刀”,把相關功能塞進了主程式。
現行AOP的實現,主要是通過攔截方法(即攔截主程式),並修改其參數+返回值來完成。
網上有很多相關方案,比如:特性註釋攔截、動態代碼生成、派遣代理模式、等。但這些方案要麼實現的很複雜、要麼耦合度沒完全切斷、邏輯有變化時還是需要修改代碼重新編譯。均不夠理想。
而今天要隆重登場的主角-DeveloperSharp平臺中的AOP技術,則提供了一種簡便、快捷、徹底解耦的AOP實現。使用它,在程式邏輯有變化時,你只需要修改配置文件就行,而不再需要對主程式進行一丁丁點的代碼修改!!
1.第一個AOP程式
製作一個AOP程式需要四個步驟:
(1)製作主程式
(2)製作橫切麵程式
(3)製作配置文件,讓橫切麵程式攔截主程式
(4)調用主程式
下麵,我們一步一步來實現上述四個步驟。
【第一步】:製作主程式
我們在Visual Studio中新建一個名為“School.Logic”的類庫工程,併在該工程中新建一個名為PersonManage的類,該類中有一個名為GetInfo1的方法,代碼如下:
//從NuGet引用DeveloperSharp包 using DeveloperSharp.Structure.Model; namespace School.Logic { //主程式必須繼承自LogicLayer類 public class PersonManage : LogicLayer { public string GetInfo1(string Name, int Num) { return $"共有{Name}{Num}人"; } } }
以上,編寫了一個非常簡單的主程式。
【第二步】:製作橫切麵程式
我們再在Visual Studio中新建一個名為“School.Aspect”的類庫工程,併在該工程中新建一個名為Interceptor1的類,代碼如下:
//從NuGet引用DeveloperSharp包 using DeveloperSharp.Structure.Model.Aspect; namespace School.Aspect { //橫切麵程式必須繼承自AspectModel類 public class Interceptor1 : AspectModel { //PreProcess方法先於主程式執行 public override void PreProcess(object sender, AspectEventArgs e) { //把主程式的兩個參數值改掉 e.MethodInfo.ParameterValues[0] = "老師"; e.MethodInfo.ParameterValues[1] = 20; } //PostProcess方法後於主程式執行 public override void PostProcess(object sender, AspectEventArgs e) { } } }
以上,編寫了一個橫切麵程式。它的主要功能是把主程式方法的兩個參數值給改掉。
AspectModel基類中的PreProcess方法會在主程式方法執行之前被執行,而PostProcess方法會在主程式方法執行之後被執行。它兩就是AOP橫向攔截的核心要素。它兩均需要被override重寫覆蓋掉。
【第三步】:製作配置文件,讓橫切麵程式攔截主程式
若是在.Net Core環境下,我們創建一個名為DeveloperSharp.json的配置文件,設置讓Interceptor1攔截PersonManage中的GetInfo1方法。文件內容如下:
{ "DeveloperSharp": { "AspectObject": [ { "name":"School.Aspect.Interceptor1", //橫切麵攔截器類 "scope":"School.Logic.PersonManage", //被攔截的主程式類 "method":"GetInfo1" //被攔截的方法 } ] } }
若是在.Net Framework環境下,我們創建一個名為DeveloperSharp.xml的配置文件,設置讓Interceptor1攔截PersonManage中的GetInfo1方法。文件內容如下:
<?xml version="1.0" encoding="utf-8" ?> <DeveloperSharp> <AspectObject> <Ao name="School.Aspect.Interceptor1" scope="School.Logic.PersonManage" method="GetInfo1"/> </AspectObject> </DeveloperSharp>
註意:以上配置中所有的類名,都要用完全限定名。
【第四步】:調用主程式
最後,我們再在Visual Studio中創建一個控制台工程,讓它來調用主程式中的GetInfo1方法,代碼如下:
//需要引用School.Aspect、School.Logic、DeveloperSharp三項 static void Main(string[] args) { var pm = new School.Logic.PersonManage(); //要用這種形式調用主程式中的方法,AOP功能才會生效 var str = pm.InvokeMethod("GetInfo1", "學生", 200); Console.WriteLine(str); Console.ReadLine(); }
附註:有人會覺得上述InvokeMethod這種調用方法不夠優雅,但事實上ASP.NET Web Api也是被類似InvokeMethod這種方式包裹調用才實現了各種Filter攔截器的攔截(本質也是AOP),只不過它的這個InvokeMethod動作是在.NET自身的CLR管道運行時中進行的。而且,那些Filter攔截器還只能用於ASP.NET Web Api環境,而不能像本方案這樣用於一般程式。
現在,為了讓前面第三步創建的配置文件生效,我們此時還需要在此主調項目中對它進行鏈接:
若是在.Net Core環境下,我們只需要把DeveloperSharp.json文件放到程式執行目錄中(即bin目錄下與dll、exe等文件的同一目錄中,放錯了位置會報錯)(註意:有些.Net Core版本在Visual Studio“調試”時,不會在bin目錄下生成全部的dll、exe,此時需要把此配置文件放在應用程式的“根目錄”下)。
若是在.Net Framework環境下,我們需要在工程配置文件App.config/Web.config中添加appSettings節點,節點內容如下:
<appSettings> <add key="ConfigFile" value="D:\Test\Assist\DeveloperSharp.xml" /> </appSettings>
此處需要設置為配置文件的“絕對路徑”(使用“絕對路徑”而不是“相對路徑”,一是有利於安全性,二是有利於分散式部署)
一切準備完畢,運行,結果如下:
【控制台顯示出】:共有老師20人
可見AOP已經攔截成功。
若此時,我們在配置文件DeveloperSharp.json/DeveloperSharp.xml中稍做修改,比如:把“GetInfo1”這個方法名改為“ABC”這樣一個不存在的方法名,再運行,結果如下:
【控制台顯示出】:共有學生200人
2.Aspect橫切麵編程
上面,第二步,製作的橫切麵程式,是通過修改主程式方法的參數值,而最終改變了主程式的返回值。
其實,我們也有辦法直接修改主程式方法的返回值,比如把上面Interceptor1類的代碼修改為如下:
//從NuGet引用DeveloperSharp包 using DeveloperSharp.Structure.Model.Aspect; namespace School.Aspect { //橫切麵程式必須繼承自AspectModel類 public class Interceptor1 : AspectModel { //PreProcess方法先於主程式執行 public override void PreProcess(object sender, AspectEventArgs e) { } //PostProcess方法後於主程式執行 public override void PostProcess(object sender, AspectEventArgs e) { //把主程式的返回值改掉 e.MethodInfo.ReturnValue = $"共有校長2人"; } } }
運行,結果如下:
【控制台顯示出】:共有校長2人
到目前為止,我們已經知道瞭如何通過“Aspect橫切麵程式”修改主程式方法的參數值、返回值。
如果我們想進一步獲取主程式的“命名空間”、“類名”、“方法名”、“參數名”、“參數類型”、“返回值類型”,則可以通過如下代碼獲取:
e.MethodInfo.NamespaceName //命名空間 e.MethodInfo.ClassName //類名 e.MethodInfo.MethodName //方法名 e.MethodInfo.ParameterInfos[0].Name //參數名(第一個參數) e.MethodInfo.ParameterInfos[0].ParameterType //參數類型(第一個參數) e.MethodInfo.ReturnValue.GetType() //返回值類型
有時候,在某些特殊情況下,我們希望主程式方法不運行,此時則可以通過在PreProcess方法里把e.Continue設置為false來完成。
接前面的“第一個AOP程式”,比如:我們希望當人數大於10000時,主程式方法就不再運行,則可以通過把Interceptor1類的代碼修改為如下樣式來實現:
//從NuGet引用DeveloperSharp包 using DeveloperSharp.Structure.Model.Aspect; namespace School.Aspect { //橫切麵程式必須繼承自AspectModel類 public class Interceptor1 : AspectModel { //PreProcess方法先於主程式執行 public override void PreProcess(object sender, AspectEventArgs e) { //當人數大於10000時,主程式方法就不再運行 if (Convert.ToInt32(e.MethodInfo.ParameterValues[1]) > 10000) e.Continue = false; } //PostProcess方法後於主程式執行 public override void PostProcess(object sender, AspectEventArgs e) { } } }
現在的這個示例是一個Aspect橫切麵程式攔截一個主程式。在後續將要講解的“多個Aspect橫切麵程式攔截一個主程式”的情況中,只要有一個e.Continue=false被設置,主程式方法就不會運行(在此事先提點)。
3.一個橫切麵程式攔截多個主程式
為了演示這部分的內容,我們首先在前面“第一個AOP程式”的基礎上,把主程式進行擴充。採取的動作是:
(1)在PersonManage類中增加一個GetInfo2方法
(2)再新增一個主程式類SystemManage,該類中有一個名為GetMessage1的方法。代碼如下:
PersonManage類:
//從NuGet引用DeveloperSharp包 using DeveloperSharp.Structure.Model; namespace School.Logic { //主程式必須繼承自LogicLayer類 public class PersonManage : LogicLayer { public string GetInfo1(string Name, int Num) { return $"共有{Name}{Num}人"; } public string GetInfo2(string Name, int Num) { return $"學校共有{Name}{Num}人"; } } }
SystemManage類:
//從NuGet引用DeveloperSharp包 using DeveloperSharp.Structure.Model; namespace School.Logic { //主程式必須繼承自LogicLayer類 public class SystemManage : LogicLayer { public string GetMessage1(string Name1, int Num1, string Name2, int Num2) { return $"第一組共有{Name1}{Num1}人,第二組共有{Name2}{Num2}人"; } } }
如此一來,現在就有了3個主程式方法。
接下來,我們修改配置文件,讓Interceptor1去攔截上述的3個主程式方法。
若是在.Net Core環境下,DeveloperSharp.json文件的內容修改為如下:
{ "DeveloperSharp": { "AspectObject": [ { "name":"School.Aspect.Interceptor1", "scope":"School.Logic.PersonManage", "method":"*" //星號*代表該作用域下的全部方法 }, { "name":"School.Aspect.Interceptor1", "scope":"School.Logic.SystemManage", "method":"GetMessage1" } ] } }
若是在.Net Framework環境下,DeveloperSharp.xml文件的內容修改為如下:
<?xml version="1.0" encoding="utf-8" ?> <DeveloperSharp> <AspectObject> <Ao name="School.Aspect.Interceptor1" scope="School.Logic.PersonManage" method="*"/> <Ao name="School.Aspect.Interceptor1" scope="School.Logic.SystemManage" method="GetMessage1"/> </AspectObject> </DeveloperSharp>
最後,我們把控制台啟動程式修改為如下:
//需要引用School.Aspect、School.Logic、DeveloperSharp三項 static void Main(string[] args) { var pm = new School.Logic.PersonManage(); var sm = new School.Logic.SystemManage(); //要用這種形式調用主程式中的方法,AOP功能才會生效 var str1 = pm.InvokeMethod("GetInfo1", "學生", 200); var str2 = pm.InvokeMethod("GetInfo2", "學生", 200); var str3 = sm.InvokeMethod("GetMessage1", "學生", 200, "院士", 10); Console.WriteLine(str1); Console.WriteLine(str2); Console.WriteLine(str3); Console.ReadLine(); }
運行結果如下:
【控制台顯示出】:
共有老師20人
學校共有老師20人
第一組共有老師20人,第二組共有院士10人
可見AOP所有攔截均已成功!
4.多個橫切麵程式攔截一個主程式
為了演示這部分的內容,我們還是要先回到前面的“第一個AOP程式”,在它的基礎上,我們新增一個名為Interceptor2的Aspect橫切麵類,代碼如下:
//從NuGet引用DeveloperSharp包 using DeveloperSharp.Structure.Model.Aspect; namespace School.Aspect { //橫切麵程式必須繼承自AspectModel類 public class Interceptor2 : AspectModel { //PreProcess方法先於主程式執行 public override void PreProcess(object sender, AspectEventArgs e) { //把主程式的兩個參數值改掉 e.MethodInfo.ParameterValues[0] = "輔導員"; e.MethodInfo.ParameterValues[1] = 40; } //PostProcess方法後於主程式執行 public override void PostProcess(object sender, AspectEventArgs e) { } } }
如此一來,我們就有了2個Aspect橫切麵程式Interceptor1與Interceptor2。
接下來,我們修改配置文件,讓Interceptor1、Interceptor2都去攔截主程式方法GetInfo1。
若是在.Net Core環境下,DeveloperSharp.json文件的內容修改為如下:
{ "DeveloperSharp": { "AspectObject": [ { "name":"School.Aspect.Interceptor1", "scope":"School.Logic.PersonManage", "method":"GetInfo1" }, { "name":"School.Aspect.Interceptor2", "scope":"School.Logic.PersonManage", "method":"GetInfo1" } ] } }
若是在.Net Framework環境下,DeveloperSharp.xml文件的內容修改為如下:
<?xml version="1.0" encoding="utf-8" ?> <DeveloperSharp> <AspectObject> <Ao name="School.Aspect.Interceptor1" scope="School.Logic.PersonManage" method="GetInfo1"/> <Ao name="School.Aspect.Interceptor2" scope="School.Logic.PersonManage" method="GetInfo1"/> </AspectObject> </DeveloperSharp>
上述修改完畢,運行控制台主調程式,結果如下:
【控制台顯示出】:共有輔導員40人
從上述運行結果,我們大致可以推斷出:Interceptor1、Interceptor2這兩個Aspect橫切麵攔截器是按配置順序執行的。其中,Interceptor1先把GetInfo1方法的兩個參數值改為了("老師",20),接著,Interceptor2又把GetInfo1方法的兩個參數值改為了("輔導員",40),所以最終GetInfo1方法的參數值變為了("輔導員",40)。
5.AOP的泛型處理
如果我們的主程式是泛型方法,則需要用InvokeMethod<T>這種方式來進行調用。
比如,現有如下的主程式代碼:
//從NuGet引用DeveloperSharp包 using DeveloperSharp.Structure.Model; namespace Test4Logic { //主程式必須繼承自LogicLayer類 public class Calculate : LogicLayer { public int add(int i, int j) { return i + j; } public int add(int i, int j, int k) { return i + j + k; } public string add<T>(T i, T j, T k) { return "T" + i.ToString() + j.ToString() + k.ToString(); } public string add<T, V>(T i, T j, V k) { return "TTV" + i.ToString() + j.ToString() + k.ToString(); } public string add<T, V>(T i, V j, V k) { return "TVV" + i.ToString() + j.ToString() + k.ToString(); } } }
對應的Aspect橫切麵類代碼如下:
//從NuGet引用DeveloperSharp包 using DeveloperSharp.Structure.Model.Aspect; namespace Test4Aspect { //橫切麵程式必須繼承自AspectModel類 public class Interceptor : AspectModel { //PreProcess方法先於主程式執行 public override void PreProcess(object sender, AspectEventArgs e) { //把主程式的第一個參數值改掉 e.MethodInfo.ParameterValues[0] = 8; } //PostProcess方法後於主程式執行 public override void PostProcess(object sender, AspectEventArgs e) { } } }
對應的配置文件如下:
若是在.Net Core環境下,DeveloperSharp.json文件的內容如下:
{ "DeveloperSharp": { "AspectObject": [ { "name":"Test4Aspect.Interceptor", //橫切麵攔截器類 "scope":"Test4Logic.Calculate", //被攔截的主程式類 "method":"add" //被攔截的方法 } ] } }
若是在.Net Framework環境下,DeveloperSharp.xml文件的內容如下:
<?xml version="1.0" encoding="utf-8" ?> <DeveloperSharp> <AspectObject> <Ao name="Test4Aspect.Interceptor" scope="Test4Logic.Calculate" method="add"/> </AspectObject> </DeveloperSharp>
控制台主調程式代碼如下:
//需要引用Test4Aspect、Test4Logic、DeveloperSharp三項 static void Main(string[] args) { var cal = new Test4Logic.Calculate(); //要用這種形式調用主程式中的方法,AOP功能才會生效 var r1 = cal.InvokeMethod("add", 1, 2); var r2 = cal.InvokeMethod("add", 1, 2, 3); var r3 = cal.InvokeMethod<int>("add", 1, 2, 3); var r4 = cal.InvokeMethod<int, float>("add", 1, 2, (float)3); var r5 = cal.InvokeMethod<int, float>("add", 1, (float)2, (float)3); Console.WriteLine(r1); Console.WriteLine(r2); Console.WriteLine(r3); Console.WriteLine(r4); Console.WriteLine(r5); Console.ReadLine(); }
運行上述控制台主調程式,結果如下:
【控制台顯示出】:
10
13
T823
TTV823
TVV823
主程式中每個泛型方法的對應調用一目瞭然。
6.AOP的非同步處理
如果我們的主程式是非同步方法,還是使用InvokeMethod來進行調用。下麵給出一個代碼樣式示例(代碼做了簡化處理):
//主程式 //主程式必須繼承自LogicLayer類 public class UserService : LogicLayer { public async Task<Worker> GetUser(string Id, int Age, string Name) { //...相關代碼... } public async Task<T> GetUser<T>(string Id, int Age, string Name) where T : User, new() { //...相關代碼... } } ---------------------------------------------------------------------- //主調程式 var us = new UserService(); //要用這種形式調用主程式中的方法,AOP功能才會生效 var worker = await us.InvokeMethod("GetUser", "C007", 26, "alex"); var user = await us.InvokeMethod<Manager>("GetUser", "A002", 46, "kevin"); Console.WriteLine(worker.Name); Console.WriteLine(user?.Name);
即然主程式可以是非同步的,那Aspect橫截面攔截程式能不能也是非同步的了?答案是肯定的。你可以把PreProcess與PostProcess中的至少一個改為非同步方法,實現單個Aspect類的同步非同步混用,其代碼結構與原先的同步Aspect類一致,這點連.NET/微軟自身都還沒有實現...
下麵給出一個示例代碼:
//橫切麵程式必須繼承自AspectModel類 public class UserInterceptor : AspectModel { //PreProcess方法先於主程式執行 public override void PreProcess(object sender, AspectEventArgs e) { } //PostProcess方法後於主程式執行 public override async void PostProcess(object sender, AspectEventArgs e) { await Task.Run(() => { Thread.Sleep(10000); File.AppendAllText("D:/zzz.txt", "耗時操作"); }); } }
7.優勢總結
本文所講述的,是全網唯一實現AOP徹底解耦的技術方案。使用它,當你需要給主程式增加Aspect橫切麵攔截器時,無論是增加一個還是多個,都不再需要修改&重新編譯主程式。這實現了不同功能構件之間的0依賴拼裝/拆卸開發方式,隨之而來的也會對研發團隊的管理模式產生重大影響,意義深遠...
8.展望
AOP對於程式代碼的解耦、業務模塊的拆分與拼裝組合,有著巨大的作用。正確的使用AOP,甚至能對傳統的軟體架構設計,產生顛覆性的影響,如超級戰士出場一般,讓所有人刮目相看,完全耳目一新!!
為了讓讀者能直觀感知AOP的上述神奇魅力,下麵給出一個業務案例:
有一批貨品要錄入資料庫,貨品包含長、寬、高、顏色、類型等屬性。現在有業務需求如下,
(1)當貨