近期項目中須要實現「熱插拔」式的插件程序,例如:定義一個插件接口;由不一樣開發人員實現具體的插件功能類庫;並最終在應用中調用具體插件功能。html
此時須要考慮:插件執行的安全性(隔離運行)和插件可卸載升級。說到隔離運行和可卸載首先想到的是AppDomain。git
那麼AppDomain是什麼呢?github
AppDomain是.Net平臺裏一個很重要的特性,在.Net之前,每一個程序是"封裝"在不一樣的進程中的,這樣致使的結果就造就佔用資源大,可複用性低等缺點.而AppDomain在同一個進程內劃分出多個"域",一個進程能夠運行多個應用,提升了資源的複用性,數據通訊等. 詳見
json
CLR在啓動的時候會建立系統域(System Domain),共享域(Shared Domain)和默認域(Default Domain),系統域與共享域對於用戶是不可見的,默認域也能夠說是當前域,它承載了當前應用程序的各種信息(堆棧),因此,咱們的一切操做都是在這個默認域上進行."插件式"開發很大程度上就是依靠AppDomain來進行.跨域
應用程序域具備如下特色:安全
必須先將程序集加載到應用程序域中,而後才能執行該程序集。app
一個應用程序域中的錯誤不會影響在另外一個應用程序域中運行的其餘代碼。dom
可以在不中止整個進程的狀況下中止單個應用程序並卸載代碼。不能卸載單獨的程序集或類型,只能卸載整個應用程序域。測試
經過AppDomain來實現程序集的卸載,這個思路是很是清晰的。因爲在程序設計中,非特殊的須要,咱們都是運行在同一個應用程序域中。this
因爲程序集的卸載存在上述的缺陷,咱們必需要關閉應用程序域,方可卸載已經裝載的程序集。然而主程序域是不能關閉的,所以惟一的辦法就是在主程序域中創建一個子程序域,經過它來專門實現程序集的裝載。一旦要卸載這些程序集,就只須要卸載該子程序域就能夠了,它並不影響主程序域的執行。
實現方式以下圖:
一、AssemblyDynamicLoader類提供建立子程序域和卸載程序域的方法;
二、RemoteLoader類提供裝載程序集、執行接口方法;
三、AssemblyDynamicLoader類得到RemoteLoader類的代理對象,並調用RemoteLoader類的方法;
四、RemoteLoader類的方法在子程序域中完成;
那麼AssemblyDynamicLoader 和 RemoteLoader 如何實現呢?
一、首先定義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類進行跨域通訊,則必須提供本身的跨域訪問器.
二、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(); } } }
三、插件接口和實現:
插件接口:
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; } }
四、插件執行:
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("插件未指定或未加載"); }
五、測試界面實現:
建立個測試窗體以下:
插件測試基本完成:那麼看下運行效果:能夠看出當前主程序域中未加載PrintPlugin.dll,而是在子程序集中加載
當咱們更新PrintPlugin.dll邏輯,並更新測試程序加載位置中dll,不會出現不容許覆蓋提示;而後先卸載dll在再次加載剛剛dll(模擬插件升級)
到此已實現插件化的基本實現
固然隔離運行和「插件化」都還有其餘實現方式,等着解鎖。可是隻要搞清楚本質、實現原理、底層邏輯這些都相對簡單。因此對越基礎的內容越要理解清楚。
官網介紹:https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/application-domains
示例源碼:https://github.com/cwsheng/PluginAppDemo