基于AppDomain的插件开发-自动加载(三)
前面已经得到了热插拔的插件原型,这次讨论如果插件是服务提供者怎么办?
我能想到的,
- 需要在起动时加载所有插件
- 然后在插件变动时,及时卸载旧的插件,加载新的插件。
- 如果有新插件放在目录中,需要马上加载新的插件。
- 如果插件被删除,我们要把对应的服务也移除。
最终使用时,如下:
private void FormMain_Load(object sender, EventArgs e)
{
var inst = PluginManager.Instance;
inst.PluginChanged += OnPluginChanged;
}
void OnPluginChanged(object sender, PluginManagerEventArgs e)
{
if (e.ChangeType == PluginChangeType.Created)
{
// 这里初始化插件,提供服务
e.PluginInstance.Run(DateTime.Now.ToString());
}
}
一、 监视目录,第一想到的便是 FileSystemWatcher ,我们就用它来实现监视一个目录。 如果有经验的,会知道这个类的Changed事件,在文件变化时,因为文件属性多次变化,也会激发多次。我这里的解决方案是:
把收到的变动放置在容器中,任你变化,再你不再变化时,我统一处理一次。其中,重命名,理解为删除原来的,增加新文件。
/// <summary>
///监视插件目录
///输出插件DLL的变更,修改,删除,增加
/// </summary>
public class PluginManager
{
#region实现
#region字段
private static PluginManager _instance = null;
private FileSystemWatcher pluginWatcher;
private Timer timerProcess = null;
private ConcurrentDictionary<string, FileSystemEventArgs> changedPlugins = new ConcurrentDictionary<string, FileSystemEventArgs>();
private ConcurrentDictionary<string, PluginCallerProxy> plugins = new ConcurrentDictionary<string, PluginCallerProxy>();
#endregion
static PluginManager()
{
if (_instance == null)
{
lock (typeof(PluginManager))
{
if (_instance == null)
_instance = new PluginManager();
}
}
}
private PluginManager()
{
string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "plugins");
//监控
this.pluginWatcher = new FileSystemWatcher(path, "*.dll");
this.pluginWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;
this.pluginWatcher.Changed += OnPluginChanged;
this.pluginWatcher.Created += OnPluginChanged;
this.pluginWatcher.Deleted += OnPluginChanged;
this.pluginWatcher.Renamed += OnPluginRenamed;
pluginWatcher.EnableRaisingEvents = true;
timerProcess = new Timer(new TimerCallback(e => this.ProcessChangedPlugin()));
//加载所有
Directory.GetFiles(path, "*.dll").ToList().ForEach(file =>
{
FileInfo fi = new FileInfo(file);
this.changedPlugins[fi.Name] = new FileSystemEventArgs(WatcherChangeTypes.Created, fi.DirectoryName, fi.Name);
});
this.timerProcess.Change(10, -1);
}
void OnPluginRenamed(object sender, RenamedEventArgs e)
{
//重命名,理解为去掉原来的,增加新命名的
FileInfo old = new FileInfo(e.OldFullPath);
this.changedPlugins[old.Name] = new FileSystemEventArgs(WatcherChangeTypes.Deleted, old.DirectoryName, old.Name);
FileInfo n = new FileInfo(e.FullPath);
this.changedPlugins[n.Name] = new FileSystemEventArgs(WatcherChangeTypes.Created, n.DirectoryName, n.Name);
//1秒后再处理
this.timerProcess.Change(1000, -1);
}
void OnPluginChanged(object sender, FileSystemEventArgs e)
{
Debug.Print(e.Name + e.ChangeType);
//记录变更
this.changedPlugins[e.Name] = e;
//1秒后再处理
this.timerProcess.Change(1000, -1);
}
protected void ProcessChangedPlugin()
{
…
}
#endregion
}
二、 插件最终要提供给使用者 IPlugin 供用户调用,但是如果把 PluginLoader.RemotePlugin 直接提供给用户,则如果插件升级替换旧有逻辑时,其引用不易马上更新,造成仍用调用原来指向卸载的AppDomain的引用,造成调用失败。 所以我们这里需要引入 PluginCallerProxy ,里面保存真实跨域引用,我们插件更新时,直接更新这里的引用就一改全改了。
internal class PluginCallerProxy : IPlugin
{
private IPlugin _plugin;
private PluginLoader _pluginLoader;
private System.Threading.ReaderWriterLockSlim locker = new ReaderWriterLockSlim();
internal PluginLoader PluginLoader
{
get
{
return _pluginLoader;
}
set
{
_pluginLoader = value;
this.Plugin = _pluginLoader == null ? null : _pluginLoader.RemotePlugin;
}
}
internal IPlugin Plugin
{
get
{
locker.EnterReadLock();
try
{
if (_plugin == null)
{
throw new PluginException("插件已经卸载");
}
return _plugin;
}
finally
{
locker.ExitReadLock();
}
}
set
{
locker.EnterWriteLock();
try
{
_plugin = value;
}
finally
{
locker.ExitWriteLock();
}
}
}
public PluginCallerProxy(PluginLoader loader)
{
this.PluginLoader = loader;
this.Plugin = loader.RemotePlugin;
}
public Guid PluginId
{
get { return Plugin.PluginId; }
}
public string Run(string args)
{
return Plugin.Run(args);
}
public string Run(string args, Action action)
{
return Plugin.Run(args, action);
}
public string Run(string args, Func<string> func)
{
return Plugin.Run(args, func);
}
}
public class PluginManager
{
protected void ProcessChangedPlugin()
{
#region处理插件变化
foreach (var kv in this.changedPlugins)
{
FileSystemEventArgs e;
if (changedPlugins.TryRemove(kv.Key, out e))
{
Debug.Print(e.Name + "=>" + e.ChangeType);
switch (e.ChangeType)
{
case WatcherChangeTypes.Created:
{
//加载
var loader = new PluginLoader(e.Name);
var proxy = new PluginCallerProxy(loader);
plugins.TryAdd(e.Name, proxy);
OnPluginChange(new PluginManagerEventArgs(e.Name, PluginChangeType.Created, proxy));
}
break;
case WatcherChangeTypes.Deleted:
{
PluginCallerProxy proxy;
if (plugins.TryRemove(e.Name, out proxy))
{
OnPluginChange(new PluginManagerEventArgs(e.Name, PluginChangeType.Deleted, proxy));
var loader = proxy.PluginLoader;
proxy.PluginLoader = null;
loader.Unload();
}
}
break;
case WatcherChangeTypes.Changed:
{
PluginCallerProxy proxy;
if (plugins.TryGetValue(e.Name, out proxy))
{
OnPluginChange(new PluginManagerEventArgs(e.Name, PluginChangeType.Deleted, proxy));
var loader = proxy.PluginLoader;
loader.Unload();
loader = new PluginLoader(e.Name);
proxy.PluginLoader = loader;
OnPluginChange(new PluginManagerEventArgs(e.Name, PluginChangeType.Created, proxy));
}
}
break;
}
}
}
#endregion
}
}