当前位置:   article > 正文

插件化开发探索与实践_插件开发

插件开发

一、背景

顾名思义,插件化开发就是将某个功能代码封装为一个插件模块,通过插件中心的配置来下载、激活、禁用、或者卸载,主程序无需再次重启即可获取新的功能,从而实现快速集成。当然,实现这样的效果,必须遵守一些插件接口的标准,不能与已有的功能冲突。目前能支持插件化开发的成熟框架很多,但本文仅从思路的实现角度,从0到1实现简单的插件化开发框架。

二、实现思路

思路:定义插件接口 -> 实现插件接口 -> 通过反射机制加载插件 -> 调用插件方法。

开发语言:支持反射机制的所有高级语言均可实现插件式开发,或有 FFI 调用 Native 函数的编程语言。

三、Java 通过反射机制实现插件化开发

1、创建插件接口

定义插件接口:一个执行方法

  1. package service;
  2. /**
  3. * 通用插件接口
  4. *
  5. * @author yushanma
  6. * @since 2023/3/5 16:36
  7. */
  8. public interface IPluginService {
  9. /**
  10. * 执行插件
  11. */
  12. public void run();
  13. }

2、实现插件接口

  1. package impl;
  2. import service.IPluginService;
  3. /**
  4. * 打印插件
  5. *
  6. * @author yushanma
  7. * @since 2023/3/5 16:37
  8. */
  9. public class MyPrinterPlugin implements IPluginService {
  10. @Override
  11. public void run() {
  12. System.out.println("执行插件方法...");
  13. }
  14. }

3、插件中心

管理与加载插件。

Step 1、插件实体类封装

  1. package entity;
  2. import lombok.Data;
  3. /**
  4. * 插件实体类
  5. *
  6. * @author yushanma
  7. * @since 2023/3/5 16:44
  8. */
  9. @Data
  10. public class PluginEntity {
  11. /**
  12. * 插件名
  13. */
  14. private String pluginName;
  15. /**
  16. * 插件路径
  17. */
  18. private String jarPath;
  19. /**
  20. * 字节码名字
  21. */
  22. private String className;
  23. }

需要获取插件名、插件实现的Jar包路径、字节码路径

Step 2、通过反射机制实现插件实例化

  1. package loader;
  2. import entity.PluginEntity;
  3. import exception.PluginException;
  4. import lombok.Data;
  5. import lombok.NoArgsConstructor;
  6. import service.IPluginService;
  7. import java.io.File;
  8. import java.net.URL;
  9. import java.net.URLClassLoader;
  10. import java.util.HashMap;
  11. import java.util.List;
  12. import java.util.Map;
  13. /**
  14. * 插件管理器
  15. *
  16. * @author yushanma
  17. * @since 2023/3/5 16:44
  18. */
  19. @Data
  20. @NoArgsConstructor
  21. public class PluginManager {
  22. private Map<String, Class<?>> clazzMap = new HashMap<>();
  23. public PluginManager(List<PluginEntity> plugins) throws PluginException {
  24. initPlugins(plugins);
  25. }
  26. public void initPlugin(PluginEntity plugin) throws PluginException {
  27. try {
  28. //URL url = new URL("file:" + plugin.getJarPath());
  29. URL url = new File(plugin.getJarPath()).toURI().toURL();
  30. URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
  31. Class<?> clazz = classLoader.loadClass(plugin.getClassName());
  32. clazzMap.put(plugin.getClassName(), clazz);
  33. } catch (Exception e) {
  34. throw new PluginException("plugin " + plugin.getPluginName() + " init error: >>> " + e.getMessage());
  35. }
  36. }
  37. public void initPlugins(List<PluginEntity> plugins) throws PluginException {
  38. for (PluginEntity plugin : plugins) {
  39. initPlugin(plugin);
  40. }
  41. }
  42. public IPluginService getInstance(String className) throws PluginException {
  43. Class<?> clazz = clazzMap.get(className);
  44. Object instance = null;
  45. try {
  46. instance = clazz.newInstance();
  47. } catch (Exception e) {
  48. throw new PluginException("plugin " + className + " instantiate error," + e.getMessage());
  49. }
  50. return (IPluginService) instance;
  51. }
  52. }

Step 3、通过 XML 文件来配置管理插件

  1. <dependency>
  2. <groupId>org.dom4j</groupId>
  3. <artifactId>dom4j</artifactId>
  4. <version>2.1.1</version>
  5. </dependency>
  1. package conf;
  2. import entity.PluginEntity;
  3. import exception.PluginException;
  4. import org.dom4j.Document;
  5. import org.dom4j.Element;
  6. import org.dom4j.io.SAXReader;
  7. import java.io.File;
  8. import java.util.ArrayList;
  9. import java.util.List;
  10. /**
  11. * 解析 XML 插件配置
  12. *
  13. * @author yushanma
  14. * @since 2023/3/5 16:44
  15. */
  16. public class PluginXmlParser {
  17. public static List<PluginEntity> getPluginList() throws PluginException {
  18. List<PluginEntity> list = new ArrayList<>();
  19. SAXReader saxReader = new SAXReader();
  20. Document document = null;
  21. try {
  22. document = saxReader.read(new File("src/main/resources/plugin.xml"));
  23. } catch (Exception e) {
  24. throw new PluginException("read plugin.xml error," + e.getMessage());
  25. }
  26. Element root = document.getRootElement();
  27. List<?> plugins = root.elements("plugin");
  28. for (Object pluginObj : plugins) {
  29. Element pluginEle = (Element) pluginObj;
  30. PluginEntity plugin = new PluginEntity();
  31. plugin.setPluginName(pluginEle.elementText("name"));
  32. plugin.setJarPath(pluginEle.elementText("jar"));
  33. plugin.setClassName(pluginEle.elementText("class"));
  34. list.add(plugin);
  35. }
  36. return list;
  37. }
  38. }
  1. <!-- plugin.xml -->
  2. <?xml version="1.0" encoding="UTF-8"?>
  3. <plugins>
  4. <plugin>
  5. <name>测试插件</name>
  6. <jar>plugins/PrinterPlugin-1.0-SNAPSHOT.jar</jar>
  7. <class>impl.MyPrinterPlugin</class>
  8. </plugin>
  9. <plugin>
  10. <name>测试插件</name>
  11. <jar>plugins/PrinterPlugin-1.0-SNAPSHOT.jar</jar>
  12. <class>impl.MyPrinterPlugin</class>
  13. </plugin>
  14. </plugins>

Step 4、解析 XML 文件并加载插件

  1. package loader;
  2. import conf.PluginXmlParser;
  3. import entity.PluginEntity;
  4. import exception.PluginException;
  5. import service.IPluginService;
  6. import java.util.List;
  7. /**
  8. * 插件加载器
  9. *
  10. * @author yushanma
  11. * @since 2023/3/5 16:44
  12. */
  13. public class PluginLoader {
  14. public void run() throws PluginException {
  15. // 从配置文件加载插件
  16. List<PluginEntity> pluginList = PluginXmlParser.getPluginList();
  17. PluginManager pluginManager = new PluginManager(pluginList);
  18. for (PluginEntity plugin : pluginList) {
  19. IPluginService pluginService = pluginManager.getInstance(plugin.getClassName());
  20. System.out.println("开始执行[" + plugin.getPluginName() + "]插件...");
  21. // 调用插件
  22. pluginService.run();
  23. System.out.println("[" + plugin.getPluginName() + "]插件执行完成");
  24. }
  25. // 动态加载插件
  26. // PluginEntity plugin = new PluginEntity();
  27. // plugin.setPluginName("");
  28. // plugin.setJarPath("");
  29. // plugin.setClassName("");
  30. // pluginManager.initPlugin(plugin);
  31. // IPluginService pluginService = pluginManager.getInstance("");
  32. // pluginService.run();
  33. }
  34. }

4、测试效果

  1. import exception.PluginException;
  2. import loader.PluginLoader;
  3. /**
  4. * desc
  5. *
  6. * @author yushanma
  7. * @since 2023/3/5 16:44
  8. */
  9. public class DemoMain {
  10. public static void main(String[] args) throws PluginException {
  11. PluginLoader loader = new PluginLoader();
  12. loader.run();
  13. }
  14. }

四、Rust 通过 libloader 库实现插件化开发

通过 libloader 库可以调用动态链接库函数,需要 FFI 支持。

Step 1、创建 lib

  1. cargo new --lib mydll
  1. // 有参数没有返回值
  2. #[no_mangle]
  3. pub fn println(str: &str) {
  4. println!("{}", str);
  5. }
  6. // 有参数有返回值
  7. #[no_mangle]
  8. pub fn add(a: usize, b: usize) -> usize {
  9. a + b
  10. }
  11. // 没有参数没有返回值
  12. #[no_mangle]
  13. pub fn print_hello() {
  14. println!("Hello");
  15. }
  16. // 字符串类型
  17. #[no_mangle]
  18. pub fn return_str(s1: &str) -> &str{
  19. s1
  20. }

Step 2、toml 配置编译类型

  1. [package]
  2. name = "mydll"
  3. version = "0.1.0"
  4. edition = "2021"
  5. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
  6. [dependencies]
  7. # rlib:Rust库,这是cargo new默认的种类,只能被Rust调用;
  8. # dylib:Rust规范的动态链接库,windows上编译成.dll,linux上编译成.so,也只能被Rust调用;
  9. # cdylib:满足C语言规范的动态链接库,windows上编译成.dll,linux上编译成.so,可以被其他语言调用
  10. # staticlib:静态库,windows上编译成.lib,linux上编译成.a,可以被其他语言调用
  11. [lib]
  12. crate-type = ["cdylib"]

Step 3、编译为 dll

cargo build

可以看到,所有的函数都被正常导出,具体原理请参考:So you want to live-reload Rust

Step 4、动态加载 dll

  1. use cstr::cstr;
  2. use libloader::*;
  3. use std::{ffi::CStr,os::raw::c_char};
  4. fn main() {
  5. get_libfn!("dll/mydll.dll", "println", println, (), s: &str);
  6. println("你好");
  7. get_libfn!("dll/mydll.dll", "add", add, usize, a: usize, b: usize);
  8. println!(" 1 + 2 = {}", add(1, 2));
  9. get_libfn!("dll/mydll.dll", "print_hello", print_hello, bool);
  10. print_hello();
  11. get_libfn!("dll/mydll.dll","return_str", return_str,*const c_char, s: *const c_char);
  12. let str = unsafe { CStr::from_ptr(return_str(cstr!("你好 ").as_ptr())) };
  13. print!("out {}", str.to_str().unwrap());
  14. }

五、C# 通过反射机制实现插件化开发

Step 1、定义插件接口

  1. namespace PluginInterface
  2. {
  3. public interface IPlugin
  4. {
  5. // 获取插件名字
  6. public string GetName();
  7. // 获取插件所提供的功能列表
  8. public string[] GetFunction();
  9. // 执行插件某个功能
  10. public bool Execute(string fn);
  11. }
  12. }

Step 2、实现插件接口

  1. using PluginInterface;
  2. using System;
  3. using System.Linq;
  4. namespace MyPlugin
  5. {
  6. public class PrinterPlugin : IPlugin
  7. {
  8. private static readonly string PLUGIN_NAME = "PrinterPlugin";
  9. // 获取插件名字
  10. public string GetName()
  11. {
  12. return PLUGIN_NAME;
  13. }
  14. // 获取插件所提供的功能列表
  15. public string[] GetFunction()
  16. {
  17. return PrinterFunc.FuncDics.Keys.ToArray();
  18. }
  19. // 执行插件某个功能
  20. public bool Execute(string fn)
  21. {
  22. return PrinterFunc.Run(fn);
  23. }
  24. // 传参功能
  25. public static object PrintLabel(string sn)
  26. {
  27. Console.WriteLine($"打印标签{sn}...DONE");
  28. return true;
  29. }
  30. }
  31. }
  1. using System;
  2. using System.Collections.Generic;
  3. namespace MyPlugin
  4. {
  5. // 封装打印机支持的功能
  6. internal class PrinterFunc
  7. {
  8. // 功能字典
  9. public static Dictionary<string, Func<bool>> FuncDics = new Dictionary<string, Func<bool>>
  10. {
  11. {"PrintPhoto",PrintPhoto },
  12. {"PrintDoc",PrintDoc }
  13. };
  14. // 执行某个功能
  15. public static bool Run(string name)
  16. {
  17. if (!FuncDics.ContainsKey(name))
  18. {
  19. return false;
  20. }
  21. return (bool)FuncDics[name].Invoke();
  22. }
  23. // 打印照片
  24. public static bool PrintPhoto()
  25. {
  26. Console.WriteLine("打印照片...DONE");
  27. return true;
  28. }
  29. // 打印文档
  30. public static bool PrintDoc()
  31. {
  32. Console.WriteLine("打印文档...DONE");
  33. return true;
  34. }
  35. }
  36. }

Step 3、通过反射实例化插件

  1. using PluginInterface;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Reflection;
  6. namespace CLI.Loader
  7. {
  8. public class PluginLoader
  9. {
  10. // 初始化时加载插件
  11. public PluginLoader()
  12. {
  13. LoadPlugin();
  14. }
  15. public Dictionary<string, IPlugin> ListName = new Dictionary<string, IPlugin>();
  16. // 加载所有插件
  17. public void LoadPlugin()
  18. {
  19. try
  20. {
  21. // 清除所有插件缓存
  22. ListName.Clear();
  23. // 插件文件夹
  24. string fileName = "D:\\AwsomeWorkSpace\\CLI\\Plugins\\net5.0\\";
  25. // 获取所有插件文件
  26. DirectoryInfo info = new DirectoryInfo(fileName);
  27. FileInfo[] files = info.GetFiles();
  28. foreach (FileInfo file in files)
  29. {
  30. if (!file.FullName.EndsWith(".dll"))
  31. {
  32. continue;
  33. }
  34. // 通过反射机制创建插件实例
  35. Assembly assembly = Assembly.LoadFile(file.FullName);
  36. Type[] types = assembly.GetTypes();
  37. foreach (Type type in types)
  38. {
  39. // 如果某些类实现了预定义的插件接口,则认为该类适配与主程序(是主程序的插件)
  40. if (type.GetInterface("IPlugin") != null)
  41. {
  42. // 创建该类实例
  43. IPlugin plugin = assembly.CreateInstance(type.FullName) as IPlugin;
  44. if (plugin == null)
  45. {
  46. throw new Exception("插件错误");
  47. }
  48. ListName.Add(plugin.GetName(), plugin);
  49. // 调用插件的某个传参方法
  50. MethodInfo printLabel = type.GetMethod("PrintLabel");
  51. object res = printLabel.Invoke(plugin, parameters: new object[] { "HQ31122222222222" });
  52. Console.WriteLine(res?.ToString());
  53. // 调用插件内部的 Execute 方法
  54. MethodInfo execute = type.GetMethod("Execute");
  55. res = execute.Invoke(plugin, parameters: new object[] { "PrintPhoto" });
  56. Console.WriteLine(res?.ToString());
  57. res = execute.Invoke(plugin, parameters: new object[] { "PrintDoc" });
  58. Console.WriteLine(res?.ToString());
  59. }
  60. }
  61. }
  62. }
  63. catch (Exception e)
  64. {
  65. Console.WriteLine(e.Message);
  66. }
  67. }
  68. // 插件启动
  69. public void Start()
  70. {
  71. Console.WriteLine("==== 插件中心 ====");
  72. Console.WriteLine("1--加载插件列表");
  73. Console.WriteLine("2--重新刷新插件");
  74. int switchVal = int.Parse(Console.ReadLine());
  75. switch (switchVal)
  76. {
  77. case 1:
  78. GetPluginList();
  79. break;
  80. case 2:
  81. LoadPlugin();
  82. break; ;
  83. }
  84. }
  85. // 加载插件列表
  86. public void GetPluginList()
  87. {
  88. Console.WriteLine("--------插件列表--------");
  89. foreach (var VARIABLE in ListName.Keys)
  90. {
  91. Console.WriteLine($"----{VARIABLE}");
  92. }
  93. Console.WriteLine("--------请输入插件名--------");
  94. GetPluginFunc(Console.ReadLine());
  95. }
  96. // 加载插件功能
  97. public void GetPluginFunc(string pluginName)
  98. {
  99. if (!ListName.ContainsKey(pluginName))
  100. {
  101. return;
  102. }
  103. IPlugin plugin = ListName[pluginName];
  104. string[] funcList = plugin.GetFunction();
  105. for (int i = 0; i < funcList.Length; i++)
  106. {
  107. Console.WriteLine(funcList[i]);
  108. plugin.Execute(funcList[i]);
  109. }
  110. }
  111. }
  112. }

ok,可以看到,插件化开发的实现并不复杂,但是其中用到的反射机制会消耗部分性能,并且 dll 也会存在一些逆向工程或者反向注入等信安问题,需要谨慎使用。当然,框架的完善更是任重道远的过程。

六、.NET 6/7 导出非托管函数能力

环境:Visual Studio 2022 / .NET7

参考:https://github.com/dotnet/runtime/tree/main/src/coreclr/nativeaot/docs

Step 1、创建类库项目

  1. dotnet new classlib -o mydll -f net6.0

Step 2、配置 AOT Native

  1. <Project Sdk="Microsoft.NET.Sdk">
  2. <PropertyGroup>
  3. <TargetFramework>net7.0</TargetFramework>
  4. <ImplicitUsings>enable</ImplicitUsings>
  5. <Nullable>enable</Nullable>
  6. <PublishAot>true</PublishAot>
  7. </PropertyGroup>
  8. </Project>

Step 3、导出非托管函数

  1. using System.Runtime.InteropServices;
  2. using Seagull.BarTender.Print;
  3. namespace ClassLibrary1
  4. {
  5. public class Class1
  6. {
  7. // 无参数有返回值
  8. [UnmanagedCallersOnly(EntryPoint = "IsOk")]
  9. public static bool IsOk()
  10. {
  11. return true;
  12. }
  13. // 有参数无返回值
  14. [UnmanagedCallersOnly(EntryPoint = "MyPrinter")]
  15. public static void MyPrinter(IntPtr pString)
  16. {
  17. try
  18. {
  19. if (pString != IntPtr.Zero)
  20. {
  21. string str = new(Marshal.PtrToStringAnsi(pString));
  22. Console.WriteLine(str);
  23. }
  24. }
  25. catch (Exception e)
  26. {
  27. Console.WriteLine(">>> Exception " + e.Message);
  28. }
  29. }
  30. // 有参数有返回值
  31. [UnmanagedCallersOnly(EntryPoint = "MyConcat")]
  32. public static IntPtr MyConcat(IntPtr pString1, IntPtr pString2)
  33. {
  34. string concat = "";
  35. try
  36. {
  37. if (pString1 != IntPtr.Zero && pString2 != IntPtr.Zero)
  38. {
  39. string str1 = new(Marshal.PtrToStringAnsi(pString1));
  40. string str2 = new(Marshal.PtrToStringAnsi(pString2));
  41. concat = string.Concat(str1, str2);
  42. }
  43. }
  44. catch (Exception e)
  45. {
  46. concat = e.Message;
  47. }
  48. return Marshal.StringToHGlobalAnsi(concat);
  49. }
  50. // 无参数无返回值
  51. [UnmanagedCallersOnly(EntryPoint = "PrintHello")]
  52. public static void PrintHello()
  53. {
  54. Console.WriteLine(">>> Hello");
  55. }
  56. }
  57. }

Step 4、查看导出结果

  1. dotnet publish /p:NativeLib=Shared /p:SelfContained=true -r win-x64 -c release

可以看到 native 、publish 文件夹,里面的 dll 文件

函数正常导出,最后一个是默认导出的函数。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小舞很执着/article/detail/861064
推荐阅读
相关标签
  

闽ICP备14008679号