插件式開發
思考一種情況,短信發送,默認實現中只寫了一種實現,因為某些原因該模塊的所依賴的第三方無法繼續提供服務,或者對於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