-0.1 系列目录
无价值人生记录.0
AOP 有几种实现方式?
用 Roslyn 做个 JIT 的 AOP
基于 Source Generators 做个 AOP 静态编织小实验
思想无语言边界:以 cglib 介绍 AOP 在 java 的一个实现方式
常见的emit实现AOP demo
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)
}
复制代码
新的实验特性,还在设计修改变化中
官方文档: 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"
复制代码
它们也是编程语言的一种,也是可以写的,所以我们可以用来把别人方法体改了。
当然怎么改,怎么改得各种各样方法都兼容,做的人简直👍
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 编译后静态编织一次
2.2.4 运行时
严格来说,运行时也是编译后
不过不是再编织一次,而是每次运行都编织
并且没有什么 前中后了,
都是程序启动后,在具体类执行之前,把这个类编织了
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;
}
}
复制代码
所以我们调用的时候会是这样
下一篇介绍一下 Roslyn 的 source generator 怎么做 Aop
评论 (2 条评论)