前言:
近期項目中需要實作“熱插拔”式的插件程式,例如:定義一個插件接口;由不同開發人員實作具體的插件功能類庫;并最終在應用中調用具體插件功能。
此時需要考慮:插件執行的安全性(隔離運作)和插件可解除安裝更新。說到隔離運作和可解除安裝首先想到的是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