Categories
程式開發

AOP有几种实现方式?


0. 前言

副标题:无价值人生记录.0:浪费 1000% 时间去做一个用来节省 1% 时间的“轮子玩具”(中:AOP回顾)

上接:https://xie.infoq.cn/article/5d29fbc6edc2bbdae68a5880c

上面说的是我为什么想做这个aop。

接下来说说aop是啥,怎么搞它。

1. 回顾 AOP 是什么?

维基百科解释如下:

面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向方面的程序设计、剖面导向程序设计)是计算机科学“中的一种程序设计思想“,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰,如“对所有方法名以‘set*’开头的方法添加后台日志”。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。面向切面的程序设计思想也是面向切面软件开发的基础。面向切面的程序设计将代码逻辑切分为不同的模块(即关注点(Concern),一段特定的逻辑功能)。几乎所有的编程思想都涉及代码功能的分类,将各个关注点封装成独立的抽象模块(如函数、过程、模块、类以及方法等),后者又可供进一步实现、封装和重写。部分关注点“横切”程序代码中的数个模块,即在多个模块中都有出现,它们即被称作“横切关注点(Cross-cutting concerns, Horizontal concerns)”。日志功能即是横切关注点的一个典型案例,因为日志功能往往横跨系统中的每个业务模块,即“横切”所有有日志需求的类及方法体。而对于一个信用卡应用程序来说,存款、取款、帐单管理是它的核心关注点,日志和持久化将成为横切整个对象结构的横切关注点。参见: https://zh.wikipedia.org/wiki/%E9%9D%A2%E5%90%91%E5%88%87%E9%9D%A2%E7%9A%84%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1

简单来说,就是功能上我们要加其他感觉和原本功能无关的逻辑,比如性能日志,代码混在一起,看着不爽,影响我们理解。

举个例子, 如下代码我们要多花几眼时间才能看明白:

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 fieldSymbols = new List();
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 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 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);
}

}

///


/// Created on demand before each generation pass
///


class SyntaxReceiver : ISyntaxReceiver
{
public List CandidateFields { get; } = new List();

///

/// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation
///

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 ''
{
} // end of class

.class public auto ansi beforefieldinit C
extends [mscorlib]System.Object
{
// Fields
.field private initonly int32 '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::'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::'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::'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."":()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
#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 // "":()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."":()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 看起来一模一样

AOP有几种实现方式? 1

但两者的真实的代码可能是这样子的

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

所以我们调用的时候会是这样

AOP有几种实现方式? 2

下一篇介绍一下 Roslyn 的 source generator 怎么做Aop