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("釋放內存");
        }
    }

    這個插件端的整體架構:

 

 

     1.3 然后寫主程序端,也就是加載和應用插件的程序。首先主程序端要有IPlugin這個接口的定義,如下

 

 

     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發生變化時,就會進行相應的操作,重新加載到內存中,其測試結果如下:

  

     2.4 當然,這只是一個簡單的小demo,還會有很多問題,希望以后遇到了再改進。

參考:

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

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


免責聲明!

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



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