在不锁定文件的情况下加载和卸载插件






4.99/5 (25投票s)
本文介绍了一种解决方案,该方案允许应用程序加载和执行插件,并在不锁定程序集文件的情况下卸载它。
引言
使用插件扩展应用程序的逻辑是代码抽象的一个很好的例子。插件接口描述了功能,而实现与应用程序是解耦的,除了插件接口本身。此外,插件在运行时加载和卸载是一个强大的功能。我在互联网上找到的许多文章都描述了如何将插件加载到临时的 AppDomain 中。但大多数情况下,插件实际上仍保留在应用程序的 AppDomain 中,因此无法卸载,导致硬盘上的插件程序集被文件锁定。本文介绍了一种方法,可以在卸载插件后,在不锁定插件文件的情况下使用和释放插件。
背景
最近,我参与了一个项目,该项目使用插件框架来更新另一个系统。应用程序会检查是否有更新的插件版本,并在可用时下载它们。下载完成后,插件将被执行以更新正在运行的系统。该系统的一个缺点是,为了卸载旧插件以使用最新版本,它必须重新启动自身。
问题的根源在于为插件创建了一个临时的 AppDomain,但在加载它时,它仍然与应用程序的 AppDomain 关联。尽管系统为插件使用了单独的 AppDomain,但在卸载它时,已加载的插件仍然锁定驱动器上的程序集,并且无法删除,因为它实际上已加载到应用程序的 AppDomain 中。删除旧版本的插件是系统要求之一,以确保系统使用最新版本。这种意外的文件锁定引起了对 AppDomain 使用不当的怀疑。
我在这个问题here的答案中找到了解决我的问题的方法
使用代码
示例应用程序将从指定文件夹加载和执行插件。插件在为给定文件夹中找到的每个插件单独创建的临时 AppDomain 中加载和执行。插件的加载和执行在单独的插件 AppDomain 上下文中完成。执行完毕后,插件将从硬盘删除。这样做是为了表明在临时 AppDomain 卸载后,应用程序不会对程序集产生文件锁定。
实现
以下各节将描述示例代码的实现
- 类图:UML概述,描述了包(程序集)、类及其关系
- PluginBase:基础程序集,包含插件接口描述
- PlugIn1:示例插件实现
- Application:包含插件加载和执行例程
类图
该示例包含一个控制台应用程序、插件接口和一个示例插件实现。以下类图显示了接口和类在其程序集中的排列。
PluginBase
PluginBase 程序集包含插件的接口定义。
public interface IPlugin { void Activate(); void Execute(); void Deactivate(); }
Plugin1
一个演示插件,通过显示消息框来展示方法调用。
using System.Windows.Forms; using PluginBase; public class Plugin1 : IPlugin { public void Activate() { MessageBox.Show("Activating Plugin 1"); } public void Execute() { MessageBox.Show("Peforming Action Plugin 1"); } public void Deactivate() { MessageBox.Show("Deactivating Plugin 1"); } }
PluginContext
插件上下文类,用作跨 AppDomain 数据传输的示例。它包含插件的文件路径(从何处加载)以及 CanDeletePlugin 标志,以返回插件是否可以从硬盘删除的状态。请注意,类必须是可序列化的,以便可以在 AppDomain 边界之间交换。
using System; [Serializable] public class PluginContext { public string FilePath { get; set; } public bool CanDeletePlugin { get; set; } }
Application
测试应用程序包含两个方法
- Main():扫描插件目录并为找到的每个插件创建一个新的临时 AppDomain
- PluginCallback():临时 AppDomain 的回调方法,在其中加载和执行插件
public class Program { static void Main() { try { // Iterate through all plug-ins. foreach (var filePath in Directory.GetFiles(Constants.PluginPath, Constants.PluginSearchPattern)) { // Create the plug-in AppDomain setup. var pluginAppDomainSetup = new AppDomainSetup(); pluginAppDomainSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; // Create the plug-in AppDomain with the setup. var plugInAppDomain = AppDomain.CreateDomain(filePath, null, pluginAppDomainSetup); // Pass the plug-in file path to the AppDomain var pluginContext = new PluginContext { FilePath = filePath }; plugInAppDomain.SetData(Constants.PluginContextKey, pluginContext); // Execute the loader in the plug-in AppDomain's context. // This will also execute the plug-in. plugInAppDomain.DoCallBack(PluginCallback); // Retrieve the flag if the plug-in has executed and can be deleted. pluginContext = plugInAppDomain.GetData(Constants.PluginContextKey) as PluginContext; // Unload the plug-in AppDomain. AppDomain.Unload(plugInAppDomain); // Delete the plug-in if applicable. if (pluginContext != null && pluginContext.CanDeletePlugin) { File.Delete(filePath); } } } catch (Exception exception) { Console.WriteLine(exception); } } /// <summary> /// The callback routine that is executed in the plug-in AppDomain context. /// </summary> private static void PluginCallback() { try { // Retrieve the filePath from the plug-in AppDomain var pluginContext = AppDomain.CurrentDomain.GetData(Constants.PluginContextKey) as PluginContext; if (pluginContext != null) { // Load the plug-in. var pluginAssembly = Assembly.LoadFrom(pluginContext.FilePath); // Iterate through types of the plug-in assembly to find the plug-in class. foreach (var type in pluginAssembly.GetTypes()) { if (type.IsClass && typeof(IPlugin).IsAssignableFrom(type)) { // Create the instance of the plug-in and call the interface methods. var plugin = Activator.CreateInstance(type) as IPlugin; if (plugin != null) { plugin.Activate(); plugin.Execute(); plugin.Deactivate(); // Set the delete flag to true, to signal that the plug-in can be deleted. pluginContext.CanDeletePlugin = true; AppDomain.CurrentDomain.SetData(Constants.DeletePluginKey, pluginContext); break; } } } } } catch (Exception exception) { Console.WriteLine(exception); } } }
关注点
AppDomain.DoCallback(CrossAppDomainDelegate)
AppDomain.DoCallback() 方法是魔法发生的地方。它将在临时插件 AppDomain 上下文中调用分配的回调例程。
插件实例化和执行
在临时 AppDomain 上下文中加载插件程序集后,将搜索插件类类型。找到后,将使用 Activator 类创建插件的新实例。成功实例化后,将调用插件的方法。最后,通过调用 SetData() 设置 CanDeletePlugin 标志并将其返回给主应用程序。
AppDomain.SetData(string, object), AppDomain.GetData(string)
由于插件在单独的 AppDomain 中运行,因此必须使用 AppDomain 的 SetData() 和 GetData() 方法进行跨 AppDomain 通信。在此示例中,我使用 PluginContext 类来显示插件在另一个 AppDomain 中执行,并且 PluginContext 类必须是可序列化的,以便它可以从一个域发送到另一个域。
失败的实现
为了完整起见:下一个代码部分描述了失败的实现。在我对互联网的研究中,我多次发现了这种类型的实现。请注意,插件部分是有效的!插件被加载到内存中,位于其自己的 AppDomain 中,使用后,临时 AppDomain 将被卸载。但是文件删除失败,因为插件实际上已加载到应用程序的域中。
public class Program { static void Main() { try { // Iterate through all plug-ins. foreach (var filePath in Directory.GetFiles(Constants.PluginPath, Constants.PluginSearchPattern)) { // Create the plug-in AppDomain setup. var pluginAppDomainSetup = new AppDomainSetup(); pluginAppDomainSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; // Create the plug-in AppDomain with the setup. var plugInAppDomain = AppDomain.CreateDomain(filePath, null, pluginAppDomainSetup); AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainAssemblyResolve; byte[] fileContent; using (FileStream dll = File.OpenRead(filePath)) { fileContent = new byte[dll.Length]; dll.Read(fileContent, 0, (int)dll.Length); } var pluginAssembly = plugInAppDomain.Load(fileContent); bool canDelete = false; // Iterate through types of the plug-in assembly to find the plug-in class. foreach (var type in pluginAssembly.GetTypes()) { if (type.IsClass && typeof(IPlugin).IsAssignableFrom(type)) { // Create the instance of the plug-in and call the interface methods. var plugin = Activator.CreateInstance(type) as IPlugin; if (plugin != null) { plugin.Activate(); plugin.Execute(); plugin.Deactivate(); canDelete = true; break; } } } // Unload the plug-in AppDomain. AppDomain.Unload(plugInAppDomain); // Delete the plug-in if applicable. if (canDelete) { File.Delete(filePath); } } } catch (Exception exception) { Console.WriteLine(exception); } } private static Assembly CurrentDomainAssemblyResolve(object sender, ResolveEventArgs args) { // Quick and dirty string fileName = args.Name.Split(new[] { ',' })[0]; fileName = Path.ChangeExtension(fileName, "DLL"); fileName = Path.Combine(Constants.PluginPath, fileName); fileName = Path.GetFullPath(fileName); return Assembly.LoadFile(fileName); } }
结论
此示例展示了一种方法,可以使用临时 AppDomain 加载和执行插件,在卸载临时 AppDomain 后不对程序集保持文件锁定。
我希望本文能为您提供一个关于如何使用临时 AppDomain 的完整场景。