場景:
這項目用到了插件化開發,不是我做的,趁着現在有空學習一下。插件就是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