用 Natasha 写个类型调用的架子
一、想法
自上篇文章,我一直琢磨整個好點的例子來展示 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;??第三次遞歸
注意:這里我們用的初始化方法都是針對 Readonly 字段,實際上真實腳本并不是上面那樣,這里只是為了展示代碼邏輯。
這段代碼展示了初始化腳本對注入類型和非注入類型的實例化處理:
if (_injectionMapping.ContainsKey(type)) {//對注入的接口/抽象類等類型使用已實現的類型進行實例化ctorBuilder.Append($"var?{instance}?=?new?{_injectionMapping[type].GetDevelopName()}();"); }else {//對非注入類型直接?newctorBuilder.Append($"var {instance} = new {type.GetDevelopName()}();"); }下面這段代碼遍歷了當前類的只讀字段,如果遇到了只讀字段則發生以下處理:
觸發遞歸,繼續生成實例化代碼。
給只讀字段賦值
HelloRpc 為路由和方法的載體,繼承 BaseRpc 以便被掃描
下面便是對參數轉換和方法調用的處理,需要反射的技術,如果還不清楚,可以參考我們公眾號【 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
長按左側二維碼關注
歡迎打賞組織
給予我們更多的支持
總結
以上是生活随笔為你收集整理的用 Natasha 写个类型调用的架子的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET Core 实现基于Websoc
- 下一篇: .Net Core in Docker极