.net core 插件式開發


插件式開發

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

對於插件卸載,我認為沒有必要去考慮,對於同一類型插件,只需要將不同版本的放到不同的位置,在一個公共位置維護當前使用的插件所在位置,如果有更新直接找最新的實現去執行就行,卸載很麻煩,需要刪除掉所有的依賴項,還容易出錯,不解決就是最好的解決方案


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM