AOP 有幾種實現方式?

victor.x.qu發表於2020-12-15

1. 回顧 AOP 是什麼?

維基百科解釋如下:

面向切面的程式設計(Aspect-oriented programming,AOP,又譯作面向方面的程式設計、剖面導向程式設計)是電腦科學中的一種程式設計思想,旨在將橫切關注點與業務主體進行進一步分離,以提高程式程式碼的模組化程度。通過在現有程式碼基礎上增加額外的通知(Advice)機制,能夠對被宣告為“切點(Pointcut)”的程式碼塊進行統一管理與裝飾,如“對所有方法名以‘set*’開頭的方法新增後臺日誌”。該思想使得開發人員能夠將與程式碼核心業務邏輯關係不那麼密切的功能(如日誌功能)新增至程式中,同時又不降低業務程式碼的可讀性。面向切面的程式設計思想也是面向切面軟體開發的基礎。

面向切面的程式設計將程式碼邏輯切分為不同的模組(即關注點(Concern),一段特定的邏輯功能)。幾乎所有的程式設計思想都涉及程式碼功能的分類,將各個關注點封裝成獨立的抽象模組(如函式、過程、模組、類以及方法等),後者又可供進一步實現、封裝和重寫。部分關注點“橫切”程式程式碼中的數個模組,即在多個模組中都有出現,它們即被稱作“橫切關注點(Cross-cutting concerns, Horizontal concerns)”。

日誌功能即是橫切關注點的一個典型案例,因為日誌功能往往橫跨系統中的每個業務模組,即“橫切”所有有日誌需求的類及方法體。而對於一個信用卡應用程式來說,存款、取款、帳單管理是它的核心關注點,日誌和持久化將成為橫切整個物件結構的橫切關注點。

參見: https://zh.wikipedia.org/wiki/面向切面的程式設計

簡單來說,就是功能上我們要加其他感覺和原本功能無關的邏輯,比如效能日誌,程式碼混在一起,看著不爽,影響我們理解。

舉個例子, 如下程式碼我們要多花幾眼時間才能看明白:

 public int doAMethod(int n)
 {
   int sum = 0;
   for (int i = 1; i <= n; i++)
   {
     if (n % i == 0)
     {
       sum += 1;
     }
   }
   if (sum == 2)
   {
     return sum;
   }
   else
   {
     return -1;
   }
 }

然後我們需要記錄一系列日誌,就會變成這樣子:

 public int doAMethod(int n,Logger logger, HttpContext c, .....)
 {
   log.LogInfo($" n is {n}.");
   log.LogInfo($" who call {c.RequestUrl}.");
   log.LogInfo($" QueryString {c.QueryString}.");
   log.LogInfo($" Ip {c.Ip}.");
   log.LogInfo($" start {Datetime.Now}.");
   int sum = 0;
   for (int i = 1; i <= n; i++)
   {
     if (n % i == 0)
     {
       sum += 1;
     }
   }
   if (sum == 2)
   {
     return sum;
   }
   else
   {
     return -1;
   }
   log.LogInfo($" end {Datetime.Now}.");
 }

一下子這個方法就複雜多了,至少呼叫它還得找一堆貌似和方法無關的引數

AOP 的想法就是把上述方法拆分開, 讓log之類的方法不在我們眼中:

 public int doAMethod(int n)
 {
   int sum = 0;
   for (int i = 1; i <= n; i++)
   {
     if (n % i == 0)
     {
       sum += 1;
     }
   }
   if (sum == 2)
   {
     return sum;
   }
   else
   {
     return -1;
   }
 }

AOP 讓看著只呼叫的 doAMethod 方法實際為:

 public int doAMethodWithAOP(int n,Logger logger, HttpContext c, .....)
 {
   log.LogInfo($" n is {n}.");
   log.LogInfo($" who call {c.RequestUrl}.");
   log.LogInfo($" QueryString {c.QueryString}.");
   log.LogInfo($" Ip {c.Ip}.");
   log.LogInfo($" start {Datetime.Now}.");
   return doAMethod(n);
   log.LogInfo($" end {Datetime.Now}.");
 }

所以AOP 實際就是幹這個事情,

無論語言,

無論實現,

其實只要幹這個事不就是AOP嗎?

2. 類似AOP想法的實現方式分類

達到AOP要做的這種事情有很多種方法,下面來做個簡單分類,不一定很全面哦

2.1 按照方式

2.1.1 超程式設計

很多語言都有內建類似這樣一些“增強程式碼”的功能,

一般來說,從安全性和編譯問題等角度考慮,大多數超程式設計都只允許新增程式碼,不允許修改。

這種都是編譯器必須有才能做到。(沒有的,你也可以自己寫個編譯器,只要你做的到)

當然超程式設計的概念不僅僅可以用來做類似AOP的事情,

還可以做各種你想做的事情,(只要在限制範圍內能做的)

以下的例子就是生成一些新的方法。

巨集

例如 Rust / C++ 等等都具有這樣的功能

例如 Rust 的文件:https://doc.rust-lang.org/stable/book/ch19-06-macros.html

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
    Pancakes::hello_macro();
}

巨集實現

extern crate proc_macro;
use crate::proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    let ast = syn::parse(input).unwrap();
    impl_hello_macro(&ast)
}

csharp 的 Source Generators

新的實驗特性,還在設計修改變化中

官方文件: https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md

public partial class ExampleViewModel
{
  [AutoNotify]
  private string _text = "private field text";
  [AutoNotify(PropertyName = "Count")]
  private int _amount = 5;
}

生成器實現

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Analyzer1
{
    [Generator]
    public class AutoNotifyGenerator : ISourceGenerator
    {
        private const string attributeText = @"
using System;
namespace AutoNotify
{
    [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
    sealed class AutoNotifyAttribute : Attribute
    {
        public AutoNotifyAttribute()
        {
        }
        public string PropertyName { get; set; }
    }
}
";
        public void Initialize(InitializationContext context)
        {
            // Register a syntax receiver that will be created for each generation pass
            context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
        }
        public void Execute(SourceGeneratorContext context)
        {
            // add the attribute text
            context.AddSource("AutoNotifyAttribute", SourceText.From(attributeText, Encoding.UTF8));
            // retreive the populated receiver 
            if (!(context.SyntaxReceiver is SyntaxReceiver receiver))
                return;
            // we're going to create a new compilation that contains the attribute.
            // TODO: we should allow source generators to provide source during initialize, so that this step isn't required.
            CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
            Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options));
            // get the newly bound attribute, and INotifyPropertyChanged
            INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute");
            INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");
            // loop over the candidate fields, and keep the ones that are actually annotated
            List<IFieldSymbol> fieldSymbols = new List<IFieldSymbol>();
            foreach (FieldDeclarationSyntax field in receiver.CandidateFields)
            {
                SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree);
                foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables)
                {
                    // Get the symbol being decleared by the field, and keep it if its annotated
                    IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
                    if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
                    {
                        fieldSymbols.Add(fieldSymbol);
                    }
                }
            }
            // group the fields by class, and generate the source
            foreach (IGrouping<INamedTypeSymbol, IFieldSymbol> group in fieldSymbols.GroupBy(f => f.ContainingType))
            {
                string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context);
               context.AddSource($"{group.Key.Name}_autoNotify.cs", SourceText.From(classSource, Encoding.UTF8));
            }
        }
        private string ProcessClass(INamedTypeSymbol classSymbol, List<IFieldSymbol> fields, ISymbol attributeSymbol, ISymbol notifySymbol, SourceGeneratorContext context)
        {
            if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
            {
                return null; //TODO: issue a diagnostic that it must be top level
            }
            string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
            // begin building the generated source
            StringBuilder source = new StringBuilder($@"
namespace {namespaceName}
{{
    public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()}
    {{
");
            // if the class doesn't implement INotifyPropertyChanged already, add it
            if (!classSymbol.Interfaces.Contains(notifySymbol))
            {
                source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
            }
            // create properties for each field 
            foreach (IFieldSymbol fieldSymbol in fields)
            {
                ProcessField(source, fieldSymbol, attributeSymbol);
            }
            source.Append("} }");
            return source.ToString();
        }
        private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol)
        {
            // get the name and type of the field
            string fieldName = fieldSymbol.Name;
            ITypeSymbol fieldType = fieldSymbol.Type;
            // get the AutoNotify attribute from the field, and any associated data
            AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
            TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value;
            string propertyName = chooseName(fieldName, overridenNameOpt);
            if (propertyName.Length == 0 || propertyName == fieldName)
            {
                //TODO: issue a diagnostic that we can't process this field
                return;
            }
            source.Append($@"
public {fieldType} {propertyName} 
{{
    get 
    {{
        return this.{fieldName};
    }}
    set
    {{
        this.{fieldName} = value;
        this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName})));
    }}
}}
");
            string chooseName(string fieldName, TypedConstant overridenNameOpt)
            {
                if (!overridenNameOpt.IsNull)
                {
                    return overridenNameOpt.Value.ToString();
                }
                fieldName = fieldName.TrimStart('_');
                if (fieldName.Length == 0)
                    return string.Empty;
                if (fieldName.Length == 1)
                    return fieldName.ToUpper();
                return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1);
            }
        }
        /// <summary>
        /// Created on demand before each generation pass
        /// </summary>
        class SyntaxReceiver : ISyntaxReceiver
        {
            public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();
            /// <summary>
            /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation
            /// </summary>
            public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
            {
                // any field with at least one attribute is a candidate for property generation
                if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax
                    && fieldDeclarationSyntax.AttributeLists.Count > 0)
                {
                    CandidateFields.Add(fieldDeclarationSyntax);
                }
            }
        }
    }
}

2.1.2 修改程式碼

程式碼檔案修改

一般來說,很少有這樣實現的,程式碼檔案都改了,我們碼農還怎麼寫bug呀。

中間語言修改

有很多語言編譯的結果並不是直接的機器碼,而是優化後的一個接近底層的中間層語言,方便擴充套件支援不同cpu,不同機器架構。

比如 dotnet 的 IL

.class private auto ansi '<Module>'
{
} // end of class <Module>
.class public auto ansi beforefieldinit C
    extends [mscorlib]System.Object
{
    // Fields
    .field private initonly int32 '<x>k__BackingField'
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 21 (0x15)
        .maxstack 8
        IL_0000: ldarg.0
        IL_0001: ldc.i4.5
        IL_0002: stfld int32 C::'<x>k__BackingField'
        IL_0007: ldarg.0
        IL_0008: call instance void [mscorlib]System.Object::.ctor()
        IL_000d: ldarg.0
        IL_000e: ldc.i4.4
        IL_000f: stfld int32 C::'<x>k__BackingField'
        IL_0014: ret
    } // end of method C::.ctor
    .method public hidebysig specialname 
        instance int32 get_x () cil managed 
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x2066
        // Code size 7 (0x7)
        .maxstack 8
        IL_0000: ldarg.0
        IL_0001: ldfld int32 C::'<x>k__BackingField'
        IL_0006: ret
    } // end of method C::get_x
    // Properties
    .property instance int32 x()
    {
        .get instance int32 C::get_x()
    }
} // end of class C

比如 java 的位元組碼 (反編譯的結果)

Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/Main.class
  Last modified 2018-4-7; size 362 bytes
  MD5 checksum 4aed8540b098992663b7ba08c65312de
  Compiled from "Main.java"
public class com.rhythm7.Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#19         // com/rhythm7/Main.m:I
   #3 = Class              #20            // com/rhythm7/Main
   #4 = Class              #21            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/rhythm7/Main;
  #14 = Utf8               inc
  #15 = Utf8               ()I
  #16 = Utf8               SourceFile
  #17 = Utf8               Main.java
  #18 = NameAndType        #7:#8          // "<init>":()V
  #19 = NameAndType        #5:#6          // m:I
  #20 = Utf8               com/rhythm7/Main
  #21 = Utf8               java/lang/Object
{
  private int m;
    descriptor: I
    flags: ACC_PRIVATE
  public com.rhythm7.Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/rhythm7/Main;
  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/rhythm7/Main;
}
SourceFile: "Main.java"

它們也是程式語言的一種,也是可以寫的,所以我們可以用來把別人方法體改了。

當然怎麼改,怎麼改得各種各樣方法都相容,做的人簡直?

生成代理程式碼

不修改原來的程式碼檔案,新增代理程式碼實現

不修改編譯好的IL 或 位元組碼等,往裡面新增IL或位元組碼等形式代理程式碼

2.1.3 利用編譯器或者執行時的功能

一般來說,也是利用編譯器自身提供得擴充套件功能做擴充套件

java的 AspectJ 好像就可以利用了ajc編譯器做事情

2.1.4 利用執行時功能

理論上 dotnet 也可以實現CLR Profiling API 在JIT編譯時修改method body。實現真正無任何限制的執行時靜態AOP (不過貌似得用C++才能做CLR Profiling API,文件少,相容貌似也挺難做的)

2.2 按照編織時機

2.2.1 編譯前

比如

  • 修改掉別人的程式碼檔案(找死)
  • 生成新的程式碼,讓編譯器編譯進去,執行時想辦法用新的程式碼

2.2.2 編譯時

  • 超程式設計
  • 做個編譯器

2.2.3 編譯後靜態編織一次

根據編譯好的東西(dotnet的dll或者其他語言的東西)利用反射,解析等技術生成代理實現,然後塞進去

2.2.4 執行時

嚴格來說,執行時也是編譯後

不過不是再編織一次,而是每次執行都編織

並且沒有什麼 前中後了,

都是程式啟動後,在具體類執行之前,把這個類編織了

比如java 的 類載入器:在目標類被裝載到JVM時,通過一個特殊的類載入器,對目標類的位元組碼重新“增強。

具有aop功能的各類 IOC 容器在生成例項前建立代理例項

其實也可以在註冊IOC容器時替換為代理型別

3. 代理

這裡單獨再說一下代理是什麼,

畢竟很多AOP框架或者其他框架都有利用代理的思想,

為什麼都要這樣玩呢?

很簡單,代理就是幫你做相同事情,並且可以比你做的更多,還一點兒都不動到你原來的程式碼。

比如如下 真實的class 和代理class 看起來一模一樣

但兩者的真實的程式碼可能是這樣子的

RealClass:

public class RealClass
{
  public virtual int Add(int i, int j)
  {
    return i + j;
  }
}
ProxyClass:

public class ProxyClass : RealClass
{
    public override int Add(int i, int j)
    {
        int r = 0;
        i += 7;
        j -= 7;
        r = base.Add(i, j);
        r += 55;
        return r;
    }
}

所以我們呼叫的時候會是這樣

相關文章