用 Natasha 写个类型调用的架子

一、想法

自上篇文章,我一直琢磨整个好点的例子来展示 Natasha 动态编程能力, 于是就写了一个简单的类型调用的架子,耗时40分钟左右,  项目地址:https://github.com/NMSAzulX/TypeCaller

二、功能特点

a)、简单的注入功能

  • 支持无参构造注入

  • 支持递归注入

  • 保证原生性能

b)、方法映射与扫描

  • 仅扫描当前程序集

  • 扫描继承自 BaseRpc 类的 Rpc 路由

  • 使用类名+方法名与动态委托形成映射

  • 在当前生命周期使用静态 AOP 类包裹方法

c)、对接与调用    

  • 使用 params object[] 来对接调用参数

  • 使用 object 作为返回值

三、功能点解析

使用代码预览:

用 Natasha 写个类型调用的架子_第1张图片

在请求达到后,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
{
  //对非注入类型直接 new
  ctorBuilder.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(
@$"
//实则这里是个 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();  注入进去。

public class DefaultTypeService
{
    public virtual void Show()
    {
       Console.WriteLine("Run : In TypeService! Means : Dependency injection Succeed! ");
    }
}

DefaultHelloService 实现了 IHelloServices 抽象类或者接口,通过 FrameworkService.AddInjection(); 注入进去。

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 生成的动态映射方法:

用 Natasha 写个类型调用的架子_第2张图片

运行:xx.exe Hello.GetHello  "1"

用 Natasha 写个类型调用的架子_第3张图片

四、功能扩展

上面的例子有点过于简单,这里我从几个角度来扩展一下,看看 Natasha 还能为它做些什么:

注入功能:

  • 无配置

  • 无区分生命周期

  • 无域隔离

  • 无热拔管理

生命周期以及域隔离与回收,增加了编程的维度,配置可以让注入规则更加多样化。从生命周期的维度来讲,增加该维度可以让对象的创建与回收可控,对作用域有帮助,对提升性能和减小内存开销有一定的好处。域隔离则更是让插件编程放肆起来,结合域回收与创建,我们可以实现在不重启的情况下,更换方法依赖的插件,从而改变执行结果。若使用域隔离的回收,你要搜集关于该域的所有引用,只有移除引用才能回收,从而实现热拔 。

路由映射:

  • 无热拔

  • 未支持插件程序集扫描

热拔同上不细说了,插件程序集扫描可以根据开发者加载的 DLL,扫描符合 BaseRpc 的路由类型,动态编译到路由映射字典中,实现热插。

上下文与调用链:

调用链可以满足中间件的需求,添加认证,静态资源,权限校验,监控等功能模块,另外主链与旁链的处理也是必不可少的功能,这里参照 ASP.NET CORE 的实现即可。

五、性能优化

该示例虽然已经可以满足高性能要求,但比起极致还远远不足。

注入方面

        幂等方法注入:某些类的方法满足幂等性,考虑是否可以使用单例对像与其进行映射,从而减少内存开销和对象创建的时间。 

        按需注入:虽然全网我都没听说过按需注入的功能,但想了一下可以实现,通过空引用异常或反编译我们可以对映射方法进行多次优化编译,从而达到按需注入的功能,例如:在不需要 ServiceA 的方法中,初始化代码则不会对 _aService 字段进行赋值和初始化,可能有人会说如果你检测不出来怎么办,检测不出来也不影响你使用。

        注入对象的AOP :  注入的对象可以通过代理方式实现 AOP ,参见下面的代理AOP.

        对象池:针对注入字段较多且可池化的对象,可以采用对象池进行存储,当然了对象池用处不仅仅在这里用,其他场景也会用到。

高速分发

真正的 RPC 需要对接网络层,在协议的约束下我们在拿到路由的时候可以以 比特数组 / 字符串等方式作为寻址依据,找到与其映射的方法并调用,高速映射实现的方式有多种,比如 .NET的并发字典,只读并发字典,Trie 结构,我和小曾写的查找树变种等等。

更高效的委托执行

还在调研中,如果你已经了解该技术要点,欢迎贡献,真的感激不尽。

强类型参数

我们例子中的参数使用了 Object 类型,拆装箱肯定是有性能损耗,这点可以从序列化层面去解决,在路由解析完之后,把参数部分的 byte[] span 传入动态映射的方法中,内部对其做强类型的反序列化操作,并直接传给被调用的实例方法做参数。原来的映射方法 Func 变成 Func 这里没打错,你序列化进来,再序列化出去,如果你有上下文的设计,还可以自定义一些返回值,然后与尾部的序列化方法做对接。

代理AOP

例子中的 AOP 实现是用了静态泛型类加上并发字典 ( Aop.After["method"]()),实际上我们可以把 HelloRpc 的 AOP 进行静态类的代理, 比如动态的创建 static class StaticAopHelloRpc ,并把 Before 及 After 方法全部换成和原函数的强类型实现 StaticAopHelloRpc.Beforexxx() ,伪代码例如:

用 Natasha 写个类型调用的架子_第4张图片

此时 AOP 的方法需要我们手写代码或者动态脚本编译进去,可以用我的 RuntimeToDynamic 库,R2D库可以让运行时的数据压入到动态域中,可以放在静态类、也可以放在普通类中等待调用,而且是强类型。(其实我原本没想把这个库推出来,但实在想不到有比这个更直接的方法了, 这只是一个建议,希望老友们有更好的方法)

代理类合并

代理类合并, 我们可能在动态构建的过程中产生很多的类,这些类在后期可以被整合与优化,减少调用路径。该优化可以先期考虑进去,这关系到你动态构建的一个习惯,如果你的逻辑不是那么强,也可以放在后期去做优化。

选其他组件

如果以上都完成了,性能就优化得差不多了,下面选一些组件,初步打通远程调用:

通信组件: 老江的 SuperSocket 高性能易用,内置了加密和协议解析等。

序列化组件:牛逼哥的 Swifter.Json

就此一个模棱两可的 RPC 就差不多能跑了,后续根据反馈或者需求逐一进行优化,使用 Natasha 对请求、调用、返回整个流程进行动态化管理是一件很刺激的事情,甚至需要持久化的支持。当然了 Natasha 还有很多别的用处,比如对象映射,ORM,奇奇怪怪的调用 等等,Natasha 属于 “正向编程” , 即便你没有看相关的源码,也可能写出满足你需求的框架。

六、鸣谢

Natasha 能做到以上那么多离不开黑科技的加持。

谢 ”天天向上卡索“, 提供了 禁断低版本程序集 的编译标识,借此我开放了 Roslyn 未开放的一些标识与方法,卡索老铁为人低调谦和,名下还有很多有趣的项目,大家多多支持。

感谢 ”牛逼哥“, Readonly 初始化后赋值的方法和委托执行性能提升的信息是由他提供的。 这里我想多说几嘴,此人及其恐怖,6月份拿 Emit 实现的查找算法硬刚我的动态高速缓存,虽然不知道他写了多少代码,但我知道 Natasha 输了1项,就很恐怖,我们群里也有说,Json.net 作者”遭遇“了日本的卡哇伊,日本的卡哇伊”遭遇了中国的牛逼哥。在看到 Swifter 性能测试结果时真的为他高兴一把,力压群雄,干得漂亮。

七、开源生态

很多情况下性能,易用性,稳定性是一起进步的,因为我们没能做到极致,这时候跟别的语言比反而显得有点急功近利。后浪们要多关注技术,多实践,别总做伸手党,就这些框架分分钟不就支棱起来么。有一部分大佬也是,愿意站在山头磨磨唧唧讲故事,车轱辘话转来转去不挑干货讲,在不就是上来否定这个否定那个,在弱势生态里,都是弱逼,别做生态的局外人,不能置身事外。

反观一下今年上半年,开源项目多起来了,质量也在慢慢提高,不得不说,部分国产库做的要比国外的强得多,这是个很好的趋势!老铁们,每天拿出一点时间来给技术,路上多积累一些灵感,该支棱就得支棱起来,相信自己,能行啊!  

如果奇迹有颜色,那一定是中国红!

用 Natasha 写个类型调用的架子_第5张图片


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

长按左侧二维码关注

欢迎打赏组织

给予我们更多的支持

你可能感兴趣的:(java,aop,kubernetes,javascript,反射)