1. 程式人生 > >.NET Core中實現AOP程式設計

.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的方式注入切面

注入計算執行時間的邏輯

在已有的方法上注入一段邏輯可以分為三種情況:

  1. 在方法執行前注入一段邏輯,例如注入統一的認證邏輯
  2. 在方法執行後注入一段邏輯,例如將結果寫入日誌
  3. 方法前後同時注入邏輯,例如計算時間,又或者給整個方法內容包裹一個事務
    已知一個計算個數的方法如下:
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可以讓你的業務邏輯程式碼不會過度臃腫,也是除了繼承之外另一種可複用程式碼的方式。