AOP 有幾種實現方式?

1. 回顧 AOP 是什麼?

維基百科解釋以下:html

面向切面的程序設計(Aspect-oriented programming,AOP,又譯做面向方面的程序設計、剖面導向程序設計)是計算機科學中的一種程序設計思想,旨在將橫切關注點與業務主體進行進一步分離,以提升程序代碼的模塊化程度。經過在現有代碼基礎上增長額外的通知(Advice)機制,可以對被聲明爲「切點(Pointcut)」的代碼塊進行統一管理與裝飾,如「對全部方法名以‘set*’開頭的方法添加後臺日誌」。該思想使得開發人員可以將與代碼核心業務邏輯關係不那麼密切的功能(如日誌功能)添加至程序中,同時又不下降業務代碼的可讀性。面向切面的程序設計思想也是面向切面軟件開發的基礎。java

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

日誌功能便是橫切關注點的一個典型案例,由於日誌功能每每橫跨系統中的每一個業務模塊,即「橫切」全部有日誌需求的類及方法體。而對於一個信用卡應用程序來講,存款、取款、賬單管理是它的核心關注點,日誌和持久化將成爲橫切整個對象結構的橫切關注點。git

參見: https://zh.wikipedia.org/wiki...github

編程

簡單來講,就是功能上咱們要加其餘感受和本來功能無關的邏輯,好比性能日誌,代碼混在一塊兒,看着不爽,影響咱們理解。安全

舉個例子, 以下代碼咱們要多花幾眼時間才能看明白:架構

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/sta...



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/ros...

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;
    }
}

因此咱們調用的時候會是這樣



相關文章
相關標籤/搜索