上接:AOP有几种实现方式
接下来说说怎么做AOP的demo,先用csharp 说下动态编织和静态编织,有时间再说点java的对应内容。
第一篇先说Roslyn 怎么做个JIT的AOP demo。
为啥这样讲呢?
实际是因为Roslyn 已经包含了JIT的全部部分,那我也就不用说任何JIT的实现了。(真爽)
所以本篇实际说的是以下这些内容:
怎么引入Roslyn做JIT编译代码
代理模式的AOP是什么样
为什么不推荐在生产环境不做优化就这样玩?
1. JIT编译代码Roslyn 是.NET的编译器,感兴趣的可以参见文档 docs.microsoft.com/en-us/dotne…
实际上Roslyn已经做的非常简单了,几行代码引入进来就可以编译csharp代码了。
不信我们就手把手写一个
1.1 引入Roslyn包
<ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" /> <PackageReference Include="System.Runtime.Loader" Version="4.3.0" /> </ItemGroup>复制代码
1.2 代码转化为语法树
public SyntaxTree ParseToSyntaxTree(string code) { var parseOptions = new CSharpParseOptions(LanguageVersion.Latest, preprocessorSymbols: new[] { "RELEASE" }); // 有许多其他配置项,最简单这些就可以了 return CSharpSyntaxTree.ParseText(code, parseOptions); }复制代码
1.3 准备编译器实例
public CSharpCompilation BuildCompilation(SyntaxTree syntaxTree) { var compilationOptions = new CSharpCompilationOptions( concurrentBuild: true, metadataImportOptions: MetadataImportOptions.All, outputKind: OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release, allowUnsafe: true, platform: Platform.AnyCpu, checkOverflow: false, assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default); // 有许多其他配置项,最简单这些就可以了 var references = AppDomain.CurrentDomain.GetAssemblies() .Where(i => !i.IsDynamic && !string.IsNullOrWhiteSpace(i.Location)) .Distinct() .Select(i => MetadataReference.CreateFromFile(i.Location)); // 获取编译时所需用到的dll, 这里我们直接简单一点 copy 当前执行环境的 return CSharpCompilation.Create("code.cs", new SyntaxTree[] { syntaxTree }, references, compilationOptions); }复制代码
1.4 编译到内存中
public Assembly ComplieToAssembly(CSharpCompilation compilation) { using (var stream = new MemoryStream()) { var restult = compilation.Emit(stream); if (restult.Success) { stream.Seek(0, SeekOrigin.Begin); return AssemblyLoadContext.Default.LoadFromStream(stream); } else { throw new Exception(restult.Diagnostics.Select(i => i.ToString()).DefaultIfEmpty().Aggregate((i, j) => i + j)); } } }复制代码
1.5 测试一下
static void TestJIT() { var code = @" public class HiJ { public void Test(string v) { System.Console.WriteLine($""Hi, {v}!""); } }"; var jit = new Jit(); var syntaxTree = jit.ParseToSyntaxTree(code); var compilation = jit.BuildCompilation(syntaxTree); var assembly = jit.ComplieToAssembly(compilation); // test foreach (var item in assembly.GetTypes()) { Console.WriteLine(item.FullName); item.GetMethod("Test").Invoke(Activator.CreateInstance(item), new object[] { "joker" }); } }复制代码
运行结果:
HiJ Hi, joker!复制代码
就这么简单,你就可以JIT了,想干什么都可以了。
2. 用代理方式实现AOP2.1 回顾代理是什么
这里单独再说一下代理是什么,
毕竟很多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; } }复制代码
所以我们调用的时候会是这样
2.2 做一个Proxy代码生成器
那么我们来做一个上面例子中能生成一模一样的ProxyClass 代码生成器
首先,我们都知道 csharp 再运行中可以反射获取元数据(反编译出代码也可以做,就是我们杀鸡用牛刀呢?)
我们知道了元数据,就可以拼字符串拼出我们想要的代码(对,你没看错,我们拼字符串就够了)
废话不说, show you code
public class ProxyGenerator { public string GenerateProxyCode(Type type, Action<StringBuilder, MethodBase> beforeCall, Action<StringBuilder, MethodBase> afterCall) { var sb = new StringBuilder(); sb.Append($"{(type.IsPublic ? "public" : "")} class {type.Name}Proxy : {type.Name} {{ "); foreach (var method in type.GetTypeInfo().DeclaredMethods) { GenerateProxyMethod(beforeCall, afterCall, sb, method); } sb.Append(" }"); return sb.ToString(); } private static void GenerateProxyMethod(Action<StringBuilder, MethodBase> beforeCall, Action<StringBuilder, MethodBase> afterCall, StringBuilder sb, MethodInfo method) { var ps = method.GetParameters().Select(p => $"{p.ParameterType.FullName} {p.Name}"); sb.Append($"{(method.IsPublic ? "public" : "")} override {method.ReturnType.FullName} {method.Name}({string.Join(",", ps)}) {{"); sb.Append($"{method.ReturnType.FullName} r = default;"); beforeCall(sb, method); sb.Append($"r = base.{method.Name}({string.Join(",", method.GetParameters().Select(p => p.Name))});"); afterCall(sb, method); sb.Append("return r; }"); } }复制代码
测试一下
public static class TestProxyGenerator { public static void Test() { var generator = new ProxyGenerator(); var code = generator.GenerateProxyCode(typeof(RealClass), (sb, method) => { }, (sb, method) => { sb.Append("r++;"); }); Console.WriteLine(code); } }复制代码
结果:
public class RealClassProxy : RealClass { public override System.Int32 Add(System.Int32 i,System.Int32 j) {System.Int32 r = default;r = base.Add(i,j);r++;return r; } }复制代码
2.3 结合在一起测试一下
public static class TestProxyJit { public static RealClass GenerateRealClassProxy() { var generator = new ProxyGenerator(); var code = generator.GenerateProxyCode(typeof(RealClass), (sb, method) => { }, (sb, method) => { sb.Append("r++;"); }); var jit = new Jit(); var syntaxTree = jit.ParseToSyntaxTree(code); var compilation = jit.BuildCompilation(syntaxTree); var assembly = jit.ComplieToAssembly(compilation); return Activator.CreateInstance(assembly.GetTypes().First()) as RealClass; } public static void Test() { RealClass proxy = GenerateRealClassProxy(); var i = 5; var j = 10; Console.WriteLine($"{i} + {j} = {(i + j)}, but proxy is {proxy.Add(i, j)}"); } }复制代码
结果为:
5 + 10 = 15, but proxy is 16复制代码
是的,我们写了这么多代码就是为了让 15 变成 16 ,让别人不知道 多了个 r++; ,这就是AOP的意义
完整的demo 放在 github.com/fs7744/AopD…
2.4 再完善完善就可以了。。。(也就再写个几年)
你只需要完善如下:
- 支持 void 方法
- 支持 async await 方法
- 支持抽象类
- 支持接口
- 支持构造方法
- 支持属性
- 支持索引器
- 支持 in out ref
- 支持泛型
- 支持嵌套类
- 支持剩下的各种各样情况
嗯,相信你自己,你可以的
3. 不推荐在生产环境不经过优化就这样玩,为什么?3.1 两幅图
手写的proxy :
jit 编译proxy:
随着需要编译的Proxy class 增多, cpu 和 内存都会一样增多 所以要使用呢,最好用一些优化过的方案,情况会好的多,比如 github.com/dotnetcore/…
3.2 你信不信得过调用你api的对方
嗯,这是一个信任度的问题,所以不要调用 Roslyn sdk 的输入暴露不做校验,黑客的骚操作是大家永远想不完的
只要对方可信,Roslyn sdk api 别人是不会调用的哦
但是我们都知道前人们都告诉我们有个准则:不要相信任何Input。
所以大家的猫主子会不会跟着别人跑掉就看大家信心和手段了。