cad.net AOP切面编程
插入到函数前后
曾几何时,大家会冥冥之中觉得,想在函数执行时候加一个执行前/执行后的效果.
但是函数是很难修改的,函数体body被编译成MSIL中间语言了,
没有经过一定汇编培训的话是困难重重.
并且想改的函数或许已经写了很多地方了,
直接加入两个前后函数并不合适,
未来还可能存在修改,又不要这两个前后函数了.
有什么是代码上面的技巧去实现呢?
我们很自然而然想到事件,不过事件是预埋,
我们并不会在每个方法上面实现预埋事件,也不会那么做.
我们更多是希望后埋.
方案一:装饰器模式
装饰器是其实网站开发最常用的,因为它是用IOC进行动态创建,
也就是代理类包裹原始类,全部都走代理中调用,
代理就很容易在它前后插入其他语句,
因此代理和原始类需要共同接口.
无论这个模式接口实现还是事件实现,装饰器模式它都有一定问题.
缺点:接口需要实现一个新类,
并且如果之前没有考虑到接口,那么还需要更换旧实现里面的函数.
优点:扩展调用链方便,并且还可以装饰器套装饰器.
例如你需要同时支持json,xml,db写入配置,
但是你已经写了json,那么就可以这样一层套一层.
// 接口
public interface IService{
void Execute();
}
// 服务(原始对象)
public class Service : IService{
public void Execute() {
Console.WriteLine("Service Executed");
}
}
// 装饰器(把原始对象生命周期纳入本类字段中)
public class ServiceDecorator : IService {
private readonly IService _service;
public ServiceDecorator(IService service) {
_service = service;
}
public void Execute() {
Console.WriteLine("开始,插入新方法");
Console.WriteLine("展示返回值去控制是否回调原始方法,在修复bug代码时候就可以实现此功能");
Console.WriteLine("输入字符a回调,否则屏蔽:");
string input = Console.ReadLine();
// 这样就可以屏蔽方法
if (input == "a") {
_service.Execute(); //原始方法
}
Console.WriteLine("结束,插入新方法");
}
}
// 调用
class Program {
static void Main(string[] args) {
// 创建服务对象
IService s = new Service();
// 创建装饰器,并将服务对象作为参数传递给装饰器,这个方法IFox用在填充类了
s = new ServiceDecorator(s);
// 调用装饰后的执行方法(旧方法调用就不需要改动了)
s.Execute();
Console.ReadKey();
}
}
方案二:AOP切面编程
举个例子,反射全部命令,然后全部回调到一个S方法上面.
那么这个S方法内部就可以写,执行前调用A方法,执行后调用B方法.
这有什么用呢?
可以在S方法内部: try{cmd()}catch{}
不就是把全部命令的错误都拦截了吗?
// 插件命令管理器类
public static class CommandManager {
// 定义命令组名称
private const string GroupName = "My";
// 键为命令名称(全大写),值为方法名称
private static Dictionary<string, string> _cmdMap = new();
// 加载插件命令的方法,应放在ExtensionApplication接口的Initialize事件中调用
public static void AddCommands() {
// 权限验证
if (true) return;
_cmdMap.Clear();
// 反射本程序集,获取所有带有CustomFlags特性的方法,并添加插件命令
foreach (var info in typeof(CommandManager).GetMethods()) {
object[] atts = info.GetCustomAttributes(typeof(CustomFlags), false);
if (atts.Length <= 0) continue;
// 获取CustomFlags特性
var cu = (CustomFlags)atts[0];
string cmdName = info.Name.ToUpper();
_cmdMap.Add(cmdName,info.Name);
// 添加cad命令栈(acad08没有此方法,采用动态编译跨程序域调用,或者直接通过_cmdMap在输入钩子调用)
Utils.AddCommand(GroupName,
cmdName,
cmdName,
cu.Parameter,
OnCommand);
}
}
// 统一将全部命令回调到此方法,实现AOP.
private static void OnCommand() {
// 将焦点设置到DWG视图
Utils.SetFocusToDwgView();
// 获取当前正在执行的命令名称(acad08可以采用键盘钩子拦截获取)
string curCmd = Env.Editor.Document.CommandInProgress.ToUpper();
// 反射获取方法并调用...这里没有缓存?
MethodInfo methodInfo = typeof(CommandManager).GetMethod(_cmdMap[curCmd]);
try{
methodInfo?.Invoke(null,null);
}catch{...}
}
// 卸载插件命令的方法,通常在插件过期后
public static void RemoveCommands() {
if (_cmdMap.Count == 0) return;
// 移除命令
foreach (var key in _cmdMap.Keys) {
Utils.RemoveCommand(GroupName, key);
}
_cmdMap.Clear();
}
// 以下是自定义的插件命令示例
[CustomFlags(CommandFlags.UsePickSet)] // 使用CustomFlags特性标记命令
public static void cs1() {
MessageBox.Show("Cs1");
}
[CustomFlags(CommandFlags.UsePickSet)] // 同上
public static void cs2() {
MessageBox.Show("Cs2");
}
}
// 自定义CommandFlags特性
[AttributeUsage(AttributeTargets.Method)]
public class CustomFlags : Attribute {
public CommandFlags Parameter { get; set; }
public CustomFlags(CommandFlags commandFlags) {
Parameter = commandFlags;
}
}
方案三:事件/钩子
C/C++上面其实只能埋钩子,使用函数指针作为钩子的触发点.
typedef void (*HookFunction)(void);
HookFunction hook1 = NULL;
HookFunction hook2 = NULL;
void trigger_hook() {
// 开头的钩子函数
if (hook1) {
hook1();
}
// 执行原始函数...
// 结束的钩子函数
if (hook2) {
hook2();
}
}
// 外部实现函数指针传递到参数中
void set_hook1(HookFunction func) {
hook1 = func;
}
void set_hook2(HookFunction func) {
hook2 = func;
}
方案四:MSIL方式注入
除了反射纯代码实现之外,还可以通过修改MSIL的方式进行.
几个包说明:
Mono.Cecil 它不能动态注入,只可以选择一个dll然后注入.
Fody 能反射自己的特性来进行注入
Harmony2 能反射人家并且动态注入,
但是我发现被注入的函数不能断点了,而注入的头尾两个函数可以,真奇怪...估计是注入行为已经破坏了调试文件,但是它没有把调试文件也顺带处理...
另见此文
若你的dll有签名的话,注意实践起来的问题.
Mono.Cecil
引用nuget
<ItemGroup Condition="'$(TargetFramework)' != 'net35'">
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net35'">
<PackageReference Include="Mono.Cecil" Version="0.9.4" />
</ItemGroup>
代码
using Autodesk.AutoCAD.Runtime;
using Acap = Autodesk.AutoCAD.ApplicationServices.Application;
using System;
using System.IO;
using System.Reflection;
/*
* 警告: 此处测试若同时使用 nuget:Mono.Cecil 和 0Harmony.dll 将导致冲突
* 此处为 nuget:Mono.Cecil 的测试
*/
using AssemblyDef = Mono.Cecil.AssemblyDefinition;
using OpCodes = Mono.Cecil.Cil.OpCodes;//0Harmony.dll 把这个类设置为内部的!
/*
* 旧例子
* https://www.cnblogs.com/RicCC/archive/2010/03/21/mono-cecil.html
* https://www.cnblogs.com/whitewolf/archive/2011/07/28/2119969.html
* 新例子:
* https://blog.csdn.net/ZslLoveMiwa/article/details/82192522
* https://blog.csdn.net/lee576/article/details/38780889
* AssemblyFactory 0.6版本被移除,改用 AssemblyDefinition
* CilWorker 类被移除,改用 GetILProcessor()
* Import 过时,改用 ImportReference
*
*/
namespace JoinBox;
public class TestMonoClass {
[CommandMethod(nameof(TestMono))]
public void TestMono() {
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var ed = doc.Editor;
ed.WriteMessage("Hello, World!");
}
}
public class Program {
[JoinBoxInitialize]
public static void Main(string[] args) {
string fullName = AutoGo.ConfigPathAll;
if (fullName == null || !File.Exists(fullName)) {
Console.WriteLine("没有此路径文件:" + fullName);
return;
}
// Mono.Cecil 只能读取非占用文件来改函数,所以用改用 Lib.Harmony
// 没有文件流就不可以保存
using var file = new FileStream(fullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
var assembly = AssemblyDef.ReadAssembly(file);
if (assembly is null) return;
// 构造Console方法
var tt = typeof(System.Diagnostics.Debug)
.GetMethod("WriteLine", new Type[] { typeof(string) });
var method = assembly.MainModule.Types
.Where(type => type.Name != "<Module>")
.SelectMany(type => type.Methods)
.Where(method => method.Name == nameof(TestMonoClass.TestMono))
.FirstOrDefault();
if (method is null) return;
var worker = method.Body.GetILProcessor();
// 开头插入
var ins = method.Body.Instructions[0];
// 定义字符串变量
worker.InsertBefore(ins, worker.Create(OpCodes.Ldstr,
"Method start…"));
// 定义方法
worker.InsertBefore(ins, worker.Create(OpCodes.Call,
Helper.ImRef(assembly.MainModule, tt)));
// 尾巴插入
ins = method.Body.Instructions[method.Body.Instructions.Count - 1];
// 定义字符串变量
worker.InsertBefore(ins, worker.Create(OpCodes.Ldstr,
"Method finish…"));
// 定义方法
worker.InsertBefore(ins, worker.Create(OpCodes.Call,
Helper.ImRef(assembly.MainModule, tt)));
// 保存入程序集
assembly.Write(fullName);
}
public static class Helper {
public static Mono.Cecil.MethodReference ImRef(
Mono.Cecil.ModuleDefinition module, MethodInfo methodInfo) {
#if NET35
return module.Import(methodInfo);
#else
return module.ImportReference(methodInfo);
#endif
}
}
}
Fody
https://blog.csdn.net/qq_28448587/article/details/120067843
https://www.cnblogs.com/cqgis/p/6360231.html
FodyWeavers.xml 编译的时候自动生成,所以不需要准备
引用nuget
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Costura.Fody" Version="5.8.0-alpha0098" PrivateAssets="All" />
<PackageReference Include="MethodDecorator.Fody" Version="1.1.1" PrivateAssets="All" />
<!--WPF的属性绑定-->
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" PrivateAssets="All" />
</ItemGroup>
</Project>
属性注入
WPF用得多
Fody它会自动找INotifyPropertyChanged
接口,
然后把这个类下的属性上注入到通知接口
代码
public class Person : INotifyPropertyChanged {
//实现事件来进行通知
public event PropertyChangedEventHandler PropertyChanged;
//不触发事件通知
[PropertyChanged.DoNotNotify]
public string GivenNames { get; set; }
//触发事件通知
[PropertyChanged.DependsOn("GivenName", "FamilyName")] //依赖属性
public string FamilyName { get; set; }
public string FullName => $"{GivenNames} {FamilyName}";
}
public static void Main(string[] args) {
var person = new Person();
person.PropertyChanged += (sender, e) => {
// 没有新旧值拦截(修改前,修改后)
// 有要求就改用:https://github.com/canton7/PropertyChanged.SourceGenerator
System.Console.WriteLine(sender);//来源 Person
System.Console.WriteLine(e.PropertyName);//触发通知的属性 FamilyName
};
person.FamilyName = "vvvvv";
}
函数注入
注册类 AssemblyInfo.cs
//新建一个 AssemblyInfo.cs
using TestDemo;
[module: FodyAtt]
namespace TestDemo {
public class AssemblyInfo {
}
}
主函数 Main
public static void Main(string[] args) {
// 0x02 调用测试类
var f = new FodyCmd();
f.DoSomething();
}
测试类 FodyTest.cs
using System;
namespace TestDemo;
[FodyAtt] // 写在方法上就是方法注入,写在类上就是全部方法注入
public class FodyCmd {
public FodyCmd() {
Console.WriteLine("默认构造函数也会执行");
}
public void DoSomething() {
Console.WriteLine("执行了:" + nameof(DoSomething));
}
}
特性
using System;
using System.Reflection;
namespace TestDemo;
[AttributeUsage(AttributeTargets.All)]
public class FodyAtt : Attribute {
protected object InitInstance;
protected MethodBase InitMethod;
protected object[] Args;
public void Init(object instance, MethodBase method, object[] args) {
InitMethod = method;
InitInstance = instance;
Args = args;
}
public void OnEntry() {
Console.WriteLine("进入函数之前");
}
public void OnExit() {
Console.WriteLine("进入函数之后");
}
public void OnException(Exception exception) {
Console.WriteLine("例外" + exception.Message);
}
}
Harmony2
基本上跟Fody差不多,但是它可以反射搜索函数,不需要写特性
https://stackoverflow.com/questions/7299097/dynamically-replace-the-contents-of-a-c-sharp-method
https://harmony.pardeike.net/articles/basics.html
引用nuget
Lib.Harmony
控制台注入例子
官方还有一个例子不是很好懂,因此不展示,上面的链接有.
using HarmonyLib;
using System;
namespace MyTest;
public class Program {
static void Main(string[] args) {
var harmony = new Harmony(nameof(Program));
var mPrefix = SymbolExtensions.GetMethodInfo(() => JoinBoxCmdAddFirst());
var mPostfix = SymbolExtensions.GetMethodInfo(() => JoinBoxCmdAddLast());
var mp1 = new HarmonyMethod(mPrefix);
var mp2 = new HarmonyMethod(mPostfix);
var mOriginal = AccessTools.Method(typeof(Program), nameof(DD));
var newMet = harmony.Patch(mOriginal, mp1, mp2);
DD();
Console.WriteLine("Main");
}
public static void JoinBoxCmdAddFirst() {
Console.WriteLine("JoinBoxCmdAddFirst");
}
public static void JoinBoxCmdAddLast() {
Console.WriteLine("JoinBoxCmdAddLast");
}
public static void DD() {
Console.WriteLine("Hello World!"); // 此处无法断点
}
}
声明式事务(这只是实验,不要用于项目)
它可以反射处理所有的cad命令特性,
然后前面注入事务,后面提交事务,中间就不用写事务了.
而本类库默认情况不侵入用户的命令,
用户想侵入需要手动进行 AOP.Run(nameSpace) 侵入到指定的命名空间,
而启动策略之后,自动将事务侵入命名空间下的命令,此时有拒绝特性的策略保证括免.
所以只是用一个模块就可以改变其他模块行为,实现修改少,作用大.
启用策略
public class AutoAOP {
[JoinBoxInitialize]
public void Initialize() {
// 添加声明式事务
AOP.Run(nameof(TestNameSpace));
}
}
注入和反射
using HarmonyLib;
public class JoinBoxRefuseInjectionTransaction : Attribute {
/// <summary>
/// 拒绝注入事务
/// </summary>
public JoinBoxRefuseInjectionTransaction(){
}
}
public class AOP {
/// <summary>
/// 含有拒绝注入的特性
/// </summary>
/// <param name="attr">特性集合</param>
/// <returns>含有true</returns>
static bool RefuseInjectionTransaction(object[] atts) {
var att = atts.FirstOrDefault(a => a is JoinBoxRefuseInjectionTransaction);
return att is not null;
}
/// <summary>
/// 在此命名空间下的命令会注入事务
/// </summary>
public static void Run(string nameSpace) {
Dictionary<string, (CommandMethodAttribute Cmd, Type MetType, MethodInfo MetInfo)> cmdMap = new();
// https://www.cnblogs.com/JJBox/p/10850000.html
var types = AutoClass.AppDomainGetTypes()
.Where(tyep => type.Namespace == nameSpace)
.ToArray();
foreach(var type in types) {
// 类上面特性
if (type.IsClass) {
var atts = type.GetCustomAttributes(true);
if (RefuseInjectionTransaction(atts))
continue;
}
// 函数上面特性
// 特性下面的方法要是Public,否则就被编译器优化掉了.
type.GetMethods().Where(method => {
var atts = method.GetCustomAttributes(true);
if(RefuseInjectionTransaction(atts)) return;
var has = atts.FirstOrDefault(a => a is CommandMethodAttribute);
if (has is CommandMethodAttribute cmdAtt)
cmdMap.Add(cmdAtt.GlobalName, (cmdAtt, type, method));
});
}
if (cmdMap.Count == 0) return;
var harmony = new Harmony(nameSpace);
var mPrefix = SymbolExtensions.GetMethodInfo(() => JoinBoxCmdAddFirst());//进入函数前
var mPostfix = SymbolExtensions.GetMethodInfo(() => JoinBoxCmdAddLast());//进入函数后
var mp1 = new HarmonyMethod(mPrefix);
var mp2 = new HarmonyMethod(mPostfix);
foreach (var item in cmdMap)
{
// 原函数执行(所处类type,函数名)
var mOriginal = AccessTools.Method(item.Value.MetType, item.Value.MetInfo.Name);
// mOriginal.Invoke();
// 新函数执行:创造两个函数加入里面
var newMet = harmony.Patch(mOriginal, mp1, mp2);
// newMet.Invoke();
}
}
public static Transaction Transaction;
// 命令开启打开事务
public static void JoinBoxCmdAddFirst() {
Transaction = db.TransactionManager.StartTransaction();
}
// 命令结束提交事务
public static void JoinBoxCmdAddLast() {
if(Transaction.IsDisposed) return;
if(Transaction.TransactionManager.NumberOfActiveTransactions != 0) {
Transaction.Commit();
Transaction.Dispose();
}
}
}
拒绝策略
namespace TestNameSpace {
public class AopTestClass {
// 类不拒绝,这里拒绝
[JoinBoxRefuseInjectionTransaction]
[CommandMethod("JoinBoxRefuseInjectionTransaction")]
public void JoinBoxRefuseInjectionTransaction()
{
}
//不拒绝
[CommandMethod("InjectionTransaction")]
public void InjectionTransaction()
{
// 怎么用事务呢? 直接 AOP.Transaction
}
}
// 拒绝注入事务,写类上,则方法全都拒绝
[JoinBoxRefuseInjectionTransaction]
public class AopTestClassRefuseInjection
{
// 此时这个也是拒绝的..这里加特性是多余
[JoinBoxRefuseInjectionTransaction]
[CommandMethod("JoinBoxRefuseInjectionTransaction2")]
public void JoinBoxRefuseInjectionTransaction2()
{
// 拒绝注入就要自己开事务,通常用在循环提交事务上面.
// 另见 报错0x02 https://www.cnblogs.com/JJBox/p/10798940.html
// 就自己新建啊~
}
[CommandMethod("InjectionTransaction2")]
public void InjectionTransaction2()
{
}
}
}
(完)