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

缺陷
Fody很方便,也有不方便的时候

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()
        {
        }
    }
}

(完)

posted @ 2022-04-17 22:00  惊惊  阅读(607)  评论(1编辑  收藏  举报