来源: .net core 插件式开发 – FreeTimeWorker – 博客园
插件式开发
思考一种情况,短信发送,默认实现中只写了一种实现,因为某些原因该模块的所依赖的第三方无法继续提供服务,或者对于winform程序,某按钮单击,需要在运行时增加额外的操作,或者替换目前使用的功能,对于类似这样的需求,可以考虑使用插件式的方式搭建框架,以实现更灵活的可拆卸动态增加功能。 .net core 中提供了一种热加载外部dll的方式,可以满足该类型的需求 AssemblyLoadContext
流程
1,定义针对系统中所有可插拔点的接口
2,针对接口开发插件/增加默认实现
3,根据需要,在运行时执行相应的逻辑
4,在动态载入dll时谨防内存泄漏
代码
1,定义接口
在单独的类库中定义针对插拔点的接口
public interface ICommand
{
string Name { get; }
string Description { get; }
int Execute();
}
2,开发插件
新建类库,引用接口所在的类库,值得注意的的是 CopyLocalLockFileAssemblies,表示将所有依赖项生成到生成目录,对于插件中有对其他项目或者类库有引用的这个属性是必须的,Private表示引用的类库为公共程序集,该属性默认为true,为使插件可以正确在运行时加载,该属性必须为 ** false **
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="10.1.1" />
<PackageReference Include="System.Text.Json" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Plugins\Plugins.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
</Project>
修改完类库中这两处的值以后添加类,继承自ICommand 将接口定义的方法和属性做相关的实现,如下
public class Class1 : ICommand
{
public string Name => "Classb";
public string Description => "Classb Description";
public int Execute()
{
var thisv = JsonSerializer.Serialize(this);
Assembly ass = typeof(AutoMapper.AdvancedConfiguration).Assembly;
Console.WriteLine(ass.FullName);
Console.WriteLine(thisv);
Console.WriteLine("111111111111111111111111111111111111111111");
return 10000;
}
}
3,根据需要在运行时执行相应逻辑
编写用于运行时 插件加载上下文, 该类主要负责将给定路径的dll加载到当前应用程序域,静态方法用户获取实现了插件接口的实例
public class PluginLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath,bool isCollectible) :base(isCollectible)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
//加载依赖项
protected override Assembly Load(AssemblyName assemblyName)
{
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
{
return LoadUnmanagedDllFromPath(libraryPath);
}
return IntPtr.Zero;
}
public static List<ICommand> CreateCommands(string[] pluginPaths)
{
List<Assembly> _assemblies = new List<Assembly>();
foreach (var pluginPath in pluginPaths)
{
string pluginLocation = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, pluginPath.Replace('\\', Path.DirectorySeparatorChar)));
var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(o => o.Location == pluginLocation);
//根据程序集的物理位置判断当前域中是否存在该类库,如果不存在就读取,如果存在就从当前程序域中读取,由于AssemblyLoadContext已经做了相应的上下文隔离
//,所以即便是名称一样位置一样也可以重复加载,执行也可以按照预期执行,但由于会重复加载程序集,就会造成内存一直增加导致内存泄漏
if (assembly == null)
{
PluginLoadContext pluginLoadContext = new PluginLoadContext(pluginLocation, true);
assembly = pluginLoadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation)));
}
_assemblies.Add(assembly);
}
var results = new List<ICommand>();
foreach (var assembly in _assemblies)
{
foreach (Type type in assembly.GetTypes())
{
if (typeof(ICommand).IsAssignableFrom(type))
{
ICommand result = Activator.CreateInstance(type) as ICommand;
if (result != null)
{
results.Add(result);
}
}
}
}
return results;
}
}
调用
try
{
//插件添加后,相应的位置保存下载
string[] pluginPaths = new string[]
{
"Plugin/PluginA/PluginA.dll",//将插件所在类库生成后的文件复制到PluginA下边
};
var i = 0;
while (true)
{
List<ICommand> commands = PluginLoadContext.CreateCommands(pluginPaths);
foreach (var command in commands)
{
Console.WriteLine(command.Name);
Console.WriteLine(command.Description);
Console.WriteLine(command.Execute());
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
Console.ReadKey();
图2中去掉了当前程序集中根据地址确定是否重新加载插件,可以看到内存的使用量在一直增加,最终一定会导致溢出。
对比图 1
对比图 2