AppDomain实现【插件式】开发

前言:

 近期项目中须要实现“热插拔”式的插件程序,例如:定义一个插件接口;由不一样开发人员实现具体的插件功能类库;并最终在应用中调用具体插件功能。git

 此时须要考虑:插件执行的安全性(隔离运行)和插件可卸载升级。说到隔离运行和可卸载首先想到的是AppDomain。github

 那么AppDomain是什么呢?json

1、AppDomain介绍

 AppDomain是.Net平台里一个很重要的特性,在.Net之前,每一个程序是"封装"在不一样的进程中的,这样致使的结果就造就占用资源大,可复用性低等缺点.而AppDomain在同一个进程内划分出多个"域",一个进程能够运行多个应用,提升了资源的复用性,数据通讯等. 详见
跨域

 CLR在启动的时候会建立系统域(System Domain),共享域(Shared Domain)和默认域(Default Domain),系统域与共享域对于用户是不可见的,默认域也能够说是当前域,它承载了当前应用程序的各种信息(堆栈),因此,咱们的一切操做都是在这个默认域上进行."插件式"开发很大程度上就是依靠AppDomain来进行.安全

 应用程序域具备如下特色:app

  • 必须先将程序集加载到应用程序域中,而后才能执行该程序集。dom

  • 一个应用程序域中的错误不会影响在另外一个应用程序域中运行的其余代码。测试

  • 可以在不中止整个进程的状况下中止单个应用程序并卸载代码。不能卸载单独的程序集或类型,只能卸载整个应用程序域。this

2、基于AppDomain实现“热拔式插件”

 经过AppDomain来实现程序集的卸载,这个思路是很是清晰的。因为在程序设计中,非特殊的须要,咱们都是运行在同一个应用程序域中。spa

 因为程序集的卸载存在上述的缺陷,咱们必需要关闭应用程序域,方可卸载已经装载的程序集。然而主程序域是不能关闭的,所以惟一的办法就是在主程序域中创建一个子程序域,经过它来专门实现程序集的装载。一旦要卸载这些程序集,就只须要卸载该子程序域就能够了,它并不影响主程序域的执行。 

 实现方式以下图:

  

一、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("插件未指定或未加载");
}

 五、测试界面实现:

  建立个测试窗体以下:

3、运行效果

  插件测试基本完成:那么看下运行效果:能够看出当前主程序域中未加载PrintPlugin.dll,而是在子程序集中加载

   

  当咱们更新PrintPlugin.dll逻辑,并更新测试程序加载位置中dll,不会出现不容许覆盖提示;而后先卸载dll在再次加载刚刚dll(模拟插件升级) 

   

 到此已实现插件化的基本实现

4、其余

 固然隔离运行和“插件化”都还有其余实现方式,等着解锁。可是只要搞清楚本质、实现原理、底层逻辑这些都相对简单。因此对越基础的内容越要理解清楚。

参考:

 官网介绍:https://docs.microsoft.com/zh-cn/dotnet/framework/app-domains/application-domains

 示例源码:https://github.com/cwsheng/PluginAppDemo

相关文章
相关标签/搜索