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

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

AOP 有几种实现方式?的更多相关文章

  1. 适用于app.config与web.config的ConfigUtil读写工具类 基于MongoDb官方C#驱动封装MongoDbCsharpHelper类(CRUD类) 基于ASP.NET WEB API实现分布式数据访问中间层(提供对数据库的CRUD) C# 实现AOP 的几种常见方式

    适用于app.config与web.config的ConfigUtil读写工具类   之前文章:<两种读写配置文件的方案(app.config与web.config通用)>,现在重新整理一 ...

  2. JAVA高级架构师基础功:Spring中AOP的两种代理方式:动态代理和CGLIB详解

    在spring框架中使用了两种代理方式: 1.JDK自带的动态代理. 2.Spring框架自己提供的CGLIB的方式. 这两种也是Spring框架核心AOP的基础. 在详细讲解上述提到的动态代理和CG ...

  3. AOP的两种实现方式

    技术交流群 :233513714 AOP,面向切面编程,可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术.    Aspect Oriented Progr ...

  4. C# 实现AOP 的几种常见方式

    AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的中统一处理业务逻辑的一种技术,比较常见的场景是:日志记录,错误捕获 ...

  5. Spring中AOP的两种代理方式(Java动态代理和CGLIB代理)

    第一种代理即Java的动态代理方式上一篇已经分析,在这里不再介绍,现在我们先来了解下GCLIB代理是什么?它又是怎样实现的?和Java动态代理有什么区别? cglib(Code Generation ...

  6. (一)spring aop的两种配置方式。

    sring aop的方式有两种:(1)xml文件配置方式(2)注解的方式实现,我们可以先通过一个demo认识spring aop的实现,然后再对其进行详细的解释. 一.基于注解的springAop配置 ...

  7. Spring AOP源码分析(二):AOP的三种配置方式与内部解析实现

    AOP配置 在应用代码中,可以通过在spring的XML配置文件applicationContext.xml或者基于注解方式来配置AOP.AOP配置的核心元素为:pointcut,advisor,as ...

  8. spring AOP 的几种实现方式(能测试)

    我们经常会用到的有如下几种 1.基于代理的AOP 2.纯简单Java对象切面 3.@Aspect注解形式的 4.注入形式的Aspcet切面 一.需要的java文件 public class ChenL ...

  9. spring AOP的两种配置方式

    连接点(JoinPoint) ,就是spring允许你是通知(Advice)的地方,那可就真多了,基本每个方法的前.后(两者都有也行),或抛出异常是时都可以是连接点,spring只支持方法连接点.其他 ...

随机推荐

  1. scala中的val,var和lazy

    转自:https://yerias.github.io/2020/03/19/scala/3/#3%EF%BC%9Alazy%E4%BF%AE%E9%A5%B0%E7%AC%A6%E5%8F%AF%E ...

  2. vector删除特定元素

    删除vector中小于20的元素,注意要使迭代器失效,不能简单的删除. #include <iostream>#include <vector>using namespace ...

  3. FL Studio中如何进行工具栏编辑

    菜单工具栏是我们使用FL Studio时经常需要使用的一个功能,那么,除了软件默认的菜单工具栏,我们应该如何编辑菜单工具栏呢? 图1:工具栏编辑 想要编辑更改默认菜单栏,我们只需要鼠标右键单击菜单工具 ...

  4. JS获取当前日期及 js获取当前时间和一星期前的时间

    var myDate = new Date();     new Date() 代表当前 年 月 日 时 分 秒: myDate.getYear();        //获取当前年份(2位),getY ...

  5. css3系列之transform 详解skew

    skew skewx skewy skewX()  倾斜该元素,里面填的是角度,下面↓ 你会看到,随着元素被倾斜,高度居然不变.聪明的你,一定会知道,高度不变,代表了,Y轴被拉伸了. 跟scale 同 ...

  6. 模拟赛38 B. T形覆盖 大模拟

    题目描述 如果玩过俄罗斯方块,应该见过如下图形: 我们称它为一个 \(T\) 形四格拼板 .其中心被标记为\(×\). 小苗画了一个 \(m\) 行 \(n\) 列的长方形网格.行从 \(0\) 至 ...

  7. CF980C Posterized

    先来吐槽一下这个 sb 翻译,根本就没做过题吧-- 大概就是让你给值域分成连续的几组,每组大小不能超过 \(k\),然后将序列中的值全部替换成其组内的最小值,要使得序列的字典序最小. 因为是字典序,所 ...

  8. C语言讲义——errno

    #define EPERM 1 /* Operation not permitted */ #define ENOENT 2 /* No such file or directory */ #defi ...

  9. 可变长形参,增强for语句

    `package 可变长形参; public class VarArgumen { public static int max(int...varArgs) {//就有可变长形参的求最大值方法 //可 ...

  10. Network-Emulator-Toolkit 模拟各种网络环境 windows

    背景.目标.目的 (1) 背景: 我们在使用网络时,时常遇到在正常网络环境下的代码运行一切正常,可以复杂的网络环境下的各种问题无法复现,必须搭建模拟各种网络环境,去复现问题,定位问题.不管是移动平台, ...