在C#中使用AppDomain實現【插件式】開發


前言:

 近期項目中需要實現“熱插拔”式的插件程序,例如:定義一個插件接口;由不同開發人員實現具體的插件功能類庫;並最終在應用中調用具體插件功能。

 此時需要考慮:插件執行的安全性(隔離運行)和插件可卸載升級。說到隔離運行和可卸載首先想到的是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

 

 

出處:https://www.cnblogs.com/cwsheng/p/14616685.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM