曾經也實現過.Net Framework 基于AppDomain 的 dll庫熱插拔,經歷了版本的迭代,.Net Core 不支持 AppDomain,之前也搞過.Net Core 3.1 版本的,現在搞一下子.NET 6.0的。
熱插拔運用的場景
主要運用到宿主與插件這個場景或者動態任務的場景上(假設你現在業務服務已經運行,但是,需要新增加新的業務功能,就可以用這種方式)。
就像Office 或者 Visual Studio 一樣,它們都是集插件架構之大成者。
邏輯實現
主要是根據 AssemblyLoadContext 這個系統提供的API來實現的,已經實現了對DLL程序集的加載和卸載。
之前AppDomain是通過程序域(隔離的環境)的概念進行隔離的,而 AssemblyLoadContext 的話,提供了程序集加載隔離,它允許在單個進程中加載同一程序集的多個版本。
它替換.NET Framework中多個AppDomain實例提供的隔離機制,其中AssemblyLoadContext.Default 表示運行時的默認上下文,該上下文用于應用程序主程序集及其靜態依賴項,那么,其他的上下文,就是插件DLL的上下文了。
從概念上講,加載上下文會創建一個用于加載、解析和可能卸載一組程序集的范圍。
這里就根據 AssemblyLoadContext 加載,卸載,來實現熱插播邏輯的實現.
實現邏輯
主要的邏輯是這個邏輯
///?<summary>///?dll文件的加載///?</summary>public?class?LoadDll{///?<summary>///?任務實體///?</summary>public?ITask?_task;public?Thread?_thread;///?<summary>///?核心程序集加載///?</summary>public?AssemblyLoadContext?_AssemblyLoadContext?{?get;?set;?}///?<summary>///?獲取程序集///?</summary>public?Assembly?_Assembly?{?get;?set;?}///?<summary>///?文件地址///?</summary>public?string?filepath?=?string.Empty;///?<summary>///?指定位置的插件庫集合///?</summary>AssemblyDependencyResolver?resolver?{?get;?set;?}public?bool?LoadFile(string?filepath){this.filepath?=?filepath;try{resolver?=?new?AssemblyDependencyResolver(filepath);_AssemblyLoadContext?=?new?AssemblyLoadContext(Guid.NewGuid().ToString("N"),?true);_AssemblyLoadContext.Resolving?+=?_AssemblyLoadContext_Resolving;using?(var?fs?=?new?FileStream(filepath,?FileMode.Open,?FileAccess.Read)){var?_Assembly?=?_AssemblyLoadContext.LoadFromStream(fs);var?Modules?=?_Assembly.Modules;foreach?(var?item?in?_Assembly.GetTypes()){if?(item.GetInterface("ITask")?!=?null){_task?=?(ITask)Activator.CreateInstance(item);break;}}return?true;}}catch?(Exception?ex)?{?Console.WriteLine($"LoadFile:{ex.Message}");?};return?false;}private?Assembly?_AssemblyLoadContext_Resolving(AssemblyLoadContext?arg1,?AssemblyName?arg2){Console.WriteLine($"加載{arg2.Name}");var?path?=?resolver.ResolveAssemblyToPath(arg2);if?(!string.IsNullOrEmpty(path)){using?(var?fs?=?new?FileStream(path,?FileMode.Open,?FileAccess.Read)){return?_AssemblyLoadContext.LoadFromStream(fs);}}return?null;}public?bool?StartTask(){bool?RunState?=?false;try{if?(_task?!=?null){_thread?=?new?Thread(new?ThreadStart(_Run));_thread.IsBackground?=?true;_thread.Start();RunState?=?true;}}catch?(Exception?ex)?{?Console.WriteLine($"StartTask:{ex.Message}");?};return?RunState;}private?void?_Run(){try{_task.Run();}catch?(Exception?ex)?{?Console.WriteLine($"_Run?任務中斷執行:{ex.Message}");?};}public?bool?UnLoad(){try{_thread?.Interrupt();}catch?(Exception?ex){?Console.WriteLine($"UnLoad:{ex.Message}");}finally{_thread?=?null;}_task?=?null;try{_AssemblyLoadContext?.Unload();}catch?(Exception){?}finally{_AssemblyLoadContext?=?null;GC.Collect();GC.WaitForPendingFinalizers();}return?true;}}
以上就是這個熱插拔的核心邏輯了。
ITask.cs
這個接口實現簡單,只有一個方法,當然,如果有需要,可以擴展一下。
///?<summary>///?任務接口///?</summary>public?interface?ITask{///?<summary>///?任務的運行方法///?</summary>///?<returns></returns>void?Run();}
插件庫1 PrintStrLib
插件的代碼就很簡單了
public?class?PrintStr?:?ITask{public?void?Run(){int?a?=?0;while?(true){Console.WriteLine($"PrintStr:{a}");a++;Thread.Sleep(1?*?1000);}}}
插件庫2 PrintDateLib
插件的代碼就很簡單了
public?class?PrintDate?:?ITask{public?void?Run(){while?(true){Console.WriteLine($"PrintDate:{DateTime.Now}");Thread.Sleep(1?*?1000);}}}
測試運行
使用也很簡單,加載程序集,然后,執行,間隔指定時間后,順序卸載。
static?void?Main(string[]?args)
{Console.Title?=?"AssemblyLoadContext?Dll熱插拔?測試?by?藍總創精英團隊";var?list?=?new?List<LoadDll>();Console.WriteLine("開始加載DLL");list.Add(Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"DLL",?"PrintDateLib.dll")));list.Add(Load(Path.Combine(AppDomain.CurrentDomain.BaseDirectory,?"DLL",?"PrintStrLib.dll")));foreach?(var?item?in?list){item.StartTask();}Console.WriteLine("開啟了任務!");SpinWait.SpinUntil(()?=>?false,?5?*?1000);foreach?(var?item?in?list){var?s?=?item.UnLoad();SpinWait.SpinUntil(()?=>?false,?2?*?1000);Console.WriteLine($"任務卸載:{s}");}Console.WriteLine("熱插拔插件任務?測試完畢");Console.ReadLine();
}
public?static?LoadDll?Load(string?filePath)
{var?load?=?new?LoadDll();load.LoadFile(filePath);return?load;
}
效果查看
從下圖來看,我們想要的結果都有了,加載兩個插件,插件執行自己的業務,然后,順序一個一個的卸載掉,確實已經不在執行它自己的業務了。

總結
實際上.Net的程序集的隔離問題很多,這種隔離方式實際用的過程中,如果程序集簡單還好,復雜的話,可能會有別的問題。
我非常喜歡的隔離方式就像谷歌游覽器那樣的插件方式或者IIS那樣的容器級隔離,不過,這種實際上我分析是進程級隔離方案,現在也流行docker,系統級隔離。
只能說存在即合理吧,有它存在的價值。
代碼地址
https://github.com/kesshei/AssemblyLoadContextDemo.git
https://gitee.com/kesshei/AssemblyLoadContextDemo.git
參考文檔
https://docs.microsoft.com/zh-cn/dotnet/api/system.runtime.loader.assemblyloadcontext?view=net-6.0
閱
一鍵三連呦!,感謝大佬的支持,您的支持就是我的動力!