前言:
近期專案中需要實現“熱插拔”式的外掛程式,例如:定義一個外掛介面;由不同開發人員實現具體的外掛功能類庫;並最終在應用中呼叫具體外掛功能。
此時需要考慮:外掛執行的安全性(隔離執行)和外掛可解除安裝升級。說到隔離執行和可解除安裝首先想到的是AppDomain。
那麼AppDomain是什麼呢?
一、AppDomain介紹
AppDomain是.Net平臺裡一個很重要的特性,在.Net以前,每個程式是"封裝"在不同的程式中的,這樣導致的結果就造就佔用資源大,可複用性低等缺點.而AppDomain在同一個程式內劃分出多個"域",一個程式可以執行多個應用,提高了資源的複用性,資料通訊等. 詳見
CLR在啟動的時候會建立系統域(System Domain),共享域(Shared Domain)和預設域(Default Domain),系統域與共享域對於使用者是不可見的,預設域也可以說是當前域,它承載了當前應用程式的各類資訊(堆疊),所以,我們的一切操作都是在這個預設域上進行."外掛式"開發很大程度上就是依靠AppDomain來進行.
應用程式域具有以下特點:
-
必須先將程式集載入到應用程式域中,然後才能執行該程式集。
-
一個應用程式域中的錯誤不會影響在另一個應用程式域中執行的其他程式碼。
-
能夠在不停止整個程式的情況下停止單個應用程式並解除安裝程式碼。不能解除安裝單獨的程式集或型別,只能解除安裝整個應用程式域。
二、基於AppDomain實現“熱拔式外掛”
通過AppDomain來實現程式集的解除安裝,這個思路是非常清晰的。由於在程式設計中,非特殊的需要,我們都是執行在同一個應用程式域中。
由於程式集的解除安裝存在上述的缺陷,我們必須要關閉應用程式域,方可解除安裝已經裝載的程式集。然而主程式域是不能關閉的,因此唯一的辦法就是在主程式域中建立一個子程式域,通過它來專門實現程式集的裝載。一旦要解除安裝這些程式集,就只需要解除安裝該子程式域就可以了,它並不影響主程式域的執行。
實現方式如下圖:
1、AssemblyDynamicLoader類提供建立子程式域和解除安裝程式域的方法;
2、RemoteLoader類提供裝載程式集、執行介面方法;
3、AssemblyDynamicLoader類獲得RemoteLoader類的代理物件,並呼叫RemoteLoader類的方法;
4、RemoteLoader類的方法在子程式域中完成;
那麼AssemblyDynamicLoader 和 RemoteLoader 如何實現呢?
1、首先定義RemoteLoader用於載入外掛程式集,並提供外掛介面執行方法
public class RemoteLoader : MarshalByRefObject { private Assembly _assembly; public void LoadAssembly(string assemblyFile) { _assembly = Assembly.LoadFrom(assemblyFile); } public T GetInstance<T>(string typeName) where T : class { if (_assembly == null) return null; var type = _assembly.GetType(typeName); if (type == null) return null; return Activator.CreateInstance(type) as T; } public object ExecuteMothod(string typeName, string args) { if (_assembly == null) return null; var type = _assembly.GetType(typeName); var obj = Activator.CreateInstance(type); if (obj is IPlugin) { return (obj as IPlugin).Exec(args); } return null; } }
由於每個AppDomain都有自己的堆疊,記憶體塊,也就是說它們之間的資料並非共享了.若想共享資料,則涉及到應用程式域之間的通訊.C#提供了MarshalByRefObject類進行跨域通訊,則必須提供自己的跨域訪問器.
2、AssemblyDynamicLoader 主要用於管理應用程式域建立和解除安裝;並建立RemoteLoader物件
using System; using System.IO; using System.Reflection; namespace PluginRunner { public class AssemblyDynamicLoader { private AppDomain appDomain; private RemoteLoader remoteLoader; public AssemblyDynamicLoader(string pluginName) { AppDomainSetup setup = new AppDomainSetup(); setup.ApplicationName = "app_" + pluginName; setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; setup.PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins"); setup.CachePath = setup.ApplicationBase; setup.ShadowCopyFiles = "true"; setup.ShadowCopyDirectories = setup.ApplicationBase; AppDomain.CurrentDomain.SetShadowCopyFiles(); this.appDomain = AppDomain.CreateDomain("app_" + pluginName, null, setup); String name = Assembly.GetExecutingAssembly().GetName().FullName; this.remoteLoader = (RemoteLoader)this.appDomain.CreateInstanceAndUnwrap(name, typeof(RemoteLoader).FullName); } /// <summary> /// 載入程式集 /// </summary> /// <param name="assemblyFile"></param> public void LoadAssembly(string assemblyFile) { remoteLoader.LoadAssembly(assemblyFile); } /// <summary> /// 建立物件例項 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="typeName"></param> /// <returns></returns> public T GetInstance<T>(string typeName) where T : class { if (remoteLoader == null) return null; return remoteLoader.GetInstance<T>(typeName); } /// <summary> /// 執行型別方法 /// </summary> /// <param name="typeName"></param> /// <param name="methodName"></param> /// <returns></returns> public object ExecuteMothod(string typeName, string methodName) { return remoteLoader.ExecuteMothod(typeName, methodName); } /// <summary> /// 解除安裝應用程式域 /// </summary> public void Unload() { try { if (appDomain == null) return; AppDomain.Unload(this.appDomain); this.appDomain = null; this.remoteLoader = null; } catch (CannotUnloadAppDomainException ex) { throw ex; } } public Assembly[] GetAssemblies() { return this.appDomain.GetAssemblies(); } } }
3、外掛介面和實現:
外掛介面:
public interface IPlugin { /// <summary> /// 執行外掛方法 /// </summary> /// <param name="pars">引數json</param> /// <returns>執行結果json串</returns> object Exec(string pars); /// <summary> /// 外掛初始化 /// </summary> /// <returns></returns> bool Init(); }
測試外掛實現:
public class PrintPlugin : IPlugin { public object Exec(string pars) { //v1.0 //return $"列印外掛執行-{pars} 完成"; //v1.1 return $"列印外掛執行-{pars} 完成-更新版本v1.1"; } public bool Init() { return true; } }
4、外掛執行:
string pluginName = txtPluginName.Text; if (!string.IsNullOrEmpty(pluginName) && PluginsList.ContainsKey(pluginName)) { var loader = PluginsList[pluginName]; var strResult = loader.ExecuteMothod("PrintPlugin.PrintPlugin", "Exec")?.ToString(); MessageBox.Show(strResult); } else { MessageBox.Show("外掛未指定或未載入"); }
5、測試介面實現:
建立個測試窗體如下:
三、執行效果
外掛測試基本完成:那麼看下執行效果:可以看出當前主程式域中未載入PrintPlugin.dll,而是在子程式集中載入
當我們更新PrintPlugin.dll邏輯,並更新測試程式載入位置中dll,不會出現不允許覆蓋提示;然後先解除安裝dll在再次載入剛剛dll(模擬外掛升級)
到此已實現外掛化的基本實現
四、其他
當然隔離執行和“外掛化”都還有其他實現方式,等著解鎖。但是隻要搞清楚本質、實現原理、底層邏輯這些都相對簡單。所以對越基礎的內容越要理解清楚。
參考:
官網介紹:https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/application-domains
示例原始碼:https://github.com/cwsheng/PluginAppDemo