How many ways can AOP be implemented?

Time:2021-7-12

1. Review what is AOP?

Wikipedia explains as follows:

Aspect oriented programming (AOP) is a kind of programming idea in computer science, which aims to further separate crosscutting concerns from business entities, so as to improve the modularity of program code. By adding an additional advice mechanism to the existing code, the code blocks declared as “pointcut” can be managed and decorated uniformly, such as “adding background logs to all methods whose names begin with” set * “. This idea enables developers to add functions that are not closely related to the core business logic of the code (such as log function) to the program without reducing the readability of the business code. Aspect oriented programming is also the basis of aspect oriented software development.

Aspect oriented programming divides code logic into different modules (that is, concern, a specific logical function). Almost all programming ideas involve the classification of code functions, encapsulating each concern into independent abstract modules (such as functions, procedures, modules, classes and methods), which can be further implemented, encapsulated and rewritten. Some concerns “crosscut” several modules in the program code, that is, they appear in many modules, they are called “cross cutting concerns (horizontal concerns)”.

Log function is a typical case of crosscutting concerns, because log function often spans every business module in the system, that is, crosscutting all classes and methods with log requirements. For a credit card application, deposit, withdrawal and bill management are its core concerns. Log and persistence will become the crosscutting concerns of the whole object structure.

See:https://zh.wikipedia.org/wiki…



To put it simply, we need to add other logic that has nothing to do with the original function, such as performance logs and code mixed together, which will affect our understanding.

For example, it takes us a few more minutes to understand the following code:

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

Then we need to record a series of logs, which will look like this:

 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}.");
 }

All of a sudden, this method is much more complex. At least you have to find a bunch of parameters that seem to have nothing to do with the method to call it

The idea of AOP is to split the above methods, so that we don’t see methods like log in our eyes

 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 lets you see that the doamethod method you only call is actually:

 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}.");
 }

So AOP actually does this thing,

No matter the language,

No matter what happens,

In fact, as long as you do this, isn’t it AOP?



2. Classification of implementation methods similar to AOP ideas

There are many ways to achieve AOP. Let’s make a simple classification, which is not necessarily comprehensive

2.1 by method

2.1.1 metaprogramming

Many languages have built-in “enhanced code” functions like this,

Generally speaking, from the perspective of security and compilation, most metaprogramming only allows new code, not modification.

This is a compiler must have to be able to do( If you don’t, you can write your own compiler, as long as you can do it.)

Of course, the concept of metaprogramming can not only be used to do things like AOP,

You can also do all kinds of things you want to do (as long as you can do within the limits)

The following example is to generate some new methods.

macro

For example, rust / C + + has such a function

For example, the document of 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();
}

Macro implementation

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’s source generators

New experimental characteristics are still in the process of design modification

Official document:https://github.com/dotnet/ros…

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

Generator implementation

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([email protected]"
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([email protected]"
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 code modification

Code file modification

Generally speaking, there are few such implementations. If the code files are changed, how can we write bugs.

Intermediate language modification

There are a lot of language compilation results are not direct machine code, but an optimized intermediate language close to the bottom, which is convenient to expand and support different CPU and different machine architecture.

Like dotnet’s 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

For example, the bytecode of Java (the result of decompilation)

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"

They are also a kind of programming language and can be written, so we can use them to change other people’s methods.

Of course, how to change it, how to make it compatible with all kinds of methods, the people who do it are simply not

Generate proxy code

Do not modify the original code file, add agent code implementation

Instead of modifying the compiled IL or bytecode, add the proxy code in the form of IL or bytecode

2.1.3 using compiler or runtime functions

Generally speaking, it also uses the extension function provided by the compiler itself

Java’s AspectJ seems to be able to use the AJC compiler to do things

2.1.4 using runtime functions

In theory, dotnet can also implement CLR profiling API to modify method body during JIT compilation. To achieve real unlimited runtime static AOP (but it seems that C + + can be used to do CLR profiling API, with few documents and compatibility, which seems to be very difficult to do)

2.2 according to weaving time

2.2.1 before Compilation

such as

  • Modify other people’s code files
  • Generate new code, let the compiler compile in, run time to find a way to use the new code

2.2.2 compile time

  • meta-programming
  • Be a compiler

2.2.3 static weaving once after compilation

According to the compiled things (dotnet DLL or other language things), using reflection, parsing and other technologies to generate proxy implementation, and then plug it in

2.2.4 runtime

Strictly speaking, the runtime is also compiled

But it’s not weaving again, it’s weaving every time it runs

And there’s no front, middle or back,

After the program is started, this class is woven before the specific class is executed

For example, Java’s class loader: when the target class is loaded into the JVM, it reinforces the bytecode of the target class through a special class loader.

All kinds of IOC containers with AOP function create proxy instances before generating instances

In fact, it can also be replaced with proxy type when registering IOC container

3. Agency

Let’s talk about what an agent is,

After all, many AOP frameworks or other frameworks have the idea of using agents,

Why do we all play like this?



Very simple, the agent is to help you do the same thing, and can do more than you, but also does not move to your original code.

For example, as like as two peas, the real class and the proxy class look exactly alike.
How many ways can AOP be implemented?

But the real code for both might look like this

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

So this is what we call
How many ways can AOP be implemented?