aop 階段性概況

敖毛毛發表於2024-04-16

前言

對aop進行一個階段性的總結。

正文

首先什麼是aop呢?

那麼首先看aop的解決什麼樣的問題。

public class Program
{
    public static void Main(string[] args)
    {
        
    }

    public void ChangePosition1()
    {
        // your operation
        SavePosition();
    }

    public void ChangePosition2()
    {
        // your operation
        SavePosition();
    }

    public void SavePosition()
    {
    }
}

看上面這個位置:
ChangePosition1 和 ChangePosition2

他們做完一些操作之後,需要做相同的操作,如上面所述,需要儲存位置。

只有兩個方法呼叫,那麼不是啥子問題。

但是如果有大量的方法去呼叫這個東西,那麼問題就出現了。

第一:重複性的工作
第二:出錯率,每次都要寫一遍,可能出現忘記的情況
第三:看著不優雅

那麼在如此多的案例的情況下,人們都發現了規律。

比如說,在某個行為前做什麼,在某個行為後做什麼。

那麼我們可以進行擴充套件:

public void Dosomething()
{
  // your operaion
}

aspect logging
{
   befor(dosomething is called)
   {
      Log.Write("enter dosomething")
   }

   after(dosomething is called)
   {
      Log.Write("after dosomething")
   }
}

aspect verification()
{
    befor(dosomething is called)
    {
      // your verification
    }
}

比如我們的驗證和日誌,可以透過這些aop去處理。

aop 全稱是aspect-oriented programming 中文大多翻譯過來是面向切面程式設計。

oriented 是面向,比如日誌、驗證,這些是aspect。

所以取了asepct-oriented programming 這個名字。

好的,那麼現在瞭解了aop是什麼東西,也瞭解了他的由來。

下面瞭解一下aop濫用的情況。

什麼情況aop會濫用呢?

比如說:

public void setsomething()
{
}

aspect shutup
{
  after(something is called)
  {
    // shutup
  }
}

這種aop 就是反模式,不被推薦的。

原因是,比如我執行了setsomething之後,我setsomething 的意思是設定某一些東西,而沒有shutup的含義。

但是最後卻執行了該操作,這樣難以維護和理解。

那麼為什麼logging 和 verification 能夠被人所接受呢?

因為其沒有破壞我們該程式碼的邏輯,的確setsomething做的事情就是它應該做的事情,不對執行邏輯造成影響。

深入一下aop是如何實現的呢?其實aop的原理很簡單,但是優雅實現的卻有一點點複雜。

  • 位元組碼操作

    • 優點:位元組碼操作可以在更細粒度的層面上操作程式碼,可以實現更靈活和精細的AOP功能。可以在編譯期或執行期動態修改位元組碼,對目標類進行修改。
    • 缺點:實現相對複雜,需要對位元組碼結構有一定的瞭解。可能會影響程式碼的可讀性和維護性。
  • 代理技術

    • 優點:代理技術相對簡單易懂,可以快速實現AOP功能。可以透過代理物件來實現橫切關注點的功能,不需要直接操作位元組碼。
    • 缺點:代理技術通常在執行時動態建立代理物件,可能會引入效能開銷。對於一些高效能要求的場景,可能不太適合。

下面對這個分別進行舉例:

先從好理解的代理開始:

public class LoggingAspect
{
    public void LogBefore(string methodName)
    {
        Console.WriteLine($"Logging before {methodName} execution");
    }
}

然後建立相應的代理:

public class UserServiceProxy<T> : DispatchProxy
{
    private T _decorated;
    
    private LoggingAspect _loggingAspect;
    
    protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
    {
        _loggingAspect.LogBefore(targetMethod.Name);
        
        return targetMethod.Invoke(_decorated, args);
    }
    
    public static T Create(T decorated)
    {
        object proxy = Create<T, UserServiceProxy<T>>();
        ((UserServiceProxy<T>)proxy)._decorated = decorated;
        ((UserServiceProxy<T>)proxy)._loggingAspect = new LoggingAspect();
        
        return (T)proxy;
    }
}

解釋一下,這個create建立了什麼,這個create 建立了兩個型別的繼承類。

也就是建立了動態型別。

public class UserService : IUserService
{
    public void Login()
    {
        Console.WriteLine("User logged in");
    }
}

現在是我們的程式碼實現了。

那麼看下是怎麼呼叫的。

public static void Main(string[] args)
{
    IUserService userService = new UserService();
    IUserService proxy = UserServiceProxy<IUserService>.Create(userService);
    proxy.Login();
}

這樣就呼叫完成了。

看下效果。

這樣就完成了相應的code。

至於為什麼我們在使用框架的時候進行屬性標記即可,那是因為框架幫我們把事情做了。

例如:

  1. 安裝PostSharp:首先需要安裝PostSharp NuGet包。

  2. 定義切面類

[Serializable]
public class LoggingAspect : OnMethodBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
        Console.WriteLine($"Logging before {args.Method.Name} execution");
    }
}
  1. 應用切面:在需要應用AOP的方法上新增切面標記。
public class UserService
{
    [LoggingAspect]
    public void Login()
    {
        Console.WriteLine("User logged in");
    }
}

至於這個框架是怎麼實現的,原理就是利用MSBuilder。

MSBuild工具可以透過自定義任務(Custom Tasks)來實現預處理操作。透過編寫自定義任務,可以在MSBuild構建過程中執行特定的預處理邏輯。以下是一個簡單的示例,演示如何在MSBuild中使用自定義任務進行預處理:

  1. 建立自定義任務
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

public class CustomPreprocessTask : Task
{
    public override bool Execute()
    {
        // 在這裡編寫預處理邏輯
        Log.LogMessage("Custom preprocessing task executed.");
        return true;
    }
}
  1. 在專案檔案中引用自定義任務

在專案檔案(.csproj)中新增以下內容,引用自定義任務:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <UsingTask TaskName="CustomPreprocessTask" AssemblyFile="Path\\To\\CustomTask.dll" />
  
  <Target Name="CustomPreprocessTarget" BeforeTargets="Build">
    <CustomPreprocessTask />
  </Target>
</Project>
  1. 執行預處理操作

在構建專案時,MSBuild會執行自定義任務中定義的預處理邏輯。可以在Execute方法中編寫任何需要的預處理程式碼,例如生成檔案、修改配置等操作。

透過編寫自定義任務並在專案檔案中引用,可以利用MSBuild進行預處理操作。這樣可以在構建過程中執行特定的邏輯,實現更靈活的構建流程。

代理模式,就是利用了msbuilder 預處理邏輯,在編譯前進行了預處理。

那麼位元組碼模式就是在msbuilder 編譯後進行預處理。

using Mono.Cecil;
using Mono.Cecil.Cil;

public class LoggingAspect
{
    public void LogBefore()
    {
        Console.WriteLine("Logging before method execution");
    }
}

public class Program
{
    public static void Main()
    {
        AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly("YourAssembly.dll");
        ModuleDefinition module = assembly.MainModule;

        TypeDefinition type = module.Types.Single(t => t.Name == "UserService");
        MethodDefinition method = type.Methods.Single(m => m.Name == "Login");

        ILProcessor processor = method.Body.GetILProcessor();
        Instruction firstInstruction = method.Body.Instructions.First();

        // 在方法開頭插入日誌記錄程式碼
        processor.InsertBefore(firstInstruction, processor.Create(OpCodes.Call, typeof(LoggingAspect).GetMethod("LogBefore")));

        assembly.Write("YourModifiedAssembly.dll");
    }
}

就是在我們程式編譯完成後,進行在相應的位置進行注入程式。

下一節oop,關於aop解決一些實際問題的例子後續補齊。

相關文章