.NET Core中實現AOP程式設計
AOP全稱Aspect Oriented Progarmming(面向切面程式設計),其實AOP對ASP.NET程式設計師來說一點都不神祕,你也許早就通過Filter來完成一些通用的功能,例如你使用Authorization Filter來攔截所有的使用者請求,驗證Http Header中是否有合法的token。或者使用Exception Filter來處理某種特定的異常。
你之所以可以攔截所有的使用者請求,能夠在期望的時機來執行某些通用的行為,是因為ASP.NET Core在框架級別預留了一些鉤子,他允許你在特定的時機注入一些行為。對ASP.NET Core應用程式來說,這個時機就是HTTP請求在執行MVC Action的中介軟體時。
顯然這個時機並不能滿足你的所有求,比如你在Repository層有一個讀取資料庫的方法:
public void GetUser()
{
//Get user from db
}
你試圖得到該方法執行的時間,首先想到的方式就是在整個方法外面包一層用來計算時間的程式碼:
public void GetUserWithTime() { var stopwatch = Stopwatch.StartNew(); try { //Get user from db } finally { stopwatch.Stop(); Trace.WriteLine("Total" + stopwatch.ElapsedMilliseconds + "ms"); } }
如果僅僅是為了得到這一個方法的執行時間,這種方式可以滿足你的需求。問題在於你有可能還想得到DeleteUser
或者UpdateUser
等方法的執行時間。修改每一個方法並新增計算時間的程式碼存在著明顯的code smell。
一個比較優雅的做法是給需要計算時間的方法標記一個Attribute:
[Time]
public void GetUser()
{
//Get user from db
}
你把計算時間這個功能當做一個切面(Aspect)注入到了現有的邏輯中,這是一個AOP的典型應用。
在C#中使用AOP
C#中可以用來做AOP的開源類庫有若干個,比較流行的:
這些類庫之所以能夠實現AOP是因為他們有動態修改IL程式碼的能力,這種能力又被稱為IL weaving。
還有的類庫把AOP和Dependency Injection結合在了一起,通過服務上註冊一個攔截器(Interceptor)的方式做達到AOP的目的,例如:
本文將使用一個C#開源專案aspect-injector來描述AOP的幾種常見的場景。
aspect-injector是一個非常輕量級的AOP類庫,麻雀雖小,但是已經能夠應對大部分AOP的應用場景:
- 支援.NET Core
- 支援對非同步方法注入切面
- 能夠把切面注入到方法、屬性和事件上
- 支援Attribute的方式注入切面
注入計算執行時間的邏輯
在已有的方法上注入一段邏輯可以分為三種情況:
- 在方法執行前注入一段邏輯,例如注入統一的認證邏輯
- 在方法執行後注入一段邏輯,例如將結果寫入日誌
- 方法前後同時注入邏輯,例如計算時間,又或者給整個方法內容包裹一個事務
已知一個計算個數的方法如下:
public class SampleService
{
public int GetCount()
{
Thread.Sleep(3000);
return 10;
}
}
為了將計算時間的邏輯包裹在現有的方法上,我們需要在被注入邏輯的方法上標記InjectAttribute
:
public class SampleService
{
[Inject(typeof(TimeAspect))]
public int GetCount()
{
Thread.Sleep(3000);
return 10;
}
}
TimeAspect
就是我們將要注入的一個切面:
[Aspect(Aspect.Scope.Global)]
public class TimeAspect
{
[Advice(Advice.Type.Around, Advice.Target.Method)]
public object HandleMethod(
[Advice.Argument(Advice.Argument.Source.Name)] string name,
[Advice.Argument(Advice.Argument.Source.Arguments)] object[] arguments,
[Advice.Argument(Advice.Argument.Source.Target)]
Func<object[], object> method)
{
Console.WriteLine($"Executing method {name}");
var sw = Stopwatch.StartNew();
var result = method(arguments); //呼叫被注入切面的方法
sw.Stop();
Console.WriteLine($"method {name} in {sw.ElapsedMilliseconds} ms");
return result;
}
}
大部分程式碼是非常清晰的,我們只描述幾個重要的概念:
標記了AdviceAttribute
的方法就是即將要注入到目標方法的切面邏輯,也就是說HandleMethod
描述瞭如何計算時間。
Advice.Type.Around
描述了同時在目標方法的前後都注入邏輯
方法引數Func<object[], object> method
其實就代表目標方法
注入認證邏輯
試想你有如果幹個服務,每個服務在執行前都要做安全認證,顯然安全認證的邏輯是可重用的,那我們就可以把認證的邏輯提取成一個切面(Aspect)。
[Inject(typeof(AuthorizationAspect))]
public class SampleService
{
public void MethodA(Guid userId)
{
// Do something
}
public void MethodB(Guid userId)
{
// Do something
}
}
AuthorizationAspect
就是安全認證的邏輯:
[Aspect(Aspect.Scope.Global)]
public class AuthorizationAspect
{
[Advice(Advice.Type.Before, Advice.Target.Method)]
public void CheckAccess(
[Advice.Argument(Advice.Argument.Source.Method)] MethodInfo method,
[Advice.Argument(Advice.Argument.Source.Arguments)] object[] arguments)
{
if (arguments.Length == 0 || !(arguments[0] is Guid))
{
throw new ArgumentException($"{nameof(AuthorizationAspect)} expects
every target method to have Guid as the first parameter");
}
var userId = (Guid)arguments[0];
if (!_securityService.HasPermission(userId, authorizationAttr.Permission))
{
throw new Exception($"User {userId} doesn't have
permission to execute method {method.Name}");
}
}
}
Advice.Type.Before
描述了該邏輯會在被修改的方法前執行
通過object[] arguments
得到了被修改方法的所有引數
AOP是面向物件程式設計中一種用來抽取公用邏輯,簡化業務程式碼的方式,靈活使用AOP可以讓你的業務邏輯程式碼不會過度臃腫,也是除了繼承之外另一種可複用程式碼的方式。