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是收費版本的,而且網上的教程比較多,我就不在此重複說明了,大家可以參見:使用PostSharp在.NET平臺上實現AOP
第二種:EMIT反射,即:通過Emit反射動態生成代理類,如下Castle.DynamicProxy的AOP實現方式,程式碼也還是比較簡單的,效率相對第一種要慢一點,但對於普通的反射來說又高一些,程式碼實現如下:
using Castle.Core.Interceptor;
using Castle.DynamicProxy;
using NLog;
using NLog.Config;
using NLog.Win32.Targets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp
{
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的遠端訪問物件時的直實代理類來實現,程式碼如下,這個可能相比以上兩種稍微複雜一點:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Activation;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
var A = new AopClass();
A.Hello();
var aop = new AopClassSub("夢在旅途");
aop.Pro = "test";
aop.Output("hlf");
aop.ShowMsg();
Console.ReadKey();
}
}
[AopAttribute]
public class AopClass : ContextBoundObject
{
public string Hello()
{
return "welcome";
}
}
public class AopClassSub : AopClass
{
public string Pro = null;
private string Msg = null;
public AopClassSub(string msg)
{
Msg = msg;
}
public void Output(string name)
{
Console.WriteLine(name + ",你好!-->P:" + Pro);
}
public void ShowMsg()
{
Console.WriteLine($"建構函式傳的Msg引數內容是:{Msg}");
}
}
public class AopAttribute : ProxyAttribute
{
public override MarshalByRefObject CreateInstance(Type serverType)
{
AopProxy realProxy = new AopProxy(serverType);
return realProxy.GetTransparentProxy() as MarshalByRefObject;
}
}
public class AopProxy : RealProxy
{
public AopProxy(Type serverType)
: base(serverType) { }
public override IMessage Invoke(IMessage msg)
{
if (msg is IConstructionCallMessage)
{
IConstructionCallMessage constructCallMsg = msg as IConstructionCallMessage;
IConstructionReturnMessage constructionReturnMessage = this.InitializeServerObject((IConstructionCallMessage)msg);
RealProxy.SetStubData(this, constructionReturnMessage.ReturnValue);
Console.WriteLine("Call constructor");
return constructionReturnMessage;
}
else
{
IMethodCallMessage callMsg = msg as IMethodCallMessage;
IMessage message;
try
{
Console.WriteLine(callMsg.MethodName + "執行前。。。");
object[] args = callMsg.Args;
object o = callMsg.MethodBase.Invoke(GetUnwrappedServer(), args);
Console.WriteLine(callMsg.MethodName + "執行後。。。");
message = new ReturnMessage(o, args, args.Length, callMsg.LogicalCallContext, callMsg);
}
catch (Exception e)
{
message = new ReturnMessage(e, callMsg);
}
Console.WriteLine(message.Properties["__Return"]);
return message;
}
}
}
}
以上程式碼實現步驟說明:
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程式碼執行效果如下: