zoukankan      html  css  js  c++  java
  • .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="..PluginsPlugins.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

    对于插件卸载,我认为没有必要去考虑,对于同一类型插件,只需要将不同版本的放到不同的位置,在一个公共位置维护当前使用的插件所在位置,如果有更新直接找最新的实现去执行就行,卸载很麻烦,需要删除掉所有的依赖项,还容易出错,不解决就是最好的解决方案

  • 相关阅读:
    【洛谷 P5357】 【模板】AC自动机(二次加强版)(AC自动机,差分)
    【洛谷 P1659】 [国家集训队]拉拉队排练(manacher)
    【洛谷 P3224】 [HNOI2012]永无乡(Splay,启发式合并)
    HNOI2019退役祭
    类欧几里得算法模板
    乘法逆元模板
    $Luogu$ $P3130$ $[USACO15DEC]$ 计数 $Counting$ $Haybales$
    $Luogu$ $P2746$ $[USACO5.3]$ 校园网 $Network$ $of$ $Schools$
    $Luogu$ $P2243$ 电路维修
    $Luogu$ $P4667$ $[BalticOI$ $2011$ $Day1]$ 打开灯泡 $Switch$ $the$ $Lamp$ $On$
  • 原文地址:https://www.cnblogs.com/zzfstudy/p/14110361.html
Copyright © 2011-2022 走看看