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