插件框架






4.87/5 (30投票s)
一个简单的框架,使您的应用程序能够动态地发现、加载和卸载插件。
引言
本文介绍了一个框架,它提供了一个简单的接口来加载/卸载自定义插件到您的应用程序中。它基于FileSystemWatcher
、AppDomain
和MEF
技术。主要特点是:
- 插件文件夹可以采用任何结构
- 包含子文件夹
- 同一个文件夹中可以有多个插件dll
- 单个dll中可以有多个插件实现
- 运行时创建、重命名、替换、删除插件文件夹或文件
- 加载/重新加载插件期间不会出现内存泄漏
- 插件实现中需要编写的额外代码最少
- 加载具有参数化构造函数的插件
- 插件可以使用任何常规语法(泛型方法、动态、事件等)
- 插件可以有任何额外的依赖项
- 插件中的未处理异常不会导致主机应用程序崩溃
背景
我喜欢应用程序灵活且动态。但即使在21世纪,大多数应用程序都需要在每次更改内容时重新启动。在我看来,这是极大的时间浪费。
因此,我进行了简短的调查,了解市场上有什么。最后,我找到了三种(在.Net中)在我的应用程序中实现插件架构的方法——MEF、MAF和反射。但没有一种完全满足我的要求。
- MEF
- 阻止可执行文件,防止运行时更改dll
- 在同一个AppDomain中加载dll。如果一个插件抛出未处理的异常,整个应用程序可能会崩溃。
- MAF
- 插件文件夹应具有预定义的结构
- 实现的复杂性,尤其是在插件实现方面
- Reflection(反射)
- 太底层了,我不想重新发明轮子
使用代码
基本上,该框架监视指定的文件夹和所有子文件夹的创建/更改/重命名/删除事件。当发生“创建”事件时,框架会创建一个单独的AppDomain
并将插件(位于创建的文件夹中)加载到此域中。框架使用MEF
加载插件。此外,为了避免阻塞文件,AppDomain会创建创建文件夹的影子副本
。当文件夹或其根文件之一发生更改/重命名/删除时,框架会删除旧的AppDomain并使用插件的新实例创建一个新的AppDomain。因此,每个文件夹将只有一个AppDomain。唯一的限制是所有从插件传递到主机应用程序以及反向传递的对象都应该是MarshalByRefObject
。这是因为对象通过AppDomain边界传递。
让我们看看它是如何工作的。
首先需要定义一个契约。这是一个在主机应用程序和插件之间共享的接口。因此,它应该在共享程序集中定义。
public interface IPlugin : IDisposable
{
string Name { get; }
string SayHelloTo(string personName);
}
正如我所说,唯一的限制是继承自MarshalByRefObject
的插件类。我不想吓跑将实现此契约的开发人员,因此我将在同一个项目中的基类中执行此操作。
public abstract class BasePlugin : MarshalByRefObject, IPlugin
{
public BasePlugin(string name)
{
Name = name;
}
public string Name { get; private set; }
public abstract string SayHelloTo(string personName);
public virtual void Dispose()
{
//TODO:
}
}
下一步是创建一个插件。我更喜欢每个项目实现一个插件,但没有限制可以进行多个实现。
using Contracts;
using System;
using System.ComponentModel.Composition;
namespace Plugin1
{
[Export(typeof(IPlugin))]
public class Plugin : BasePlugin
{
public Plugin()
: base("Plugin1")
{
Console.WriteLine("ctor_{0}", Name);
}
public override string SayHelloTo(string personName)
{
string hello = string.Format("Hello {0} from {1}.", personName, Name);
return hello;
}
public override void Dispose()
{
Console.WriteLine("dispose_{0}", Name);
}
}
}
为了允许MEF发现此插件,需要使用MEF属性“Export
”对其进行装饰并指定契约类型。还需要向项目添加“System.ComponentModel.Composition
”引用。
最后一步是创建一个主机应用程序。例如,我将使用控制台应用程序。需要添加以下引用:Contracts
(包含契约的程序集)、Mark42
(这是插件框架本身)。
using Contracts;
using Mark42;
using System;
using System.Collections.Generic;
using System.IO;
namespace TestConsole
{
class Program
{
static void Main(string[] args)
{
var pluginsFolderPath = Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, "Plugins");
PluginService<IPlugin> pluginService = new PluginService<IPlugin>(pluginsFolderPath, "*.dll", true);
pluginService.PluginsAdded += pluginService_PluginAdded;
pluginService.PluginsChanged += pluginService_PluginChanged;
pluginService.PluginsRemoved += pluginService_PluginRemoved;
pluginService.Start();
Console.ReadKey();
pluginService.Stop();
}
#region Event handlers
private static void pluginService_PluginRemoved(PluginService<IPlugin> sender, List<IPlugin> plugins)
{
foreach (var plugin in plugins)
{
Console.WriteLine("PluginRemoved: {0}.", plugin.Name);
plugin.Dispose();
}
}
private static void pluginService_PluginChanged(PluginService<IPlugin> sender, List<IPlugin> oldPlugins, List<IPlugin> newPlugins)
{
Console.WriteLine("PluginChanged: {0} plugins -> {1} plugins.", oldPlugins.Count, newPlugins.Count);
foreach (var plugin in oldPlugins)
{
Console.WriteLine("~removed: {0}.", plugin.Name);
plugin.Dispose();
}
foreach (var plugin in newPlugins)
{
Console.WriteLine("~added: {0}.", plugin.Name);
}
}
private static void pluginService_PluginAdded(PluginService<IPlugin> sender, List<IPlugin> plugins)
{
foreach (var plugin in plugins)
{
Console.WriteLine("PluginAdded: {0}.", plugin.Name);
Console.WriteLine(plugin.SayHelloTo("Tony Stark"));
}
}
#endregion
}
}
正如您所看到的,它非常易于使用。需要创建一个PluginService
,指定所需的契约以及查找实例的位置。还有两个附加参数:用于监视文件的通配符和一个启用服务监视子文件夹的标志。
PluginService
有三个事件,在添加、更改或删除插件时触发。
为了方便起见,请转到插件项目属性并将它的输出路径重定向到“..\TestConsole\bin\Debug\Plugins\Plugin1\”。
就是这样!现在运行控制台应用程序并尝试使用Plugin1文件夹。您将看到控制台应用程序接收有关插件重新加载和插件新实例的事件。
参数化构造函数
有一种方法可以编写具有参数化构造函数的插件。这需要一些编码。
首先需要向插件类中添加所需的构造函数,并使用属性ImportingConstructor
对其进行装饰。
[Export(typeof(IPlugin))]
public class Plugin : BasePlugin
{
[ImportingConstructor]
public Plugin(CustomParameters parameters)
: base("Plugin1")
{
Console.WriteLine("ctor_{0}", Name);
}
public override string SayHelloTo(string personName)
{
string hello = string.Format("Hello {0} from {1}.", personName, Name);
return hello;
}
public override void Dispose()
{
Console.WriteLine("dispose_{0}", Name);
}
}
类型CustomParameters
应在插件项目和主机应用程序项目之间共享。还要记住将其设为MarshalByRefObject
。
下一步是在主机应用程序项目中创建CustomPluginService
。
using Contracts;
using Mark42;
using System.Collections.Generic;
namespace TestConsole
{
public class CustomPluginService<TPlugin> : PluginService<TPlugin>
where TPlugin : class
{
public CustomPluginService(string pluginsFolder, string searchPattern, bool recursive)
: base(pluginsFolder, searchPattern, recursive)
{
}
protected override List<TPlugin> LoadPlugins(MefLoader mefLoader)
{
CustomParameters parameters = new CustomParameters()
{
SomeParameter = 42
};
return mefLoader.Load<TPlugin, CustomParameters>(parameters);
}
}
}
这里的主要思想是重写LoadPlugin
方法。在此方法中,您可以使用最多16个参数调用Load
方法。
最后,在Main
方法中将PluginService
替换为CustomPluginService
。就是这样。应该可以工作。
历史
2014年10月22日 - 首个版本