ASPNETCORE6.0開發插件系統

 上一次簡單分析了目前公司後臺任務存在的一些問題以及解決方案,主要是一些概念,沒有涉及到任何技術實現,最近花了一些時間,看了部分aspnetcore6的源碼,結合自己的理解,寫了一個簡單插件系統,那麼今天就來簡單聊聊技術上的具體實現吧。
 
在介紹插件服務實現之前,我們先了解一下,.Net這個大一統全平臺的整體架構佈局,爲什麼要了解這個呢?因爲我們有很多早期Framework版本的後臺任務,如461、462、472...,而且這些後臺任務需要平移到插件系統裏面,這就會涉及到版本的兼容問題,所以我們對底層需要有一定的知識儲備,如圖
上圖就是我對.Net整體結構佈局的一個簡單理解,2020年之前.Netcore在BCL這層定義了Corefx,之後微軟把它整合到了Runtime裏面。在全新的.Net全平臺裏面,.Netframework任然是這個全平臺上面的三大分支之一,沒辦法因爲Winforms和Wpf這兩個GUI框架還需要支持,而且以微軟目前的操作,也沒有跨平臺的可能,所以這個分支應該會持續更新下去。往下就是標準庫.Net standard,.NET Standard 是針對多個 .NET 實現推出的一套正式的 .NET API 規範。 推出 .NET Standard 的背後動機是要提高 .NET 生態系統中的一致性。簡單來說它是Api接口規範,並且在BCL層提供了多平臺複用(程序集級別的),給上層三大平臺提供了強大的複用能力,這裏有個細節需要注意,.Netstandard2.1不再支持.Netframework。再往下就是Runtime運行時了,包含了不同OS下的vm虛擬機,jit編譯器...。Clrvm虛擬機其實也是一個運行在操作系統OS之上的一個程序,微軟針對不同os實現了不同的虛擬機,虛擬機分兩種,VMware類的完整指令集架構的虛擬機,另一種就是虛擬指令集架構的虛擬機,如我們的coreclr、jvm等,這個vm程序有點特別,它能給我們的.net程序提供運行時環境,也就是說我們編寫的應用程序是跑在它這個大容器裏面的。 Runtime運行時的能力,遠不止我上面提到的這些,有興趣的朋友可以自己去研究,題外話,其實在.Netframework時代,微軟要實現跨平臺能力其實還是蠻簡單的,只需要實現不同平臺的vm虛擬機,就連jit編譯器大部分能力都能複用,可氣的是微軟在.Netframework誕生起就遵循了跨平臺規範CLI,也設計了中間語言IL,是格局小了還是太過於自信了?有了以上對全平臺.Net簡單的認知,接下來我們通過一個簡單實例,具體看看微軟是如何做到多版本兼容的。
 
我們簡單創建兩個工程,一個core6.0的控制檯,一個是framwork4.0的lib,通過ildasm打開app。
 
core6.0 // core工程直接打印在frmework4.0裏面定義的list程序集名稱
Console.WriteLine(ClassLibrary1.Class1.getAssemblyName());
Console.ReadLine();
 
framework4.0 lib  // 定義
public static string getAssemblyName() { return typeof(List<string>).Assembly.FullName; }
 
console // 打印結果
System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
 
以上代碼非常簡單,也做了簡單說明,看到這個輸出結果,是不是比較疑惑?這個System.Private.CoreLib程序集是被定義在Runtime的Coreclr裏面的,理論上來說.Netframework下的List應該是被定義在一個叫mscorlib的程序集裏面,帶着這個疑問,我們通過ildasm對這個程序集看個究竟。
 
.assembly extern System.Runtime
{
  .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )                         // .?_....:
  .ver 6:0:0:0
}
.assembly extern ClassLibrary1
{
  .ver 1:0:0:0
}
.assembly extern System.Console
{
  .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )                         // .?_....:
  .ver 6:0:0:0
}
 
好傢伙,System.Runtime在作怪,接着看看它的真面目,
 
// Metadata version: v4.0.30319
.assembly noplatform System.Runtime
 
不好意思,打開了引用程序集,這是一種特殊類型的程序集,簡單理解爲api就行了,接下來我們打開具體的實現程序集,理論上來說它也只是一個共享程序集。
 
// Metadata version: v4.0.30319
.assembly extern System.Private.CoreLib
{
  .publickeytoken = (7C EC 85 D7 BE A7 79 8E )                         // |.....y.
  .ver 6:0:0:0
}
.assembly extern System.Private.Uri
{
  .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )                         // .?_....:
  .ver 6:0:0:0
}
.assembly System.Runtime
 
好傢伙,引用了System.Private.CoreLib、System.Private.Uri 兩個程序集,這兩程序集都是.Netcore上的私有核心程序集,源碼在Coreclr裏面,這是啥情況?難道我們.Netframework的程序集通過某種手段被轉移到了當前運行時.Netcore?接着往下看這份代碼。
 
.class extern forwarder System.Collections.Generic.IEnumerator`1
{
  .assembly extern System.Private.CoreLib
}
.class extern forwarder System.Collections.Generic.IEqualityComparer`1
{
  .assembly extern System.Private.CoreLib
}
.class extern forwarder System.Collections.Generic.IList`1
{
  .assembly extern System.Private.CoreLib
}
 
看到這氣人不?繞這麼大圈子,最終通過forwarder技術轉移到了CoreLib程序集,也就完成了我們的版本兼容問題,到此第一個問題集成老版本的後臺任務理論有了支撐,接下來具體看看技術上的實現。
 
先簡單介紹下整個技術棧吧,Aspnetcoremvc6.0、Linq2db、Mysql、Eventbus、Autofac、Hangfire...,技術棧大概就是這些,沒有啥特別的,這裏推薦一下ORM框架Linq2db,這個框架我也是第一次用,給我的感覺就是簡單、開發效率高,整個插件系統目前實現的功能比較簡單,支持ALC程序集隔離,插件和Host之間通信,插件Page路由,熱插拔,插件管理(上傳、安裝、卸載、刪除等等),子插件版本支持461-6.0,標準版全支持。我們通過一張簡圖,看看整個解決方案結構,下圖就是插件服務工程結構圖。
 
 
 
簡單說明一下上圖的目錄結構,Plugins文件夾裏面包含的是一些插件,裏面寫了幾個不同版本庫的Demo。Eventbus工程是一個極簡的事件總線。Abstraction是插件抽象出來的接口,所有插件必須要引用該程序集,並且實現IPlugin接口。Core裏面主要是一些核心實現,包括Assemblyloadcontext上下文,加載卸載,插件管理等。Data是對Linq2db訪問Mysql的簡單封裝。Services工程裏面包含的就是具體業務服務實現。Web就是UI層,標準的Aspnetcoremvc工程。
 
Plugins文件夾裏面的插件必須包含一個自描述文件plugin.json,同時需要實現有如下定義的插件接口IPlugin。
 
public interface IPlugin
    {
        void Register(CancellationToken cancellationToken,params object[] args);
        void Configure(IApplicationBuilder applicationBuilder);
        void Stop(CancellationToken cancellationToken);
    }
 
以上就是IPlugin接口的全貌,定義了三個方法,分別爲註冊,停止,配置,這裏簡單提一下注冊,因爲我這邊實現主要是後臺定時任務,所以在Host主程序集會通過Hangfire定時任務來管理這些插件任務,這也就有了register方法。配置主要是提供PageMap路由,相對來說比較簡單。插件有了,接下來我們需要把它動態加載到.Netcore運行時環境,並通過Host主程對其進行管理,也就是程序集級別的隔離,原來我們基於.Netframework對程序集的加載並隔離,我們可以通過Appdomain這麼一個程序集來管理,在.Net6平臺上面這個對象基本被棄用了,那麼在.Net6裏面運行時程序集加載隔離是怎麼做呢?一個全新的上下文對象Assemblyloadcontext。Assemblyloadcontext簡稱alc,默認情況下,在當前主程運行時環境下面,會創建一個名叫default的alc(後續我直接簡稱),我們的Host主程序集裏面的所有引用和依賴程序集會在Runtime初始化階段完成加載。alc有個限制,單個 alc實例限制爲每個簡單程序集名稱 AssemblyName.Name 只加載 Assembly 的一個版本,當動態加載程序集模塊時,此限制可能會有一個問題,版本衝突。如插件A和B分別引用了三方兩個不通版本的程序集,那怎麼解決?好在微軟幫我們實現了同一個運行時環境支持多個alc管理上下文,這樣我們就可以通過創建自定義alc上下文來完成我們的插件加載問題。
 
private Assembly LoadPluginAssembly(Func<PluginConfig> action, string pluginId)
        {
            // 其他代碼
            var pluginConfig = action();  // 插件配置
            var guid = Guid.NewGuid().ToString("N");
            var contextName = $"{pluginConfig.MainPluginName}-{guid}";
            var context = new LoaderContext(pluginConfig.BasePluginPath, pluginConfig.MainPluginlyPath, contextName, _defaultLoadContext);  // 創建插件上下文實列
            if (pluginConfig.SharedAssemblies.Any())
                foreach (var sharedAssmbley in pluginConfig.SharedAssemblies) // 加載共享程序集
                    context.LoadSharedAssembly(sharedAssmbley);
            var pluginAssembly = context.LoadAssemblyFromFilePath(pluginConfig.MainPluginlyPath);  // 加載插件程序集
            _contexts.TryAdd(pluginId, context); // 添加到字典
            return pluginAssembly;
        }
 
以上代碼就是插件程序集加載的主邏輯,做了相應註釋,基於aspnetcore6簡單的插件系統大概就完成了。上面註釋裏面提到了共享程序集,共享程序集其實就是把整個插件系統需要用到的程序集加載到Default的alc上下文裏面,給整個插件系統環境提供程序集共享。如插件與Host主程序集,插件與插件之間需要交互,這裏面就會涉及到一個問題,同類型轉換的問題。在插件初始化階段,host主程序集裏面需要創建插件的實列並轉換爲IPlugin接口,如果IPlugin接口所在的程序集被每個插件的alc加載了(包括默認alc也加載了自己的Plugin程序集),那麼在Host主程序集裏面是無法完成IPlugin和插件之間的轉換,在alc看來他們不是同一個類型。有了共享程序集,那麼共享程序集是怎麼提供服務的呢?插件服務首先會通過assemblyName在自己的alc上下文加載,如果沒有,此時會從default的alc上下文裏面加載,如果有直接返回,沒有則返回null。
 
整個插件系統的技術實現大概就寫這麼多吧,功能可能有點少,畢竟我們目前的需求也就是實現後臺任務的統一部署管理,後期有時間可能會考慮把插件功能完善,如插件支持獨立的web站點,支持完整的路由系統,支持view視圖的預編譯和運行時編譯等等。其實實現這些功能都不難,有興趣的朋友研究一下aspnetcore6的源碼,aspnetcore6框架整個view視圖編譯這塊還是比較複雜的,同時它還支持兩種編譯,所以需要藉助源碼,找到視圖加載、更新、編譯的機制,找到合適的切入點,好了就聊這麼多吧。
 
 
 
 
 
 
 
 
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章