c# 支持热插拔的插件

场景:

  这项目用到了插件化开发,不是我做的,趁着现在有空学习一下。插件就是dll,主程序可以调用dll中的方法,插件之前没有关系,耦合性低。同时便于扩展和移除。今天在家,就研究一下c#的插件开发。热插拔,就是可以在运行时进行插件的添加,删除,修改等,无需停止程序。

实现:

  1.插件化

    1.1 首先先定义一个接口:接口中是每个插件都要实现的函数,或者属性。这里我就一个获取插件信息的方法。继承Disposeable是为了移除插件时做的内存释放操作。

public interface IPlugin : IDisposable
    {
        PluginInfo GetPluginInformation();
    }

    PluginInfo类定义如下:

public class PluginInfo
    {
        public string Name { set; get; }

        public string Version { set; get; }

        public string Author { set; get; }

        public DateTime LastTime { set; get; }
    }

    1.2 然后写另一个项目,和这个插件的项目放在同一个解决方法中,作为插件端。内容:

public class Plugin_Chen : IPlugin
    {
        /// <summary>
        /// 获取插件信息
        /// </summary>
        /// <returns></returns>
        public PluginInfo GetPluginInformation()
        {
            return new PluginInfo()
            {
                Author = "Test",
                Name = "测试插件",
                Version = "V1.3.0",
                LastTime = DateTime.Now
            };
        }

        void IDisposable.Dispose()
        {
            Console.WriteLine("释放内存");
        }
    }

    这个插件端的整体架构:

c# 支持热插拔的插件

 

 

     1.3 然后写主程序端,也就是加载和应用插件的程序。首先主程序端要有IPlugin这个接口的定义,如下

c# 支持热插拔的插件

 

 

     1.4 然后在另一个项目的main函数中,做插件的加载和初始化(Init函数)。先从指定文件路径下读取dll文件,再从dll中读取出程序集,指定其中一个type验证是否实现了插件接口,实现了,就可以实例化接口,从而调用接口下的各个方法。

    代码如下:

class Program
    {
        /// <summary>
        /// 当前拥有的插件
        /// </summary>
        static Dictionary<string, IPlugin> _IPlugins = new Dictionary<string, IPlugin>();
        /// <summary>
        /// 当前拥有的插件信息
        /// </summary>
        static Dictionary<string, PluginInfo> _IPluginInfos = new Dictionary<string, PluginInfo>();
        /// <summary>
        /// 文件监听
        /// </summary>
        static FileListenerServer _fileListener = null;
        static void Main(string[] args)
        {
            Console.WriteLine("可插拔插件服务");
            var dic = Directory.GetCurrentDirectory();
            var path = Path.Combine(dic, "plugIn");
            Init(path);
            // 监听文件下插件变化,实现热插拔
            _fileListener = new FileListenerServer(path,ref _IPlugins,ref _IPluginInfos);
            _fileListener.Start();
            Console.WriteLine("按q/Q退出");
            while ( true )
            {
                string input = Console.ReadLine();
                switch ( input )
                {
                    case "q":
                        _fileListener.Stop();
                        return;
                    case "Q":
                        _fileListener.Stop();
                        return;
                    default:
                        Console.WriteLine("按q/Q退出");
                        break;
                }
            }
        }
        /// <summary>
        /// 初始化插件
        /// </summary>
        static void Init(string path)
        {
            Console.WriteLine(string.Format("==========【{0}】==========", "开始加载插件"));
            // 1.获取文件夹下所有dll文件
            DirectoryInfo directoryInfo = new DirectoryInfo(path);
            var dlls = directoryInfo.GetFiles();
            // 2.启动每个dll文件
            for ( int i = 0; i < dlls.Length; i++ )
            {
                // 2.1 获取程序集
                var fileData = File.ReadAllBytes(dlls[i].FullName);
                Assembly asm = Assembly.Load(fileData);
                var manifestModuleName = asm.ManifestModule.ScopeName;
                // 2.2 dll名称
                var classLibrayName = manifestModuleName.Remove(manifestModuleName.LastIndexOf("."), manifestModuleName.Length - manifestModuleName.LastIndexOf("."));
                Type type = asm.GetType("Plugin_Test" + "." + classLibrayName);
                if ( !typeof(IPlugin).IsAssignableFrom(type) )
                {
                    Console.WriteLine("未继承插件接口");
                    continue;
                }
                //dll实例化
                var instance = Activator.CreateInstance(type) as IPlugin;
                var protocolInfo = instance.GetPluginInformation();
                protocolInfo.LastTime = dlls[i].LastWriteTime;
                Console.WriteLine($"插件名称:{protocolInfo.Name}");
                Console.WriteLine($"插件版本:{protocolInfo.Version}");
                Console.WriteLine($"插件作者:{protocolInfo.Author}");
                Console.WriteLine($"插件时间:{protocolInfo.LastTime}");
                _IPlugins.Add(classLibrayName, instance);
                _IPluginInfos.Add(classLibrayName, protocolInfo);
                //释放插件资源
                instance.Dispose();
                instance = null;
            }

            Console.WriteLine(string.Format("==========【{0}】==========", "插件加载完成"));
            Console.WriteLine(string.Format("==========【{0}】==========", "共加载插件{0}个"), _IPlugins.Count);
        }

    }

    注意,建议用var fileData = File.ReadAllBytes(dlls[i].FullName);Assembly asm = Assembly.Load(fileData);来获取程序集,不然用其他方法容易实例化出错。原因还是不清楚。

  2.热插拔

    2.1 这里主要用到了一个文件监控的类:FileSystemWatcher,在这基础上包装了一层。它可以对指定文件夹进行文件/文件夹的添加,删除,修改等操作的监控。代码如下:

public class FileListenerServer
    {
        /// <summary>
        /// 文件监听
        /// </summary>
        private FileSystemWatcher _watcher;
        /// <summary>
        /// 插件
        /// </summary>
        private Dictionary<string, IPlugin> _iPlugin;
        /// <summary>
        /// 插件信息
        /// </summary>
        private Dictionary<string, PluginInfo> _iPluginInfos = new Dictionary<string, PluginInfo>();
        public FileListenerServer(string path,ref Dictionary<string, IPlugin> keyValuePairs,ref Dictionary<string, PluginInfo> keyValues)
        {

            try
            {
                _iPluginInfos = keyValues;
                _iPlugin = keyValuePairs;
                this._watcher = new FileSystemWatcher();
                _watcher.Path = path;
                _watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.DirectoryName;
                //_watcher.IncludeSubdirectories = true;
                _watcher.Created += new FileSystemEventHandler(FileWatcher_Created);
                _watcher.Changed += new FileSystemEventHandler(FileWatcher_Changed);
                _watcher.Deleted += new FileSystemEventHandler(FileWatcher_Deleted);
                _watcher.Renamed += new RenamedEventHandler(FileWatcher_Renamed);
            }

            catch ( Exception ex )
            {
                Console.WriteLine("Error:" + ex.Message);
            }
        }


        public void Start()
        {
            // 开始监听
            this._watcher.EnableRaisingEvents = true;
            Console.WriteLine(string.Format("==========【{0}】==========", "文件监控已经启动..."));

        }

        public void Stop()
        {

            this._watcher.EnableRaisingEvents = false;
            this._watcher.Dispose();
            this._watcher = null;
            Console.WriteLine(string.Format("==========【{0}】==========", "文件监控已经关闭"));
        }
        /// <summary>
        /// 添加插件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void FileWatcher_Created(object sender, FileSystemEventArgs e)
        {
            Console.WriteLine(string.Format("==========【{0}】==========", "添加" + e.Name));
            var dll = new FileInfo(e.FullPath);
            var fileData = File.ReadAllBytes(dll.FullName);
            Assembly asm = Assembly.Load(fileData);
            var manifestModuleName = asm.ManifestModule.ScopeName;
            var classLibrayName = manifestModuleName.Remove(manifestModuleName.LastIndexOf("."), manifestModuleName.Length - manifestModuleName.LastIndexOf("."));
            Type type = asm.GetType("Plugin_Test" + "." + classLibrayName);
            // 这里默认不替换之前的插件内容
            if ( _iPlugin.ContainsKey(classLibrayName) )
            {
                Console.WriteLine("已经加载该插件");
                return;
            }
            if ( !typeof(IPlugin).IsAssignableFrom(type) )
            {
                Console.WriteLine($"{asm.ManifestModule.Name}未继承约定接口");
                return;
            }
            //dll实例化
            var instance = Activator.CreateInstance(type) as IPlugin;
            var protocolInfo = instance.GetPluginInformation();
            protocolInfo.LastTime = dll.LastWriteTime;
            Console.WriteLine($"插件名称:{protocolInfo.Name}");
            Console.WriteLine($"插件版本:{protocolInfo.Version}");
            Console.WriteLine($"插件作者:{protocolInfo.Author}");
            Console.WriteLine($"插件时间:{protocolInfo.LastTime}");
            _iPlugin.Add(classLibrayName, instance);
            _iPluginInfos.Add(classLibrayName, protocolInfo);
            //释放插件资源
            instance.Dispose();
            instance = null;
            Console.WriteLine(string.Format("==========【{0}】==========", "共加载插件{0}个"), _iPlugin.Count);
        }
        /// <summary>
        /// 修改插件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void FileWatcher_Changed(object sender, FileSystemEventArgs e)
        {
            string pluginName = e.Name.Split(".")[0];
            var dll = new FileInfo(e.FullPath);
            // 替换插件
            if ( _iPluginInfos.ContainsKey(pluginName) )
            {
                // 修改时间不一致,说明是新的插件
                if ( _iPluginInfos[pluginName].LastTime != dll.LastWriteTime)
                {
                    Console.WriteLine(string.Format("==========【{0}】==========", "修改" + e.Name));
                    // 更新
                    var fileData = File.ReadAllBytes(e.FullPath);
                    Assembly asm = Assembly.Load(fileData);
                    var manifestModuleName = asm.ManifestModule.ScopeName;
                    var classLibrayName = manifestModuleName.Remove(manifestModuleName.LastIndexOf("."), manifestModuleName.Length - manifestModuleName.LastIndexOf("."));
                    Type type = asm.GetType("Plugin_Test" + "." + classLibrayName);
                    if ( !typeof(IPlugin).IsAssignableFrom(type) )
                    {
                        Console.WriteLine($"{asm.ManifestModule.Name}未继承约定接口");
                        return;
                    }
                    var instance = Activator.CreateInstance(type) as IPlugin;
                    var protocolInfo = instance.GetPluginInformation();
                    protocolInfo.LastTime = dll.LastWriteTime;
                    Console.WriteLine($"插件名称:{protocolInfo.Name}");
                    Console.WriteLine($"插件版本:{protocolInfo.Version}");
                    Console.WriteLine($"插件作者:{protocolInfo.Author}");
                    Console.WriteLine($"插件时间:{protocolInfo.LastTime}");
                    _iPlugin[classLibrayName] = instance;
                    _iPluginInfos[classLibrayName] = protocolInfo;
                    instance.Dispose();
                    instance = null;
                    // 避免多次触发
                    this._watcher.EnableRaisingEvents = false;
                    this._watcher.EnableRaisingEvents = true;
                    Console.WriteLine(string.Format("==========【{0}】==========", "共加载插件{0}个"), _iPlugin.Count);
                }
            }
        }
        /// <summary>
        /// 删除插件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void FileWatcher_Deleted(object sender, FileSystemEventArgs e)
        {
            Console.WriteLine(string.Format("==========【{0}】==========", "删除" + e.Name));
            string pluginName = e.Name.Split(".")[0];
            if ( _iPlugin.ContainsKey(pluginName) )
            {
                _iPlugin.Remove(pluginName);
                _iPluginInfos.Remove(pluginName);
                Console.WriteLine($"插件{e.Name}被移除");
            }
            Console.WriteLine(string.Format("==========【{0}】==========", "共加载插件{0}个"), _iPlugin.Count);
        }
        /// <summary>
        /// 重命名
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected void FileWatcher_Renamed(object sender, RenamedEventArgs e)
        {
            //TODO:暂时不做处理
            Console.WriteLine("重命名" + e.OldName + "->" + e.Name);
            //Console.WriteLine("重命名: OldPath:{0} NewPath:{1} OldFileName{2} NewFileName:{3}", e.OldFullPath, e.FullPath, e.OldName, e.Name);
        }

    2.2 EnableRaisingEvents 控制是否启用,这个类的修改方法很容易被多次调用,因此用以下代码避免多次触发:

// 避免多次触发
this._watcher.EnableRaisingEvents = false;
this._watcher.EnableRaisingEvents = true;

    2.3 这样,每当这个指定文件夹下的dll发生变化时,就会进行相应的操作,重新加载到内存中,其测试结果如下:

  c# 支持热插拔的插件

     2.4 当然,这只是一个简单的小demo,还会有很多问题,希望以后遇到了再改进。

参考:

  https://blog.csdn.net/daoer_sofu/article/details/70473691

  https://www.cnblogs.com/winformasp/articles/10893922.html

上一篇:c#基础--学习笔记


下一篇:C# 季节判断