---
phase: 06-gateway-plugin-research
plan: 01
type: execute
wave: 1
depends_on: []
files_modified: []
autonomous: true
requirements: [PLUG-01, PLUG-02]
must_haves:
truths:
- "插件可以从指定目录动态加载"
- "插件在独立的 AssemblyLoadContext 中运行"
- "插件可以被卸载并释放内存"
artifacts:
- path: "src/yarpgateway/Plugins/PluginLoadContext.cs"
provides: "ALC 隔离机制"
- path: "src/yarpgateway/Plugins/PluginLoader.cs"
provides: "插件发现和加载"
- path: "src/yarpgateway/Plugins/PluginHost.cs"
provides: "插件生命周期管理"
- path: "tests/YarpGateway.Tests/Unit/Plugins/PluginLoadTests.cs"
provides: "加载/卸载验证"
key_links:
- from: "PluginLoader"
to: "PluginLoadContext"
via: "实例化并加载程序集"
- from: "PluginHost"
to: "PluginLoader"
via: "协调插件生命周期"
---
# 计划 01:插件加载基础设施
实现插件动态加载基础设施,包括 AssemblyLoadContext 隔离、插件发现和生命周期管理。
**目的:** 为网关提供安全的插件加载机制,支持隔离和热重载。
**产出:** 可工作的插件加载系统,含单元测试。
@/Users/mac/.config/opencode/get-shit-done/workflows/execute-plan.md
@/Users/mac/.config/opencode/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/006-gateway-plugin-research/006-RESEARCH.md
# 现有基础设施
@src/Fengling.Gateway.Plugin.Abstractions/IGatewayPlugin.cs
@src/yarpgateway/Program.cs
```csharp
// Fengling.Gateway.Plugin.Abstractions
public interface IGatewayPlugin
{
string Name { get; }
string Version { get; }
string? Description { get; }
Task OnLoadAsync();
Task OnUnloadAsync();
}
```
Task 1: 创建 PluginLoadContext 隔离机制
src/yarpgateway/Plugins/PluginLoadContext.cs,
tests/YarpGateway.Tests/Unit/Plugins/PluginLoadContextTests.cs
- Test 1: 加载插件程序集到独立 ALC
- Test 2: 共享契约程序集使用默认 ALC
- Test 3: 卸载 ALC 后内存被回收
创建可卸载的 AssemblyLoadContext:
1. 创建 `src/yarpgateway/Plugins/PluginLoadContext.cs`
2. 继承 AssemblyLoadContext,设置 isCollectible: true
3. 使用 AssemblyDependencyResolver 解析依赖
4. 关键:共享 `Fengling.Gateway.Plugin.Abstractions` 到默认 ALC
```csharp
public sealed class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
private readonly string _sharedAssemblyName = "Fengling.Gateway.Plugin.Abstractions";
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly? Load(AssemblyName assemblyName)
{
if (assemblyName.Name == _sharedAssemblyName)
return null; // 使用默认 ALC
var path = _resolver.ResolveAssemblyToPath(assemblyName);
return path != null ? LoadFromAssemblyPath(path) : null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
return path != null ? LoadUnmanagedDllFromPath(path) : IntPtr.Zero;
}
}
```
**注意**:先写测试,确保卸载验证通过。
dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoadContextTests" --no-build
- PluginLoadContext 类存在
- 测试验证隔离和卸载
- 构建通过
Task 2: 创建 PluginLoader 发现和加载逻辑
src/yarpgateway/Plugins/PluginLoader.cs,
src/yarpgateway/Plugins/DiscoveredPlugin.cs,
tests/YarpGateway.Tests/Unit/Plugins/PluginLoaderTests.cs
- Test 1: 从目录发现插件程序集
- Test 2: 加载插件并返回 IGatewayPlugin 实例
- Test 3: 处理无效插件(返回 null 或异常)
创建插件发现和加载器:
1. 创建 `DiscoveredPlugin.cs` 记录:
- Id, Name, Version, AssemblyPath, EntryPoint
2. 创建 `PluginLoader.cs`:
- `DiscoverPlugins(string directory)` - 扫描目录发现插件
- `LoadPlugin(DiscoveredPlugin discovered)` - 加载并实例化
- 使用 System.Reflection.Metadata 或简单扫描
```csharp
public class PluginLoader
{
public IEnumerable DiscoverPlugins(string pluginDirectory)
{
// 扫描 plugin.json 或程序集属性
}
public IGatewayPlugin? LoadPlugin(DiscoveredPlugin discovered)
{
var shadowPath = CreateShadowCopy(discovered.AssemblyPath);
var alc = new PluginLoadContext(shadowPath);
var assembly = alc.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(discovered.AssemblyPath)));
var type = assembly.GetType(discovered.EntryPoint);
return Activator.CreateInstance(type) as IGatewayPlugin;
}
}
```
**影子复制**:创建 `CreateShadowCopy` 方法,将插件复制到临时目录。
dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoaderTests" --no-build
- PluginLoader 类存在
- 可发现和加载插件
- 测试通过
Task 3: 创建 PluginHost 生命周期管理
src/yarpgateway/Plugins/PluginHost.cs,
src/yarpgateway/Plugins/PluginHandle.cs,
tests/YarpGateway.Tests/Unit/Plugins/PluginHostTests.cs
- Test 1: LoadAllAsync 加载目录下所有插件
- Test 2: UnloadAsync 卸载指定插件
- Test 3: GetPlugins 返回当前加载的插件
- Test 4: 插件卸载后 WeakReference 显示已回收
创建插件生命周期管理器:
1. 创建 `PluginHandle.cs`:
- 封装 PluginLoadContext 和 IGatewayPlugin
- 实现 IAsyncDisposable
- 提供 TrackUnloadability() 返回 WeakReference
```csharp
public sealed class PluginHandle : IAsyncDisposable
{
private readonly PluginLoadContext _alc;
private readonly IGatewayPlugin _plugin;
private readonly string _shadowDirectory;
private bool _disposed;
public IGatewayPlugin Plugin => _plugin;
public WeakReference TrackUnloadability() => new(_alc, trackResurrection: true);
public async ValueTask DisposeAsync()
{
if (_disposed) return;
_disposed = true;
await _plugin.OnUnloadAsync();
_alc.Unload();
}
}
```
2. 创建 `PluginHost.cs`:
- 注入 PluginLoader
- 管理插件字典 (name -> PluginHandle)
- 提供 LoadAllAsync, UnloadAsync, GetPlugins
```csharp
public class PluginHost
{
private readonly ConcurrentDictionary _plugins = new();
private readonly PluginLoader _loader;
public async Task LoadAllAsync(string pluginDirectory, CancellationToken ct = default)
{
var discovered = _loader.DiscoverPlugins(pluginDirectory);
foreach (var d in discovered)
{
var handle = _loader.LoadPlugin(d);
if (handle != null)
{
await handle.Plugin.OnLoadAsync();
_plugins[d.Id] = handle;
}
}
}
public async Task UnloadAsync(string pluginId)
{
if (_plugins.TryRemove(pluginId, out var handle))
{
await handle.DisposeAsync();
}
}
}
```
dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginHostTests" --no-build
- PluginHost 和 PluginHandle 类存在
- 可加载/卸载插件
- 卸载验证测试通过
1. dotnet build src/yarpgateway/YarpGateway.csproj 无错误
2. dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoad" 通过
3. 插件加载/卸载功能可用
- 插件可在独立 AssemblyLoadContext 中加载
- 插件可通过 WeakReference 验证卸载
- 所有单元测试通过