掘金 后端 ( ) • 2024-05-04 10:25

概述:

插件框架是一种软件设计模式,它允许开发者在不修改核心系统代码的情况下,通过添加或替换插件模块来扩展或改变系统的功能。这种设计模式在软件开发中非常常见,特别是在需要高度可定制化和可扩展性的系统中。今天给大家推荐一个 .net 中非常好用的插件框架:Prise。 由于项目需要,本人刚刚接触过这款插件,期间也遇到了一些问题,踩了一些坑,因此特意将使用过程记录下来,让需要的朋友少走弯路。

项目说明

插件系统一般有这么几个角色:

  • 主机(Host)项目,也叫宿主项目,负责挂载插件,提供核心功能
  • 契约(Contract)项目,负责约定插件要实现的接口或抽象类,一般用接口
  • 插件(Plugin)项目,真正实现定制化的服务的项目,一般通过发布到指定目录,以供主机项目加载

下面我将通过简单的代码示例来介绍具体用法,本实例采用 .net8.0 实现,也可以采用 net6/7都是可以的。下图为本次示例的项目目录。

image.png

契约项目

创建一个类库项目,代码如下:

/// 契约服务
public interface IDataService
{
    Task<dynamic> GetDataAsync(dynamic input);
}

/// 插件桥,以供插件调用主机中的服务
/// 在主机实现真正的业务逻辑,而在插件中采用代理类进行调用,后面介绍
public interface IPluginBridge
{
    string GetConfig(string key);
}

主机项目

创建一个 webapi 项目,首先安装Nuget包:

<PackageReference Include="Prise" Version="6.0.0" />
<PackageReference Include="Prise.Plugin" Version="6.0.0" />
<PackageReference Include="Prise.Proxy" Version="6.1.0" />

在根目录下创建一个文件夹(_plugins),用于存放插件项目的发布文件。

接着创建一个帮助类,用于查找插件

public static class PluginHelper
{
    /// <summary>
    /// 在指定目录,寻找插件,这里的目录为主机跟目录下的 _plugins
    /// </summary>
    public static async Task<AssemblyScanResult> GetPlugin<T>(this IPluginLoader pluginLoader, string pluginName)
    {
        var path = Path.Combine(Path.GetFullPath("./"), "_plugins");
        var pluginResults = await pluginLoader.FindPlugins<T>(path);
        var pluginResult = pluginResults.FirstOrDefault(x => x.PluginType.Name == pluginName);
        if (pluginResult == null)
            throw new Exception($"plugin loader can not find {pluginName}");
        return pluginResult;
    }
}

实现插件桥

public class PluginBridgeProvider(IConfiguration configuration) : IPluginBridge
{
    public string GetConfig(string key)
    {
        return configuration[key] ?? "";
    }
}

然后添加一个控制器,作为本次测试的入口

[ApiController]
[Route("/api/[controller]/[action]")]
public class ValuesController(IPluginLoader pluginLoader, IPluginBridge pluginBridge) : ControllerBase
{
    [HttpGet]
    public async Task<dynamic> Get(string pluginName)
    {
        // 查找插件
        var pluginInfo = await pluginLoader.GetPlugin<IDataService>(pluginName);
        // 加载找到的插件
        var pluginService = await pluginLoader.LoadPlugin<IDataService>(pluginInfo,
            configure: (loadContext) =>
            {
                // 注册插件桥
                // 如果在插件中要使用主机服务,则这里需要将主机中的服务注入进来
                loadContext.AddHostService(pluginBridge);
            });

        // 调用插件提供的服务    
        return await pluginService.GetDataAsync(new { });
    }
}

项目启动时,配置 Prise,并注入插件桥服务

builder.Services.AddSingleton<IPluginBridge, PluginBridgeProvider>();
builder.Services.AddPrise();

插件项目

创建一个类库项目,安装Nuget包,引用契约项目

<PackageReference Include="Prise.Plugin" Version="6.0.0" />
<PackageReference Include="Prise.ReverseProxy" Version="6.1.0" />

添加插件服务类

[Plugin(PluginType = typeof(IDataService))]
public class OneDataService : IDataService
{
    //这个地方需要注意,如果在插件中需要使用主机服务,那必须写一个单独的插件内部服务类,这里为 IInternalService,下面会介绍
    //在那里调用注入到插件中的主机服务,而不能直接在这个类中直接调用
    //然后在这里引用内部服务,像下面这样:
    [PluginService(ServiceType = typeof(IInternalService))]
    private readonly IInternalService _internalService;

    public async Task<dynamic> GetDataAsync(dynamic input)
    {
        return await Task.FromResult(_internalService.GetContent(input.ToString()));
    }
}

主机服务代理类,继承自 ReverseProxy,实现插件桥

public class PluginBridgeProxy(object hostService) : ReverseProxy(hostService),
    IPluginBridge
{
    public string GetConfig(string key)
    {
        return InvokeOnHostService<string>(key);
    }
}

插件内部服务类

public interface IInternalService
{
    dynamic GetContent(string key);
}

// 这个 IPluginBridge 只能在插件内部服务中使用
public class InternalService(IPluginBridge pluginBridge) : IInternalService
{
    public dynamic GetContent(string key)
    {
        return new
        {
            Version = "v2",
            Content = pluginBridge.GetConfig(key)
        };
    }
}

添加插件配置类

// 这个 OneDataService 就是上面的插件服务实现类,主机测试的时候需要把这个插件名称传过来
[PluginBootstrapper(PluginType = typeof(OneDataService))]
public class Bootstrapper : IPluginBootstrapper
{
    [BootstrapperService(
        ServiceType = typeof(IPluginBridge), // The X.Contract.IPluginBridge interface
        ProxyType = typeof(PluginBridgeProxy))] // The ReverseProxy type that lives inside of this project
    private readonly IPluginBridge _pluginBridge;
    
    public IServiceCollection Bootstrap(IServiceCollection services)
    {
        services.AddScoped(sp => _pluginBridge);
        //内部服务注册
        services.AddScoped<IInternalService, InternalService>();

        return services;
    }
}

然后配置插件项目的发布目录,指定到主机项目的 _plugins 下,这里需要注意,在设置的时候,需要在指定插件目录后面多指定一级(OnePlugin),否则会提示找不到插件,这里相对路径为:/MyPrise.Host/_plugins/OnePlugin, 如下图所示。配置好之后,发布一下插件项目

image.png 至此,所有示例代码写完了,接下来进行测试

测试

运行主机项目,输入插件名称进行测试

image.png 可以看到得到了结果, 插件框架最打的好处就是,修改插件项目,不需要重启主机服务,真正实现了热插拔效果,大家可以根据上面步骤试一下,看看能否实现热插拔,我在这里就不在演示。