AOP 有几种实现方式?
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 有几种实现方式?的更多相关文章
- 适用于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通用)>,现在重新整理一 ...
- JAVA高级架构师基础功:Spring中AOP的两种代理方式:动态代理和CGLIB详解
在spring框架中使用了两种代理方式: 1.JDK自带的动态代理. 2.Spring框架自己提供的CGLIB的方式. 这两种也是Spring框架核心AOP的基础. 在详细讲解上述提到的动态代理和CG ...
- AOP的两种实现方式
技术交流群 :233513714 AOP,面向切面编程,可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术. Aspect Oriented Progr ...
- C# 实现AOP 的几种常见方式
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的中统一处理业务逻辑的一种技术,比较常见的场景是:日志记录,错误捕获 ...
- Spring中AOP的两种代理方式(Java动态代理和CGLIB代理)
第一种代理即Java的动态代理方式上一篇已经分析,在这里不再介绍,现在我们先来了解下GCLIB代理是什么?它又是怎样实现的?和Java动态代理有什么区别? cglib(Code Generation ...
- (一)spring aop的两种配置方式。
sring aop的方式有两种:(1)xml文件配置方式(2)注解的方式实现,我们可以先通过一个demo认识spring aop的实现,然后再对其进行详细的解释. 一.基于注解的springAop配置 ...
- Spring AOP源码分析(二):AOP的三种配置方式与内部解析实现
AOP配置 在应用代码中,可以通过在spring的XML配置文件applicationContext.xml或者基于注解方式来配置AOP.AOP配置的核心元素为:pointcut,advisor,as ...
- spring AOP 的几种实现方式(能测试)
我们经常会用到的有如下几种 1.基于代理的AOP 2.纯简单Java对象切面 3.@Aspect注解形式的 4.注入形式的Aspcet切面 一.需要的java文件 public class ChenL ...
- spring AOP的两种配置方式
连接点(JoinPoint) ,就是spring允许你是通知(Advice)的地方,那可就真多了,基本每个方法的前.后(两者都有也行),或抛出异常是时都可以是连接点,spring只支持方法连接点.其他 ...
随机推荐
- JVM字节码执行引擎
一.概述 在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译器执行(通过即时编译器产生本地代码执行)两种选择,所有的Java虚拟机的执行引擎都是一致的:输 ...
- 支付宝电脑网站支付 alipay.trade.page.pay
只涉及支付接口 其他接口没有使用 支付宝官方文档:https://docs.open.alipay.com/270/105899/ 支付接口文档 https://docs.open.alipay.co ...
- Ramnit蠕虫病毒分析和查杀
Ramnit是一种蠕虫病毒.拥有多种传播方式,不仅可以通过网页进行传播,还可以通过感染计算机内可执行文件进行传播.该病毒在2010年第一次被安全研究者发现,从网络威胁监控中可以看出目前仍然有大量的主机 ...
- BT下载器Folx中删除任务与删除文件的功能区别
当用户使用Folx完成了任务下载后,该任务仍会保留在下载列表中,并标注"已结束"的标记.随着使用时间的增长,Folx下载列表中会包含过多的"已结束"任务,用户需 ...
- 如何使用Camtasia制作动态动画场景?
也许在学习编辑视频的你知道Camtasia 2019(win系统),知道Camtasia的视频编辑功能,录制屏幕功能,但你可能想不到,Camtasia还可以制作动态动画场景.跟我一起学习一下吧! 一. ...
- O - Matching 题解(状压dp)
题目链接 题目大意 给你一个方形矩阵mp,边长为n(n<=21) 有n个男生和女生,如果\(mp[i][j]=1\) 代表第i个男生可以和第j个女生配对 问有多少种两两配对的方式,使得所有男生和 ...
- Java蓝桥杯——贪心算法
贪心算法 贪心算法:只顾眼前的苟且. 即在对问题求解时,总是做出在当前看来是最好的选择 如买苹果,专挑最大的买. 最优装载问题--加勒比海盗 货物重量:Wi={4,10,7,11,3,5,14,2} ...
- Arduion学习(一)点亮三色发光二极管
这是我接触Arduion以来第一个小实验 实验准备: 1.查阅相关资料,了解本次实验所用到的引脚.接口的相关知识. 2.准备Arduion板(本次实验所用到的型号为mega2560).三色发光二极管. ...
- sqli-labs-master 闯关前知识点学习
1).前期准备.知识点 开始之前,为了方便查看sql注入语句,我在sqli-labs-master网页源码php部分加了两行代码,第一行意思是输出数据库语句,第二行是换行符 一.Mysql 登录 1. ...
- Visual Studio 连接 SQL Server 关键代码
首先先把Visual Studio 上面工具打开-->连接数据库-->选择Microsoft SQL Server进入(有两种验证方式:1.windows验证方式[就是本机验证]:2.SQ ...