在.NET环境中,动态加载与卸载插件是一个常见需求,但遗憾的是,关于这个话题的文档并不丰富。本文将介绍如何在.NET中实现插件的动态加载与卸载,同时确保插件不会加载到主应用域中。
.NET使用应用程序域(AppDomain)来实现类似Java中的虚拟机隔离。一个进程中可以存在多个应用程序域,它们之间是相互隔离的。一个应用程序域中的代码不能直接访问另一个应用程序域中的代码或资源。此外,一个应用程序域中的代码失败不会影响其他应用程序域。需要注意的是,不能单独卸载单个程序集,只能卸载整个应用程序域。
在案例中,将创建一个包含主应用程序域的主域,以及一个用于加载和卸载插件的域。以下是创建新应用程序域的代码示例:
AppDomain appDomainPluginA = AppDomain.CreateDomain("appDomainPluginA");
接下来,将程序集pluginA.dll
添加到新创建的应用程序域中:
Assembly pluginAassembly = appDomainPluginA.Load("pluginA");
然而,结果可能会让人感到有些意外。可能期望appDomainPluginA
只加载pluginA
,但实际上,pluginA
也被加载到了主应用程序域中。这种行为在MSDN文档中有说明:如果当前AppDomain
对象代表应用程序域A,而Load
方法从应用程序域B调用,那么程序集将被加载到两个应用程序域中。
当需要卸载插件时,问题就出现了。一旦在应用程序域中加载了程序集,就不能在卸载整个应用程序域之前卸载它。这意味着,如果想要卸载pluginA
,就必须卸载plugins
应用程序域,也就是主应用程序。这显然不是想要的结果。
使用AppDomain.Unload
在这种情况下将不起作用,因为pluginA
已经被加载到了AppDomain.CurrentDomain
中,并且不能在AppDomain.CurrentDomain
自身被卸载之前释放。因此,需要找到一种方法来加载和卸载应用程序域,而不会让程序集泄露到主应用程序域中。
这时,MarshalByRef
对象就派上用场了。它允许跨越应用程序域的边界。不是从主域调用插件的加载,而是在第二个域中执行代码。以下是实现的代码示例:
AppDomain appDomainPluginB = AppDomain.CreateDomain("appDomainPluginB");
RemoteLoader loader = (RemoteLoader)appDomainPluginB.CreateInstanceAndUnwrap("plugins", "plugins.RemoteLoader");
loader.LoadAndExecute("pluginB");
这样,就在appDomainPluginB
中创建了一个RemoteLoader
的实例,但可以在plugins
应用程序域中访问它。现在,让appDomainPluginB
中的实例加载pluginB.dll
(当然,是加载到appDomainPluginB
中)。
在RemoteLoader
中,使用反射来扫描并执行实现了IPlugin
接口的类/类型。这意味着IPlugin
类型的信息实际上被加载到了appDomainPluginB
中。
以下是使用反射查找并执行IPlugin
接口实现的代码示例:
foreach (Type type in pluginAassembly.GetTypes())
{
if (type.GetInterface("IPlugin") != null)
{
object instance = Activator.CreateInstance(type, null, null);
string s = ((IPlugin)instance).Execute("test");
Console.WriteLine("Return from call is " + s);
}
}
当完成这些操作后,可以使用AppDomain.Unload
来卸载域:
AppDomain.Unload(appDomainPluginB);
这将能够正常工作,因为没有任何程序集泄露到主plugins
应用程序域中——它们没有被链接,所以可以被释放。
static void LoadPluginsInsideNewDomainUsingMarshalByRef()
{
AppDomain appDomainPluginB = AppDomain.CreateDomain("appDomainPluginB");
RemoteLoader loader = (RemoteLoader)appDomainPluginB.CreateInstanceAndUnwrap("plugins", "plugins.RemoteLoader");
loader.LoadAndExecute("pluginB");
AppDomain.Unload(appDomainPluginB);
}
public class RemoteLoader : MarshalByRefObject
{
public void LoadAndExecute(string assemblyName)
{
Assembly pluginAassembly = AppDomain.CurrentDomain.Load(assemblyName);
foreach (Type type in pluginAassembly.GetTypes())
{
if (type.GetInterface("IPlugin") != null)
{
object instance = Activator.CreateInstance(type, null, null);
string s = ((IPlugin)instance).Execute("test");
Console.WriteLine("Return from call is " + s);
}
}
}
}