当前位置: 首页 > ds >正文

AssemblyLoadContext`的插件化架构


一、核心技术与原理

.NET Core 为实现热更新提供了坚实的基础,其核心在于 AssemblyLoadContext (程序集加载上下文) 类。

  • 默认行为:在传统模式下,当一个程序集(DLL)被加载到默认的 AppDomain (在 .NET Core 中更为精简) 后,它无法被卸载。文件也会被锁定,无法覆盖。
  • 解决方案AssemblyLoadContext 允许你创建独立的、可卸载的加载上下文。你可以将插件或需要热更的 DLL 加载到独立的 AssemblyLoadContext 中。当需要更新时,你可以卸载整个 AssemblyLoadContext,从而释放所有资源和对 DLL 文件的锁定,然后重新加载新版本的 DLL。

实现热更新的两大核心模式:

  1. 插件化架构:将需要热更的模块设计为独立的插件。
  2. 代理模式:通过一个抽象的接口或基类来隔离具体实现,动态加载的实现类。

二、推荐方案:基于 AssemblyLoadContext 的插件化架构

这是最主流、最可控、最符合现代.NET架构的方案。

架构设计图
动态加载 & 调用
包含/加载
卸载后加载
主应用程序 Host App
共享契约项目 IMyService
自定义AssemblyLoadContext
实现项目 PluginV1.dll
实现项目 PluginV2.dll
实现步骤

第1步:创建项目结构

  1. HostApp (主应用程序,控制台/Web API均可)
    • 这是一个长期运行的进程,是程序的入口和容器。
  2. Contracts (类库)
    • 定义接口 IMyService。这是契约,是所有插件必须实现的接口。它保证了主程序和插件之间的解耦。
    • public interface IMyService { string GetMessage(); }
  3. PluginV1 (类库),PluginV2 (类库)
    • 引用 Contracts 项目。
    • 实现 IMyService 接口。
    • public class MyServiceV1 : IMyService { ... }

第2步:实现热更核心逻辑(在HostApp中)

using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader; // 核心命名空间// 1. 创建一个可卸载的自定义AssemblyLoadContext
public class SimpleUnloadableAssemblyLoadContext : AssemblyLoadContext
{// 必须重写此方法,提供程序集解析逻辑(例如,从指定路径加载)protected override Assembly Load(AssemblyName assemblyName){// 通常返回null,让其回落到默认的解析逻辑// 如果需要从特定插件目录解析所有依赖,可以在此实现return null;}public SimpleUnloadableAssemblyLoadContext() : base(isCollectible: true) {// `isCollectible: true` 表示这个上下文是可回收(卸载)的}
}// 2. 插件加载管理器
public class PluginManager
{private WeakReference _alcWeakRef; // 用于跟踪ALC,判断是否已被卸载private IMyService _serviceInstance;public IMyService LoadPlugin(string pluginPath){// 清理旧的ALC(如果存在)UnloadPlugin();// 创建新的可卸载ALCvar alc = new SimpleUnloadableAssemblyLoadContext();_alcWeakRef = new WeakReference(alc);// 使用新的ALC来加载程序集// 注意:这里使用alc.LoadFromAssemblyPath,而不是Assembly.LoadFromAssembly pluginAssembly = alc.LoadFromAssemblyPath(Path.GetFullPath(pluginPath));// 反射查找实现了IMyService的类型foreach (Type type in pluginAssembly.GetTypes()){if (typeof(IMyService).IsAssignableFrom(type) && !type.IsInterface){// 创建实例并转换为接口_serviceInstance = (IMyService)Activator.CreateInstance(type);Console.WriteLine($"Plugin loaded from {pluginPath}.");return _serviceInstance;}}throw new InvalidOperationException($"No type implementing {nameof(IMyService)} found in {pluginPath}");}public void UnloadPlugin(){if (_serviceInstance != null){_serviceInstance = null;}if (_alcWeakRef != null){// 触发卸载ALCif (_alcWeakRef.Target is AssemblyLoadContext alc){alc.Unload(); // 标记为可卸载}_alcWeakRef = null;}// 强制进行GC回收,释放ALC占用的资源(包括文件锁)// 这是关键一步,否则文件锁可能不会立即释放for (int i = 0; i < 10; i++){GC.Collect();GC.WaitForPendingFinalizers();}Console.WriteLine("Plugin unloaded.");}// 检查ALC是否已被卸载public bool IsUnloaded() => _alcWeakRef != null && !_alcWeakRef.IsAlive;}
}

第3步:主程序调用逻辑

class Program
{static PluginManager _pluginManager = new PluginManager();static FileSystemWatcher _fileWatcher;static readonly string PluginPath = @"./plugins/PluginV1.dll"; // 初始版本static readonly string PluginDir = @"./plugins/";static readonly string PluginName = "PluginV1.dll";static void Main(string[] args){// 初始加载插件var service = _pluginManager.LoadPlugin(PluginPath);Console.WriteLine(service.GetMessage());// 设置文件监视:当DLL被更新时触发热更_fileWatcher = new FileSystemWatcher(PluginDir, PluginName);_fileWatcher.Changed += OnPluginChanged;_fileWatcher.EnableRaisingEvents = true;_fileWatcher.NotifyFilter = NotifyFilters.LastWrite;Console.WriteLine("Watching for plugin changes. Press 'q' to quit.");while (Console.ReadKey().KeyChar != 'q') ;_fileWatcher.Dispose();_pluginManager.UnloadPlugin();}// 文件变化事件处理private static async void OnPluginChanged(object sender, FileSystemEventArgs e){// 文件写入通常会触发多次事件,需要防抖_fileWatcher.EnableRaisingEvents = false;Console.WriteLine($"\nDetected change in {e.FullPath}. Attempting hot reload...");await Task.Delay(500); // 等待文件写入完成try{// 卸载旧插件 -> 加载新插件 -> 调用新逻辑var newService = _pluginManager.LoadPlugin(e.FullPath);Console.WriteLine("Hot reload successful!");Console.WriteLine(newService.GetMessage()); // 验证新逻辑}catch (Exception ex){Console.WriteLine($"Hot reload failed: {ex.Message}");// 此处应有回滚策略,例如重新加载旧版本DLL}finally{_fileWatcher.EnableRaisingEvents = true;}}
}

三、生产环境增强考虑

上述方案是核心 demo,在生产环境中你需要考虑更多:

  1. 依赖管理

    • 如果插件依赖第三方 NuGet 包,需要确保主程序不会加载重复且版本冲突的依赖。可以在自定义 ALCLoad 方法中精确控制依赖项的加载路径。
  2. 版本控制与回滚

    • 不要直接覆盖正在运行的 DLL。应该采用版本化部署(如 Plugin-1.0.0.dll, Plugin-1.0.1.dll)。
    • FileSystemWatcher 监视一个软链接(如 current-plugin.dll),更新时只需将软链接指向新版本的文件。这样回滚只需修改链接指向,非常安全。
  3. 健壮性与监控

    • 热更失败不应该导致主进程崩溃。必须有 try-catch 和回滚机制。
    • 更新前,可以对新的 DLL 进行预加载和预验证,确保其是有效的程序集且实现了所需接口。
  4. 信号触发而非文件监视

    • 在生产环境中,使用 FileSystemWatcher 可能不可靠。更常见的做法是通过:
      • HTTP API 端点:例如 POST /api/plugin/reload
      • 配置中心:监听配置变化(如 Consul, Apollo)。
      • 消息队列:接收重新加载的指令。
  5. 内存泄漏

    • 确保在卸载 ALC 后,主程序中没有任何对插件中对象的静态引用事件挂钩,否则 ALC 将无法被正确卸载,导致内存泄漏。使用 WeakReference 是很好的实践。

四、其他方案对比

方案优点缺点适用场景
AssemblyLoadContext官方推荐,控制力强,隔离性好,支持卸载需要一定的架构设计,需处理依赖问题大多数生产环境,复杂的插件系统
dotnet watch简单,无需代码仅用于开发环境,通过重启实现"热更"本地开发,快速迭代
AppDomain (仅.NET FX).NET Framework 时代的方案.NET (Core) 5+ 中支持极其有限,不推荐旧版.NET Framework项目

总结

实施要点:

  1. 契约先行:严格定义接口(Contracts),实现主机与插件的完全解耦。
  2. 隔离加载:为每个插件或插件集创建独立的、可卸载的 AssemblyLoadContext
  3. 稳健更新:采用版本化部署和原子性切换(如软链接),并配备完善的回滚机制。
  4. 全面监控:对热更过程进行监控和日志记录,确保操作可观测。

这套方案既能满足业务零停机的高要求,又能保证系统的稳定性和可维护性,是经过实践检验的成熟架构模式。

http://www.xdnf.cn/news/19575.html

相关文章:

  • Qt libcurl的下载、配置及简单测试 (windows环境)
  • springboot项目启动时打印maven打包时间
  • [Mysql数据库] 知识点总结8
  • 计算机网络:(十六)TCP 的运输连接管理
  • Ring Buffer解析
  • 仓颉语言Web框架中的路由分组
  • linux系统学习(6.软件包管理)
  • 十分钟快速掌握 YML YAML 文件
  • 07.《交换机三层功能、单臂路由与端口安全基础知识》
  • 在Linux环境安装Maven(保姆级别)
  • leetcode 面试题 01.01.判定字符是否唯一
  • 【高级】系统架构师 | 信息系统基础
  • 基于Seurat的空转单样本数据分析流程学习(一)
  • JavaScript中的XMLHttpRequest对象分析
  • 基于单片机智能保温杯/智能水杯
  • Java基础第7天总结(代码块、内部类、函数式编程)
  • 【多模态】使用LLM生成html图表
  • 打开多个Excel文件后快速关闭所有的文档,并且退出Excel应用
  • s[:] = reversed(s) 和 s = reversed(s)的区别
  • 【Proteus仿真】点亮小灯系列仿真——小灯闪烁/流水灯/交通灯
  • R3:适用于 .NET 的新一代响应式扩展库,事件订阅流
  • TFS-2002《Fuzzy Clustering With Viewpoints》
  • 嵌入式ARM程序高级调试技能:19.qumu arm elf无法生成coredump
  • 接口测试:如何定位BUG的产生原因
  • nginx-增加VTS模块
  • 数据结构八股
  • 数据结构(C语言篇):(八)栈
  • vscode+EIDE+Clangd环境导入keil C51以及MDK工程
  • shell脚本第六阶段---三剑客之sed
  • C++日志系统:高效异步日志实现解析