场景:
这项目用到了插件化开发,不是我做的,趁着现在有空学习一下。插件就是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,还会有很多问题,希望以后遇到了再改进。