利用C#實現AOP常見的幾種方法詳解


利用C#實現AOP常見的幾種方法詳解

 

 AOP面向切面編程(Aspect Oriented Programming)

是通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。

下面這篇文章主要給大家介紹了關於利用C#實現AOP常見的幾種方法,需要的朋友可以參考借鑒,下面來一起看看吧。

 

前言

AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的中統一處理業務邏輯的一種技術

比較常見的場景是:日志記錄,錯誤捕獲、性能監控等

AOP的本質是通過代理對象來間接執行真實對象,在代理類中往往會添加裝飾一些額外的業務代碼,如下代碼:

class RealA
 {
     public virtual string Pro { get; set; }
  
     public virtual void ShowHello(string name)
     {
         Console.WriteLine($"Hello!{name},Welcome!");
     }
 }
  
  
//調用:
  
 var a = new RealA();
 a.Pro = "測試";
 a.ShowHello("夢在旅途");

這段代碼很簡單,只是NEW一個對象,然后設置屬性及調用方法,

但如果我想在設置屬性前后調用方法前后報錯都能收集日志信息,該如何做呢?

可能大家會想到,在設置屬性及調用方法前后都加上記錄日志的代碼不就可以了,雖然這樣是可以,但如果很多地方都要用到這個類的時候,那重復的代碼是否太多了一些吧,所以我們應該使用代理模式或裝飾模式

將原有的真實類RealA委托給代理類ProxyRealA來執行,代理類中在設置屬性及調用方法時,再添加記錄日志的代碼就可以了,這樣可以保證代碼的干凈整潔,也便於代碼的后期維護。(注意,在C#中若需被子類重寫,父類必需是虛方法或虛屬性virtual) 

class ProxyRealA : RealA
 {
  
     public override string Pro
     {
         get
         {
             return base.Pro;
         }
         set
         {
             ShowLog("設置Pro屬性前日志信息");
             base.Pro = value;
             ShowLog($"設置Pro屬性后日志信息:{value}");
         }
     }
  
     public override void ShowHello(string name)
     {
         try
         {
             ShowLog("ShowHello執行前日志信息");
             base.ShowHello(name);
             ShowLog("ShowHello執行后日志信息");
         }
         catch(Exception ex)
         {
             ShowLog($"ShowHello執行出錯日志信息:{ex.Message}");
         }
     }
  
     private void ShowLog(string log)
     {
         Console.WriteLine($"{DateTime.Now.ToString()}-{log}");
     }
 }
  
  
//調用:
 var aa = new ProxyRealA();
 aa.Pro = "測試2";
 aa.ShowHello("zuowenjun.cn");                

這段代碼同樣很簡單,就是ProxyRealA繼承自RealA類,即可看成是ProxyRealA代理RealA,由ProxyRealA提供各種屬性及方法調用。

這樣在ProxyRealA類內部屬性及方法執行前后都有統一記錄日志的代碼,不論在哪里用這個RealA類,都可以直接用ProxyRealA類代替,因為里氏替換原則,父類可以被子類替換,而且后續若想更改日志記錄代碼方式,只需要在ProxyRealA中更改就行了,這樣所有用到的ProxyRealA類的日志都會改變,是不是很爽。  

以上通過定義代理類的方式能夠實現在方法中統一進行各種執行點的攔截代碼邏輯處理

攔截點(或者稱為:橫切面,切面點)一般主要為:執行前,執行后,發生錯誤

雖然解決了之前直接調用真實類RealA時,需要重復增加各種邏輯代碼的問題,

但隨之而來的新問題又來了,那就是當一個系統中的類非常多的時候,如果我們針對每個類都定義一個代理類,那么系統的類的個數會成倍增加,而且不同的代理類中可能某些攔截業務邏輯代碼都是相同的,這種情況同樣是不能允許的,那有沒有什么好的辦法呢?

答案是肯定的,以下是我結合網上資源及個人總結的如下幾種常見的實現AOP的方式,各位可以參考學習。

 

第一種:靜態織入

即:在編譯時,就將各種涉及AOP攔截的代碼注入到符合一定規則的類中,編譯后的代碼與我們直接在RealA調用屬性或方法前后增加代碼是相同的,只是這個工作交由編譯器來完成。

 

PostSharp:PostSharp的Aspect是使用Attribute實現的,

我們只需事先通過繼承自OnMethodBoundaryAspect,然后重寫幾個常見的方法即可,如:OnEntry,OnExit等,最后只需要在需要進行AOP攔截的屬性或方法上加上AOP攔截特性類即可。

由於PostSharp是靜態織入的,所以相比其它的通過反射或EMIT反射來說效率是最高的,但PostSharp是收費版本的,而且網上的教程比較多,我就不在此重復說明了。

 

第二種:EMIT反射

即:通過Emit反射動態生成代理類

如下Castle.DynamicProxy的AOP實現方式,代碼也還是比較簡單的,效率相對第一種要慢一點,但對於普通的反射來說又高一些,代碼實現如下:

class Program
 {
     static void Main(string[] args)
     {
         ProxyGenerator generator = new ProxyGenerator();
         var test = generator.CreateClassProxy<TestA>(new TestInterceptor());
         Console.WriteLine($"GetResult:{test.GetResult(Console.ReadLine())}");
         test.GetResult2("test");
         Console.ReadKey();
     }
 }
  




 public class TestInterceptor : StandardInterceptor
 {
     private static NLog.Logger logger;
  
     protected override void PreProceed(IInvocation invocation)
     {
         Console.WriteLine(invocation.Method.Name + "執行前,入參:" + string.Join(",", invocation.Arguments));
     }
  
     protected override void PerformProceed(IInvocation invocation)
     {
         Console.WriteLine(invocation.Method.Name + "執行中");
         try
         {
             base.PerformProceed(invocation);
         }
         catch (Exception ex)
         {
             HandleException(ex);
         }
     }
  
     protected override void PostProceed(IInvocation invocation)
     {
         Console.WriteLine(invocation.Method.Name + "執行后,返回值:" + invocation.ReturnValue);
     }
  
     private void HandleException(Exception ex)
     {
         if (logger == null)
         {
             LoggingConfiguration config = new LoggingConfiguration();
  
             ColoredConsoleTarget consoleTarget = new ColoredConsoleTarget();
             consoleTarget.Layout = "${date:format=HH\\:MM\\:ss} ${logger} ${message}";
             config.AddTarget("console", consoleTarget);
  
	     LoggingRule rule1 = new LoggingRule("*", LogLevel.Debug, consoleTarget);
             config.LoggingRules.Add(rule1);
             LogManager.Configuration = config;
  
             logger = LogManager.GetCurrentClassLogger(); //new NLog.LogFactory().GetCurrentClassLogger();
         }
         logger.ErrorException("error",ex);
      }
 }
  



 public class TestA
 {
     public virtual string GetResult(string msg)
     {
         string str = $"{DateTime.Now.ToString("yyyy-mm-dd HH:mm:ss")}---{msg}";
         return str;
     }
  
     public virtual string GetResult2(string msg)
     {
         throw new Exception("throw Exception!");
     }
 }

簡要說明一下代碼原理:

  先創建ProxyGenerator類實例,從名字就看得出來,是代理類生成器

  然后實例化一個基於繼承自StandardInterceptor的TestInterceptor,這個TestInterceptor是一個自定義的攔截器

  最后通過generator.CreateClassProxy<TestA>(new TestInterceptor())動態創建了一個繼承自TestA的動態代理類,這個代理類只有在運行時才會生成的,

  后面就可以如代碼所示,直接用動態代理類對象實例Test操作TestA的所有屬性與方法

當然這里需要注意,若需要被動態代理類所代理並攔截,則父類的屬性或方法必需是virtual,這點與我上面說的直接寫一個代理類相同。  

上述代碼運行效果如下:

 

 

第三種:普通反射+利用Remoting的遠程訪問對象時的直實代理類來實現

代碼如下,這個可能相比以上兩種稍微復雜一點:

以上代碼實現步驟說明:

  1.這里定義的一個真實類AopClass必需繼承自ContextBoundObject類,而ContextBoundObject類又直接繼承自MarshalByRefObject類,表明該類是上下文綁定對象允許在支持遠程處理的應用程序中跨應用程序域邊界訪問對象

    說白了就是可以獲取這個真實類的所有信息,以便可以被生成動態代理。

  2.定義繼承自ProxyAttribute的代理特性標識類AopAttribute,以表明哪些類可以被代理,同時注意重寫CreateInstance方法,  

    在CreateInstance方法里實現通過委托與生成透明代理類的過程,realProxy.GetTransparentProxy() 非常重要,

    目的就是根據定義的AopProxy代理類獲取生成透明代理類對象實例。

  3.實現通用的AopProxy代理類,代理類必需繼承自RealProxy類,在這個代理類里面重寫Invoke方法,該方法是統一執行被代理的真實類的所有方法、屬性、字段的出入口,

    我們只需要在該方法中根據傳入的IMessage進行判斷並實現相應的攔截代碼即可。

  4.最后在需要進行Aop攔截的類上標注AopAttribute即可(注意:被標識的類必需是如第1條說明的繼承自ContextBoundObject類),在實際調用的過程中是感知不到任何的變化。

    且AopAttribute可以被子類繼承,也就意味着所有子類都可以被代理並攔截。

如上代碼運行效果如下:

這里順便分享微軟官方如果利用RealProxy類實現AOP的,詳見地址:https://msdn.microsoft.com/zh-cn/library/dn574804.aspx

 

第四種:反射+ 通過定義統一的出入口,並運用一些特性實現AOP的效果

比如:常見的MVC、WEB API中的過濾器特性 ,

我這里根據MVC的思路,實現了類似的MVC過濾器的AOP效果,只是中間用到了反射,可能性能不佳,但效果還是成功實現了各種攔截,

正如MVC一樣,既支持過濾器特性,也支持Controller中的Action執行前,執行后,錯誤等方法實現攔截

實現思路如下:

  A.過濾器及Controller特定方法攔截實現原理:

    1.獲取程序集中所有繼承自Controller的類型;

    2.根據Controller的名稱找到第1步中的對應的Controller的類型:FindControllerType

    3.根據找到的Controller類型及Action的名稱找到對應的方法:FindAction

    4.創建Controller類型的實例;

    5.根據Action方法找到定義在方法上的所有過濾器特性(包含:執行前、執行后、錯誤)

    6.執行Controller中的OnActionExecuting方法,隨后執行執行前的過濾器特性列表,如:ActionExecutingFilter

    7.執行Action方法,獲得結果;

    8.執行Controller中的OnActionExecuted方法,隨后執行執行后的過濾器特性列表,如:ActionExecutedFilter

    9.通過try catch在catch中執行Controller中的OnActionError方法,隨后執行錯誤過濾器特性列表,如:ActionErrorFilter

    10.最后返回結果;

  B.實現執行路由配置效果原理:

    1.增加可設置路由模板列表方法:AddExecRouteTemplate,在方法中驗證controller、action,並獲取模板中的占位符數組,最后保存到類全局對象中routeTemplates;

    2.增加根據執行路由執行對應的Controller中的Action方法的效果:Run,在該方法中主要遍歷所有路由模板,然后與實行執行的請求路由信息通過正則匹配,若匹配OK,並能正確找到Controller及Action,則說明正確,並最終統一調用:Process方法,執行A中的所有步驟最終返回結果。

    需要說明該模擬MVC方案並沒有實現Action方法參數的的綁定功能,因為ModelBinding本身就是比較復雜的機制,所以這里只是為了搞清楚AOP的實現原理,故不作這方面的研究,大家如果有空可以實現,

    最終實現MVC不僅是ASP.NET MVC,還可以是Console MVC,甚至是Winform MVC等。

 

以下是實現的全部代碼,代碼中我已進行了一些基本的優化,可以直接使用:

public abstract class Controller
{
 	public virtual void OnActionExecuting(MethodInfo action)
 	{
  
 	}
  
 	public virtual void OnActionExecuted(MethodInfo action)
 	{
  
 	}
  
 	public virtual void OnActionError(MethodInfo action, Exception ex)
 	{
  
 	}
  
}



  
public abstract class FilterAttribute : Attribute
{
 	public abstract string FilterType { get; }
 	public abstract void Execute(Controller ctrller, object extData);
}
  


public class ActionExecutingFilter : FilterAttribute
{
 	public override string FilterType => "BEFORE";
  
 	public override void Execute(Controller ctrller, object extData)
 	{
 		Console.WriteLine($"我是在{ctrller.GetType().Name}.ActionExecutingFilter中攔截發出的消息!-{DateTime.Now.ToString()}");
 	}
}
  


public class ActionExecutedFilter : FilterAttribute
{
 	public override string FilterType => "AFTER";
  
 	public override void Execute(Controller ctrller, object extData)
 	{
 		Console.WriteLine($"我是在{ctrller.GetType().Name}.ActionExecutedFilter中攔截發出的消息!-{DateTime.Now.ToString()}");
 	}
}
  


public class ActionErrorFilter : FilterAttribute
{
 	public override string FilterType => "EXCEPTION";
  
 	public override void Execute(Controller ctrller, object extData)
 	{
 		Console.WriteLine($"我是在{ctrller.GetType().Name}.ActionErrorFilter中攔截發出的消息!-{DateTime.Now.ToString()}-Error Msg:{(extData as Exception).Message}");
 	}
}
  


public class AppContext
{
 	private static readonly Type ControllerType = typeof(Controller);
 	private static readonly Dictionary<string, Type> matchedControllerTypes = new Dictionary<string, Type>();
 	private static readonly Dictionary<string, MethodInfo> matchedControllerActions = new Dictionary<string, MethodInfo>();
 	private Dictionary<string,string[]> routeTemplates = new Dictionary<string, string[]>();
  
  
 	public void AddExecRouteTemplate(string execRouteTemplate)
 	{
 		if (!Regex.IsMatch(execRouteTemplate, "{controller}", RegexOptions.IgnoreCase))
 		{
  			throw new ArgumentException("執行路由模板不正確,缺少{controller}");
 		}
  
 		if (!Regex.IsMatch(execRouteTemplate, "{action}", RegexOptions.IgnoreCase))
 		{
  			throw new ArgumentException("執行路由模板不正確,缺少{action}");
 		}
  
 		string[] keys = Regex.Matches(execRouteTemplate, @"(?<={)\w+(?=})", RegexOptions.IgnoreCase).Cast<Match>().Select(c => c.Value.ToLower()).ToArray();
  
 		routeTemplates.Add(execRouteTemplate,keys);
 	}
  
 	public object Run(string execRoute)
 	{
 		//{controller}/{action}/{id}
 		string ctrller = null;
 		string actionName = null;
 		ArrayList args = null;
 		Type controllerType = null;
 		bool findResult = false;
  
 		foreach (var r in routeTemplates)
 		{
  			string[] keys = r.Value;
  			string execRoutePattern = Regex.Replace(r.Key, @"{(?<key>\w+)}", (m) => string.Format(@"(?<{0}>.[^/\\]+)", m.Groups["key"].Value.ToLower()), RegexOptions.IgnoreCase);
  
  			args = new ArrayList();
  			if (Regex.IsMatch(execRoute, execRoutePattern))
  			{
  				var match = Regex.Match(execRoute, execRoutePattern);
  				for (int i = 0; i < keys.Length; i++)
  				{
   					if ("controller".Equals(keys[i], StringComparison.OrdinalIgnoreCase))
   					{
   						ctrller = match.Groups["controller"].Value;
   					}
   					else if ("action".Equals(keys[i], StringComparison.OrdinalIgnoreCase))
   					{
   						actionName = match.Groups["action"].Value;
   					}
   					else
   					{
   						args.Add(match.Groups[keys[i]].Value);
   					}
  				}
  
  				if ((controllerType = FindControllerType(ctrller)) != null && FindAction(controllerType, actionName, args.ToArray()) != null)
  				{
   					findResult = true;
   					break;
  				}
  			}
 		}
  
 		if (findResult)
 		{
  			return Process(ctrller, actionName, args.ToArray());
 		}
 		else
 		{
  			throw new Exception($"在已配置的路由模板列表中未找到與該執行路由相匹配的路由信息:{execRoute}");
 		}
 	}
  
 	public object Process(string ctrller, string actionName, params object[] args)
 	{
 		Type matchedControllerType = FindControllerType(ctrller);
  
 		if (matchedControllerType == null)
 		{
  			throw new ArgumentException($"未找到類型為{ctrller}的Controller類型");
 		}
  
 		object execResult = null;
 		if (matchedControllerType != null)
 		{
  			var matchedController = (Controller)Activator.CreateInstance(matchedControllerType);
  			MethodInfo action = FindAction(matchedControllerType, actionName, args);
  			if (action == null)
  			{
  				throw new ArgumentException($"在{matchedControllerType.FullName}中未找到與方法名:{actionName}及參數個數:{args.Count()}相匹配的方法");
  			}
  
  
  			var filters = action.GetCustomAttributes<FilterAttribute>(true);
  			List<FilterAttribute> execBeforeFilters = new List<FilterAttribute>();
  			List<FilterAttribute> execAfterFilters = new List<FilterAttribute>();
  			List<FilterAttribute> exceptionFilters = new List<FilterAttribute>();
  
  			if (filters != null && filters.Count() > 0)
  			{
  				execBeforeFilters = filters.Where(f => f.FilterType == "BEFORE").ToList();
  				execAfterFilters = filters.Where(f => f.FilterType == "AFTER").ToList();
  				exceptionFilters = filters.Where(f => f.FilterType == "EXCEPTION").ToList();
  			}
  
  			try
  			{
  				matchedController.OnActionExecuting(action);
  
  				if (execBeforeFilters != null && execBeforeFilters.Count > 0)
  				{
   					execBeforeFilters.ForEach(f => f.Execute(matchedController, null));
  				}
  
  				var mParams = action.GetParameters();
  				object[] newArgs = new object[args.Length];
  				for (int i = 0; i < mParams.Length; i++)
  				{
   					newArgs[i] = Convert.ChangeType(args[i], mParams[i].ParameterType);
  				}
  
  				execResult = action.Invoke(matchedController, newArgs);
  
  				matchedController.OnActionExecuted(action);
  
  				if (execBeforeFilters != null && execBeforeFilters.Count > 0)
  				{
   					execAfterFilters.ForEach(f => f.Execute(matchedController, null));
  				}
  
  			}
  			catch (Exception ex)
  			{
  				matchedController.OnActionError(action, ex);
  
  				if (exceptionFilters != null && exceptionFilters.Count > 0)
  				{
   					exceptionFilters.ForEach(f => f.Execute(matchedController, ex));
  				}
 			 }
  
  
 		}
  
 		return execResult;
  
 	}
  

 	private Type FindControllerType(string ctrller)
 	{
 		Type matchedControllerType = null;
 		if (!matchedControllerTypes.ContainsKey(ctrller))
 		{
  			var assy = Assembly.GetAssembly(typeof(Controller));
  
  			foreach (var m in assy.GetModules(false))
  			{
  				foreach (var t in m.GetTypes())
  				{
   					if (ControllerType.IsAssignableFrom(t) && !t.IsAbstract)
   					{
   						if (t.Name.Equals(ctrller, StringComparison.OrdinalIgnoreCase) || t.Name.Equals($"{ctrller}Controller", StringComparison.OrdinalIgnoreCase))
   						{
    							matchedControllerType = t;
    							matchedControllerTypes[ctrller] = matchedControllerType;
    							break;
   						}
   					}
  				}
  			}
 		}
 		else
 		{
  			matchedControllerType = matchedControllerTypes[ctrller];
 		}
  
 		return matchedControllerType;
 	}


  
 	private MethodInfo FindAction(Type matchedControllerType, string actionName, object[] args)
 	{
 		string ctrlerWithActionKey = $"{matchedControllerType.FullName}.{actionName}";
 		MethodInfo action = null;
 		if (!matchedControllerActions.ContainsKey(ctrlerWithActionKey))
 		{
  			if (args == null) args = new object[0];
  			foreach (var m in matchedControllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public))
  			{
  				if (m.Name.Equals(actionName, StringComparison.OrdinalIgnoreCase) && m.GetParameters().Length == args.Length)
  				{
   					action = m;
   					matchedControllerActions[ctrlerWithActionKey] = action;
   					break;
  				}
  			}
 		}
 		else
 		{
  			action = matchedControllerActions[ctrlerWithActionKey];
 		}
  
 			return action;
 	}
}

使用前,先定義一個繼承自Controller的類,

如:TestController,並重寫相應的方法,或在指定的方法上加上所需的過濾器特性,如下代碼所示:

public class TestController : Controller
{
 public override void OnActionExecuting(MethodInfo action)
 {
 Console.WriteLine($"{action.Name}執行前,OnActionExecuting---{DateTime.Now.ToString()}");
 }
  
 public override void OnActionExecuted(MethodInfo action)
 {
 Console.WriteLine($"{action.Name}執行后,OnActionExecuted--{DateTime.Now.ToString()}");
 }
  
 public override void OnActionError(MethodInfo action, Exception ex)
 {
 	Console.WriteLine($"{action.Name}執行,OnActionError--{DateTime.Now.ToString()}:{ex.Message}");
 }
  
 [ActionExecutingFilter]
 [ActionExecutedFilter]
 public string HelloWorld(string name)
 {
 	return ($"Hello World!->{name}");
 }
  
 [ActionExecutingFilter]
 [ActionExecutedFilter]
 [ActionErrorFilter]
 public string TestError(string name)
 {
 	throw new Exception("這是測試拋出的錯誤信息!");
 }
  
 [ActionExecutingFilter]
 [ActionExecutedFilter]
 public int Add(int a, int b)
 {
 	return a + b;
 }
}

最后前端實際調用就非常簡單了,代碼如下:

class MVCProgram
{
 static void Main(string[] args)
 {
 try
 {
  var appContext = new AppContext();
  object rs = appContext.Process("Test", "HelloWorld", "夢在旅途");
  Console.WriteLine($"Process執行的結果1:{rs}");
  
  Console.WriteLine("=".PadRight(50, '='));
  
  appContext.AddExecRouteTemplate("{controller}/{action}/{name}");
  appContext.AddExecRouteTemplate("{action}/{controller}/{name}");
  
  object result1 = appContext.Run("HelloWorld/Test/夢在旅途-zuowenjun.cn");
  Console.WriteLine($"執行的結果1:{result1}");
  
  Console.WriteLine("=".PadRight(50, '='));
  
  object result2 = appContext.Run("Test/HelloWorld/夢在旅途-zuowenjun.cn");
  Console.WriteLine($"執行的結果2:{result2}");
  
  Console.WriteLine("=".PadRight(50, '='));
  
  appContext.AddExecRouteTemplate("{action}/{controller}/{a}/{b}");
  object result3 = appContext.Run("Add/Test/500/20");
  Console.WriteLine($"執行的結果3:{result3}");
  
  object result4 = appContext.Run("Test/TestError/夢在旅途-zuowenjun.cn");
  Console.WriteLine($"執行的結果4:{result4}");
 }
 catch (Exception ex)
 {
  Console.ForegroundColor = ConsoleColor.Red;
  Console.WriteLine($"發生錯誤:{ex.Message}");
  Console.ResetColor();
 }
  
 Console.ReadKey();
 }
}

可以看到,與ASP.NET MVC有點類似,只是ASP.NET MVC是通過URL訪問,而這里是通過AppContext.Run 執行路由URL 或Process方法,直接指定Controller、Action、參數來執行

通過以上調用代碼可以看出路由配置還是比較靈活的,當然參數配置除外。如果大家有更好的想法也可以在下方評論交流,謝謝!

MVC代碼執行效果如下:  

 

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM