在 .NET 中创建灵活的动态插件架构






3.47/5 (17投票s)
2003年11月12日
8分钟阅读

136953

3257
本文演示了如何创建一个简单的类,该类可以扩展以协助插件的创建和管理。
引言
插件是开发人员工具库中的强大工具,原因有很多,在此不一一赘述。我的意思是,如果您正在阅读本文,那么您很可能已经知道自己想使用它们了。
问题
基于插件的环境中的陷阱通常是由使用它们的系统强加的。例如,对于分布式应用程序,插件非常有用,因为它们可以轻松地添加代码。它们也是一个相当宽容的系统。通常,升级是在应用程序关闭时完成的,并且文件锁定问题通常不是问题。
另一方面,Web 应用程序往往更加挑剔。如果您加载了网站的频繁访问者正在使用的库,那么您将很难找到一个文件未被锁定的时间。在这种情况下,唯一的解决方案是关闭 IIS,更新文件,然后重新启动它。在我们这里,这种情况每周(有时是每两周)发生一次,这是不可接受的。
解决方案
答案是创建一个可以在最短的潜在错误发生的情况下即时升级的系统。这需要几个条件:
- 不能直接使用库
- 需要扫描文件夹以获取更新
- 需要删除未使用的文件
- 需要索引库
所有这些点都需要解决。但是,在此之前,我们需要做出一个架构决策。我决定我希望我的库形成一个抽象类。我喜欢有一个 Broker 类来处理创建被插件化的类的所有必要调用。因此,我们的最终结果将是一个抽象类,其中包含 Broker 将使用的受保护的静态方法(这样我们的 Broker 就无需实例化)以及所有其他调用的私有静态方法。
public abstract class PluginLoader
{
protected static Type FindAssembly(string PluginClass,
string Class, string[] Folders, params Type[] PluginTypes)
{
// This will find the Assembly requested.
}
}
我选择的参数可以很容易地解释。 `PluginClass` 区分了这一组插件与其他任何一组插件。 `Class` 参数指定了您要创建的实际类。这可以是完全限定命名空间的,但如果可以信任它是唯一的,则可以仅提供类名。 `Folders` 提供了一个要搜索的(完全限定的)路径列表。最后一个参数 `PluginTypes` 是 `System.Type` 对象的数组,提供了插件可能派生的类型列表。这有助于我们以后在需要确保我们不加载错误类型的插件时保持清醒。
现在让我们来充实一下这个方法。我们只需要加载我们的配置文件(如果存在),然后调用几个其他例程来填充它。
public abstract class PluginLoader
{
protected static Type FindAssembly(string PluginClass,
string Class, string[] Folders, params Type[] PluginTypes)
{
// Look at each folder specified, but stop if we find the library
foreach (string Folder in Folders)
{
// Load up the file - LoadXmlFile is tolerant of a bad file,
// just in case
System.Xml.XmlDocument PluginLibrary = LoadXmlFile(
System.IO.Path.Combine(Folder, "plugins.xml"));
if (PluginLibrary == null)
// Our file doesn't exist, so create one
PluginLibrary = CreatePluginFile(Folder, PluginClass, PluginTypes);
else
// Our file does exist, so make sure it doesn't need updating
PluginLibrary = UpdatePluginFile(Folder, PluginClass, PluginTypes);
// This should never be null, but it may be if the folder is bad or
// something. I probably shouldn't ignore this, but oh well.
if (PluginLibrary != null)
{
// TODO: Check to see if the library is in here.
// If so, load it up and return it
}
}
// We didn't find it, so return a null
return null;
}
// This just loads up that xml file and ignores any errors.
// I... uh... left error
// handling out as an exercise for the reader. Yeah, that's it...
private static Xml.XmlDocument LoadXmlFile(string Path)
{
if (System.IO.File.Exists(Path))
{
try
{
Xml.XmlDocument Result = new Xml.XmlDocument();
Result.Load(Path);
return Result;
}
catch
{
return null;
}
}
else
return null;
}
}
到目前为止一切都应该相当直接。我们必须编写 `CreatePluginFile()` 和 `UpdatePluginFile()` 方法,并填充 TODO 部分。这涵盖了扫描文件夹以获取更新的要求(来自上面的列表)。此时,有必要讨论 XML 文件。
我决定将扫描插件以获取相关类,并将此数据放在包含库的文件夹中的一个名为 *plugins.xml* 的 XML 文件中。同一文件中可以索引多种类型的插件,因此这相当好地满足了索引库的要求。
从代码中可以看到,数据是从一个名为 *plugins.xml* 的文件中读取的。我们应该也向同一个文件写入数据。对于文件格式,我决定使用以下格式:
<plugins updated="11/10/2003 2:22:05 PM">
<retired>
<plugin>C:\DOCUME~1\MACHINE\ASPNET\LOCALS~1\Temp\tmp7C1.tmp</plugin>
</retired>
<active type="widgets" updated="11/10/2003 2:22:05 PM">
<plugin library="Widget.dll" interface="IWidget"
name="widget1" fullname="mynamespace.widget1">
C:\DOCUME~1\MACHINE\ASPNET\LOCALS~1\Temp\tmp7C2.tmp
</plugin>
<plugin library="Widget.dll" interface="IWidget"
name="widget2" fullname="myothernamespace.widget2">
C:\DOCUME~1\MACHINE\ASPNET\LOCALS~1\Temp\tmp7C2.tmp
</plugin>
</active>
</plugins>
这里有很多数据。我将简要介绍重要部分。 `Plugins` 是我们的根节点。这里的 `timestamp` 仅用于信息目的。 `Retired` 包含标记为删除的 `plugin` 元素。系统应在它们的锁定解除时删除这些元素,以节省服务器空间(相信我,当您因未清理此文件夹而意外耗尽空间时,这会很麻烦)。`active` 块是当前可用的插件。我们将在稍后使用它的 `updated` 属性来确定文件是否是新的。`type` 属性与上面指定的 `PluginClass` 匹配。这可以防止不同类型的插件相互干扰。
在 `plugin` 元素中,`interface` 属性只是告诉我们匹配了哪个接口(以便我们可以验证它是否应包含在内),`name` 和 `fullname` 属性分别指定类名和类名以及所有命名空间。元素的内容是 DLL 本身的位置。您可能会注意到它指向临时目录中的文件。当我们发现新文件时,我们会将它们复制到那里。
所以我们需要加载库并返回适当的类型。既然我们知道了 XML 文件中的数据位置,这并不难。所以……
System.Xml.XmlElement LibraryNode = (System.Xml.XmlElement)
PluginLibrary.SelectSingleNode(
"plugins/active[@type='" + PluginClass + "']/plugin[@name='" +
Class.ToLower() + "' or @fullname='" + Class.ToLower() + "']");
if (LibraryNode != null)
{
System.Reflection.Assembly PluginAssembly =
System.Reflection.Assembly.LoadFile(LibraryNode.InnerText);
return PluginAssembly.GetType(LibraryNode.GetAttribute("fullname"),
false, true);
}
……将上面的 TODO 部分替换为此小技巧,并在文件有效的情况下,我们就完成了。其余部分处理“待定”。对于不熟悉的人来说,上面的代码会找到所需程序集的完整路径,使用反射加载它,并创建相应的新的 Type 对象。
现在,因为这篇文章有点长,这里是 `CreatePluginFile()` 和 `UpdatePluginFile()` 方法。
private static Xml.XmlDocument CreatePluginFile(string PluginFolder,
string PluginClass, Type[] PluginTypes)
{
Xml.XmlDocument PluginLibrary = new Xml.XmlDocument();
PluginLibrary.LoadXml("<RETIRED /> ");
AddAssembliesToPluginFile(PluginFolder, PluginClass,
PluginLibrary, PluginTypes);
PluginLibrary.Save(System.IO.Path.Combine(PluginFolder, "plugins.xml"));
return PluginLibrary;
}
private static Xml.XmlDocument UpdatePluginFile(string PluginFolder,
string PluginClass, Type[] PluginTypes)
{
Xml.XmlDocument PluginLibrary = new Xml.XmlDocument();
try
{
PluginLibrary.Load(System.IO.Path.Combine(PluginFolder, "plugins.xml"));
}
catch
{
PluginLibrary = CreatePluginFile(PluginFolder, PluginClass, PluginTypes);
}
bool FileChanged = false;
foreach (string PluginFile in System.IO.Directory.GetFiles(
PluginFolder, "*.dll"))
{
DateTime LastUpdate = new DateTime();
try
{
LastUpdate = DateTime.Parse(((Xml.XmlElement)
PluginLibrary.SelectSingleNode("/plugins/active[@type='" +
PluginClass + "']")).GetAttribute("updated"));
}
catch
{ }
if (System.IO.File.GetLastWriteTime(PluginFile) > LastUpdate)
{
foreach (Xml.XmlElement OldAssembly in PluginLibrary.SelectNodes(
"/plugins/active[@type='" + PluginClass + "']/plugin"))
{
OldAssembly.ParentNode.RemoveChild(OldAssembly);
PluginLibrary.SelectSingleNode("/plugins/retired").AppendChild(
OldAssembly);
}
AddAssembliesToPluginFile(PluginFolder, PluginClass,
PluginLibrary, PluginTypes);
FileChanged = true;
break;
}
}
foreach (Xml.XmlElement OldAssembly in PluginLibrary.SelectNodes(
"/plugins/retired/plugin"))
{
try
{
System.IO.File.Delete(OldAssembly.InnerText);
OldAssembly.ParentNode.RemoveChild(OldAssembly);
FileChanged = true;
}
catch (Exception exx)
{
exx.GetType();
}
}
try
{
if (FileChanged)
PluginLibrary.Save(System.IO.Path.Combine(PluginFolder, "plugins.xml"));
}
catch
{ }
return PluginLibrary;
}
这些非常简单。`CreatePluginFile()` 创建一个新的 XML 文件并使用 `AddAssembliesToPluginFile()` 进行填充。`UpdatePluginFile()` 检查此 `active` 元素的文件。如果任何文件较新,它会将所有 `plugin` 元素移至 `retired` 元素并尝试删除它们。这涵盖了“必须删除未使用的文件”的要求。如果其中任何内容更改了文件,则此例程会保存它。现在我们剩下的是 `AddAssembliesToPluginFile()`,它将满足我们最后一个复制文件到临时位置的要求。
private static void AddAssembliesToPluginFile(string PluginFolder,
string PluginClass, Xml.XmlDocument PluginLibrary, Type[] PluginTypes)
{
if (System.IO.Directory.Exists(PluginFolder))
{
foreach (string PluginFile in System.IO.Directory.GetFiles(
PluginFolder, "*.dll"))
{
bool FoundOne = false;
string NewFileName = System.IO.Path.GetTempFileName();
string OldFileName = PluginFile.Substring(
PluginFile.LastIndexOf("\\") + 1);
System.IO.File.Copy(PluginFile, NewFileName, true);
System.Reflection.Assembly PluginAssembly =
System.Reflection.Assembly.LoadFile(NewFileName);
foreach (System.Type NewType in PluginAssembly.GetTypes())
{
bool Found = false;
foreach (System.Type InterfaceType in NewType.GetInterfaces())
foreach (System.Type DesiredType in PluginTypes)
{
if (InterfaceType == DesiredType)
{
string ClassName = NewType.Name.ToLower();
if (NewType.Namespace != null)
ClassName = NewType.Namespace.ToLower() + "." + ClassName;
FoundOne = true;
Xml.XmlElement NewNode = PluginLibrary.CreateElement("plugin");
NewNode.SetAttribute("library", OldFileName);
NewNode.SetAttribute("interface", DesiredType.Name);
NewNode.SetAttribute("name", NewType.Name.ToLower());
NewNode.SetAttribute("fullname", ClassName);
NewNode.AppendChild(PluginLibrary.CreateTextNode(NewFileName));
Xml.XmlElement Parent =
(Xml.XmlElement)PluginLibrary.SelectSingleNode(
"/plugins/active[@type='" + PluginClass + "']");
if (Parent == null)
{
Parent = PluginLibrary.CreateElement("active");
Parent.SetAttribute("type", PluginClass);
PluginLibrary.SelectSingleNode("/plugins"
).AppendChild(Parent);
}
Parent.AppendChild(NewNode);
Parent.SetAttribute("updated", System.DateTime.Now.ToString());
Found = true;
break;
}
if (Found) break;
}
}
if (!FoundOne)
{
Xml.XmlElement NewNode = PluginLibrary.CreateElement("plugin");
NewNode.AppendChild(PluginLibrary.CreateTextNode(NewFileName));
PluginLibrary.SelectSingleNode("/plugins/retired").AppendChild(
NewNode);
PluginLibrary.DocumentElement.SetAttribute("updated",
System.DateTime.Now.ToString());
}
}
}
}
这不幸有点丑陋。我们遍历文件夹中的所有 DLL 文件(我们实际上应该遍历所有 DLL 和 EXE 文件,但我不能为您做所有事情)。我们将文件复制到我们的临时文件夹,并获得新路径。然后,我们遍历该库中的所有类。在其中,我们遍历类继承的每个接口。在其中,我们遍历我们想查找的所有类型。如果其中任何一个匹配,我们就认为此类型是有效的。将必要的数据添加到 XML 文件,然后继续下一个类。 voilà,所有要求都已满足。
用法
既然我们已经有了这个很棒的小库,我们就需要能够利用它。这是一个非常简单的 `WidgetBroker` 类,演示了该类的使用(请注意,我添加了一个 `FindAssembly()` 的替代定义,它接受单个文件夹并将其作为数组传递)。
public class WidgetBroker : Library.PluginLoader
{
public static IWidget Load(string InputType)
{
Type InputElementClass = FindAssembly("input", InputType,
System.IO.Path.Combine(Environment.CurrentDirectory, "plugins"),
typeof(IWidget));
return InputElementClass.GetConstructor(System.Type.EmptyTypes).Invoke(
System.Type.EmptyTypes) as IWidget;
}
}
`WidgetBroker` 使用我们刚刚创建的库查找 Type 并创建它(假设它有一个默认构造函数)。
失败点
当然,因为这需要写成一篇有一定篇幅的文章,所以我无法完成完整的实现(而且因为我懒,所以我没有完成,所以请不要问)。因此,存在几个失败点。一个是在您更新或添加 DLL 时,如果有很多用户正在访问您的站点。这不算大问题,因为系统对此相当宽容。它会即时构建文件,使用它们,但如果保存失败也不会抱怨。如果出现严重问题(例如磁盘已满),则可能会出现问题。所有这些都可以通过明智地使用 Reader/Writer 锁来解决。但我认为真正的解决方案是让应用程序在 Broker 类首次被调用时执行此轮询。届时,它应该设置一个 `System.IO.FileSystemWatcher` 类来监视库的变化。一旦发生更改,就可以将单个更改存储到 `plugin.xml` 文件并持久化到磁盘。
这也没有适当地复制被其他程序集使用的程序集。这有几个原因,主要是因为您需要更改程序集的链接(这将把“中级”文章变成了“高级”文章)。如果您按原样复制引用的程序集(使用默认文件名),它们最终会被锁定在您的临时文件夹中。您无法真正更改文件名来像我们以前那样绕过锁定问题,因为您需要更新主程序集以指向新文件。我决定不处理这个问题,并说您的插件不允许引用外部文件。
摘要
所以,我们基本上创建了这个东西。您可以添加一些额外的方法,这些方法可以提供额外的功能或更简单地调用该库的方式。如果您下载上面的源代码,您可以看到我做的一些示例,以及一个演示此技术有用的精彩小应用程序。我们笑了,我们哭了,我想我们完成了一些代码。
随便吧。
历史
版本 | 注释 | 随机农场动物 |
1.0 | 全新的 | 鸡 |