一、想法
自上篇文章,我一直琢磨整个好点的例子来展示 Natasha 动态编程能力, 于是就写了一个简单的类型调用的架子,耗时40分钟左右, 项目地址:https://github.com/NMSAzulX/TypeCaller
二、功能特点
a)、简单的注入功能
支持无参构造注入
支持递归注入
保证原生性能
b)、方法映射与扫描
仅扫描当前程序集
扫描继承自 BaseRpc 类的 Rpc 路由
使用类名+方法名与动态委托形成映射
在当前生命周期使用静态 AOP 类包裹方法
c)、对接与调用
使用 params object[] 来对接调用参数
使用 object 作为返回值
三、功能点解析
使用代码预览:
在请求达到后,Caller 根据路由执行委托,在委托中:
1、程序首先 new 了一个 RPC 实例
2、紧接着初始化实例中注入的接口
3、然后实例调用了路由中指向的方法(下文中 RPC 实例统称为”路由“)
4、最后返回结果。
注入(初始化)实现
由于需要有递归的构建,因此需要 instanceName 来记录上一次递归出来的对象名。而上一次构建的代码段则放在 script 中。
public static (StringBuilder script, string instanceName) HandlerCtor(Type type,int deepth = 0)
返回值中的 script 为初始化代码,例如:
var instance = new XXXRpc();
instance._xxService = new DefaultXXXSerivce();
由于 service 中可能还有其他 service, 因此需要递归下去:
第二次递归
var instance1 = new DefaultXXXSerivce();
instance1._xxService = new DefaultXXXSerivce2();var instance = new XXXRpc();
instance._xxService = instance1;
第三次递归
var instance2 = new DefaultXXXService3();var instance1 = new DefaultXXXSerivce();
instance1._xxService = new DefaultXXXSerivce2();
instance1._xxService = instance2;var instance = new XXXRpc();
instance._xxService = instance1;
注意:这里我们用的初始化方法都是针对 Readonly 字段,实际上真实脚本并不是上面那样,这里只是为了展示代码逻辑。
这段代码展示了初始化脚本对注入类型和非注入类型的实例化处理:
if (_injectionMapping.ContainsKey(type))
{//对注入的接口/抽象类等类型使用已实现的类型进行实例化ctorBuilder.Append($"var {instance} = new {_injectionMapping[type].GetDevelopName()}();");
}else
{//对非注入类型直接 newctorBuilder.Append($"var {instance} = new {type.GetDevelopName()}();");
}
下面这段代码遍历了当前类的只读字段,如果遇到了只读字段则发生以下处理:
触发递归,继续生成实例化代码。
给只读字段赋值
foreach (var item in fields)
{var result = HandlerCtor(item.FieldType, deepth);if (result != default){ctorBuilder.Insert(0, result.script);var readonnlyFieldScript = $"{instance}.{item.Name}".ReadonlyScript();ctorBuilder.Append($"{readonnlyFieldScript}={result.instanceName};");}
}
HelloRpc 为路由和方法的载体,继承 BaseRpc 以便被扫描
public class HelloRpc : BaseRpc
{private readonly IHelloServices _helloServices;public string GetHello(string name){return _helloServices.GetHello(name);}
}
下面便是对参数转换和方法调用的处理,需要反射的技术,如果还不清楚,可以参考我们公众号【 NCC开源社区 】痴良的反射系列文章;
NDelegate.RandomDomain(item=> { item.LogSyntaxError() //如果语法构建出错,则记录日志 .UseFileCompile(); //将结果编译到 DLL 文件中
})
.SetClass(item=>item.AllowPrivate(type))
.Func<object[], object>(
@$"
//实则这里是个 stringbuilder 来自于对参数的处理
//这里我简化成能看懂的代码
var name = (string)arg[0];{刚才处理的初始化字符串}// AOP Before 调用
if(Aop<{type.GetDevelopName()}>.Before.TryGetValue(""{method.Name}"", out var beforeFunc)){{ beforeFunc(instance,arg); }}//调用 路由Rpc.Method(arg)
var result = instance.{method.Name}({methodParametersScript});// AOP After调用
if(Aop<{type.GetDevelopName()}>.After.TryGetValue(""{method.Name}"", out var afterFunc)){{ afterFunc(instance,arg); }}return result;"
);
准备 Service
DefaultTypeService 类自己就能实例化,如果这个类你懒得在代码里手动写它的实例化,可以通过 FrameworkService.AddInjection<DefaultTypeService>(); 注入进去。
public class DefaultTypeService
{public virtual void Show(){Console.WriteLine("Run : In TypeService! Means : Dependency injection Succeed! ");}
}
DefaultHelloService 实现了 IHelloServices 抽象类或者接口,通过 FrameworkService.AddInjection<IHelloServices, DefaultHelloService>(); 注入进去。
public abstract class IHelloServices{protected readonly DefaultTypeService _typeService;public abstract string GetHello(string name);}
public class DefaultHelloService : IHelloServices
{public override string GetHello(string name){_typeService.Show();System.Console.WriteLine("Run : Contact 'Hello' and {Parameter}!");return "Hello " + name;}
}
用 ILSpy 查看 Natasha 生成的动态映射方法:
运行:xx.exe Hello.GetHello "1"
四、功能扩展
上面的例子有点过于简单,这里我从几个角度来扩展一下,看看 Natasha 还能为它做些什么:
注入功能:
无配置
无区分生命周期
无域隔离
无热拔管理
生命周期以及域隔离与回收,增加了编程的维度,配置可以让注入规则更加多样化。从生命周期的维度来讲,增加该维度可以让对象的创建与回收可控,对作用域有帮助,对提升性能和减小内存开销有一定的好处。域隔离则更是让插件编程放肆起来,结合域回收与创建,我们可以实现在不重启的情况下,更换方法依赖的插件,从而改变执行结果。若使用域隔离的回收,你要搜集关于该域的所有引用,只有移除引用才能回收,从而实现热拔 。
路由映射:
无热拔
未支持插件程序集扫描
热拔同上不细说了,插件程序集扫描可以根据开发者加载的 DLL,扫描符合 BaseRpc 的路由类型,动态编译到路由映射字典中,实现热插。
上下文与调用链:
调用链可以满足中间件的需求,添加认证,静态资源,权限校验,监控等功能模块,另外主链与旁链的处理也是必不可少的功能,这里参照 ASP.NET CORE 的实现即可。
五、性能优化
该示例虽然已经可以满足高性能要求,但比起极致还远远不足。
注入方面
幂等方法注入:某些类的方法满足幂等性,考虑是否可以使用单例对像与其进行映射,从而减少内存开销和对象创建的时间。
按需注入:虽然全网我都没听说过按需注入的功能,但想了一下可以实现,通过空引用异常或反编译我们可以对映射方法进行多次优化编译,从而达到按需注入的功能,例如:在不需要 ServiceA 的方法中,初始化代码则不会对 _aService 字段进行赋值和初始化,可能有人会说如果你检测不出来怎么办,检测不出来也不影响你使用。
注入对象的AOP : 注入的对象可以通过代理方式实现 AOP ,参见下面的代理AOP.
对象池:针对注入字段较多且可池化的对象,可以采用对象池进行存储,当然了对象池用处不仅仅在这里用,其他场景也会用到。
高速分发
真正的 RPC 需要对接网络层,在协议的约束下我们在拿到路由的时候可以以 比特数组 / 字符串等方式作为寻址依据,找到与其映射的方法并调用,高速映射实现的方式有多种,比如 .NET的并发字典,只读并发字典,Trie 结构,我和小曾写的查找树变种等等。
更高效的委托执行
还在调研中,如果你已经了解该技术要点,欢迎贡献,真的感激不尽。
强类型参数
我们例子中的参数使用了 Object 类型,拆装箱肯定是有性能损耗,这点可以从序列化层面去解决,在路由解析完之后,把参数部分的 byte[] span 传入动态映射的方法中,内部对其做强类型的反序列化操作,并直接传给被调用的实例方法做参数。原来的映射方法 Func<object[], object> 变成 Func<byte[], byte[]> 这里没打错,你序列化进来,再序列化出去,如果你有上下文的设计,还可以自定义一些返回值,然后与尾部的序列化方法做对接。
代理AOP
例子中的 AOP 实现是用了静态泛型类加上并发字典 ( Aop<rpc>.After["method"]()),实际上我们可以把 HelloRpc 的 AOP 进行静态类的代理, 比如动态的创建 static class StaticAopHelloRpc ,并把 Before 及 After 方法全部换成和原函数的强类型实现 StaticAopHelloRpc.Beforexxx() ,伪代码例如:
此时 AOP 的方法需要我们手写代码或者动态脚本编译进去,可以用我的 RuntimeToDynamic 库,R2D库可以让运行时的数据压入到动态域中,可以放在静态类、也可以放在普通类中等待调用,而且是强类型。(其实我原本没想把这个库推出来,但实在想不到有比这个更直接的方法了, 这只是一个建议,希望老友们有更好的方法)
代理类合并
代理类合并, 我们可能在动态构建的过程中产生很多的类,这些类在后期可以被整合与优化,减少调用路径。该优化可以先期考虑进去,这关系到你动态构建的一个习惯,如果你的逻辑不是那么强,也可以放在后期去做优化。
选其他组件
如果以上都完成了,性能就优化得差不多了,下面选一些组件,初步打通远程调用:
通信组件: 老江的 SuperSocket 高性能易用,内置了加密和协议解析等。
序列化组件:牛逼哥的 Swifter.Json 。
就此一个模棱两可的 RPC 就差不多能跑了,后续根据反馈或者需求逐一进行优化,使用 Natasha 对请求、调用、返回整个流程进行动态化管理是一件很刺激的事情,甚至需要持久化的支持。当然了 Natasha 还有很多别的用处,比如对象映射,ORM,奇奇怪怪的调用 等等,Natasha 属于 “正向编程” , 即便你没有看相关的源码,也可能写出满足你需求的框架。
六、鸣谢
Natasha 能做到以上那么多离不开黑科技的加持。
感谢 ”天天向上卡索“, 提供了 禁断低版本程序集 的编译标识,借此我开放了 Roslyn 未开放的一些标识与方法,卡索老铁为人低调谦和,名下还有很多有趣的项目,大家多多支持。
感谢 ”牛逼哥“, Readonly 初始化后赋值的方法和委托执行性能提升的信息是由他提供的。 这里我想多说几嘴,此人及其恐怖,6月份拿 Emit 实现的查找算法硬刚我的动态高速缓存,虽然不知道他写了多少代码,但我知道 Natasha 输了1项,就很恐怖,我们群里也有说,Json.net 作者”遭遇“了日本的卡哇伊,日本的卡哇伊”遭遇“了中国的牛逼哥。在看到 Swifter 性能测试结果时真的为他高兴一把,力压群雄,干得漂亮。
七、开源生态
很多情况下性能,易用性,稳定性是一起进步的,因为我们没能做到极致,这时候跟别的语言比反而显得有点急功近利。后浪们要多关注技术,多实践,别总做伸手党,就这些框架分分钟不就支棱起来么。有一部分大佬也是,愿意站在山头磨磨唧唧讲故事,车轱辘话转来转去不挑干货讲,在不就是上来否定这个否定那个,在弱势生态里,都是弱逼,别做生态的局外人,不能置身事外。
反观一下今年上半年,开源项目多起来了,质量也在慢慢提高,不得不说,部分国产库做的要比国外的强得多,这是个很好的趋势!老铁们,每天拿出一点时间来给技术,路上多积累一些灵感,该支棱就得支棱起来,相信自己,能行啊!
如果奇迹有颜色,那一定是中国红!
https://github.com/dotnetcore
打赏一杯酒,削减三分愁。
跟着我们走,脱发包你有。
组织打赏账户为柠檬的账户,请标注「NCC」,并留下您的名字,以下地址可查看收支明细:https://github.com/dotnetcore/Home/blob/master/Statement-of-Income-and-Expense.md
OpenNCC,专注.NET技术的公众号
https://www.dotnetcore.xyz
微信ID:OpenNCC
长按左侧二维码关注
欢迎打赏组织
给予我们更多的支持