赞
踩
顾名思义,插件化开发就是将某个功能代码封装为一个插件模块,通过插件中心的配置来下载、激活、禁用、或者卸载,主程序无需再次重启即可获取新的功能,从而实现快速集成。当然,实现这样的效果,必须遵守一些插件接口的标准,不能与已有的功能冲突。目前能支持插件化开发的成熟框架很多,但本文仅从思路的实现角度,从0到1实现简单的插件化开发框架。
思路:定义插件接口 -> 实现插件接口 -> 通过反射机制加载插件 -> 调用插件方法。
开发语言:支持反射机制的所有高级语言均可实现插件式开发,或有 FFI 调用 Native 函数的编程语言。
定义插件接口:一个执行方法
-
- package service;
-
- /**
- * 通用插件接口
- *
- * @author yushanma
- * @since 2023/3/5 16:36
- */
- public interface IPluginService {
- /**
- * 执行插件
- */
- public void run();
- }
-
- package impl;
- import service.IPluginService;
-
- /**
- * 打印插件
- *
- * @author yushanma
- * @since 2023/3/5 16:37
- */
- public class MyPrinterPlugin implements IPluginService {
-
- @Override
- public void run() {
- System.out.println("执行插件方法...");
- }
- }
管理与加载插件。
-
- package entity;
-
- import lombok.Data;
-
- /**
- * 插件实体类
- *
- * @author yushanma
- * @since 2023/3/5 16:44
- */
- @Data
- public class PluginEntity {
- /**
- * 插件名
- */
- private String pluginName;
-
- /**
- * 插件路径
- */
- private String jarPath;
-
- /**
- * 字节码名字
- */
- private String className;
- }
需要获取插件名、插件实现的Jar包路径、字节码路径
-
- package loader;
-
- import entity.PluginEntity;
- import exception.PluginException;
- import lombok.Data;
- import lombok.NoArgsConstructor;
- import service.IPluginService;
-
- import java.io.File;
- import java.net.URL;
- import java.net.URLClassLoader;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
-
- /**
- * 插件管理器
- *
- * @author yushanma
- * @since 2023/3/5 16:44
- */
- @Data
- @NoArgsConstructor
- public class PluginManager {
- private Map<String, Class<?>> clazzMap = new HashMap<>();
-
- public PluginManager(List<PluginEntity> plugins) throws PluginException {
- initPlugins(plugins);
- }
-
- public void initPlugin(PluginEntity plugin) throws PluginException {
- try {
- //URL url = new URL("file:" + plugin.getJarPath());
- URL url = new File(plugin.getJarPath()).toURI().toURL();
- URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
- Class<?> clazz = classLoader.loadClass(plugin.getClassName());
- clazzMap.put(plugin.getClassName(), clazz);
- } catch (Exception e) {
- throw new PluginException("plugin " + plugin.getPluginName() + " init error: >>> " + e.getMessage());
- }
- }
-
- public void initPlugins(List<PluginEntity> plugins) throws PluginException {
- for (PluginEntity plugin : plugins) {
- initPlugin(plugin);
- }
- }
-
- public IPluginService getInstance(String className) throws PluginException {
- Class<?> clazz = clazzMap.get(className);
- Object instance = null;
- try {
- instance = clazz.newInstance();
- } catch (Exception e) {
- throw new PluginException("plugin " + className + " instantiate error," + e.getMessage());
- }
- return (IPluginService) instance;
- }
- }
-
- <dependency>
- <groupId>org.dom4j</groupId>
- <artifactId>dom4j</artifactId>
- <version>2.1.1</version>
- </dependency>
-
- package conf;
-
- import entity.PluginEntity;
- import exception.PluginException;
- import org.dom4j.Document;
- import org.dom4j.Element;
- import org.dom4j.io.SAXReader;
-
- import java.io.File;
- import java.util.ArrayList;
- import java.util.List;
-
- /**
- * 解析 XML 插件配置
- *
- * @author yushanma
- * @since 2023/3/5 16:44
- */
- public class PluginXmlParser {
-
- public static List<PluginEntity> getPluginList() throws PluginException {
-
- List<PluginEntity> list = new ArrayList<>();
-
- SAXReader saxReader = new SAXReader();
- Document document = null;
- try {
- document = saxReader.read(new File("src/main/resources/plugin.xml"));
- } catch (Exception e) {
- throw new PluginException("read plugin.xml error," + e.getMessage());
- }
- Element root = document.getRootElement();
- List<?> plugins = root.elements("plugin");
- for (Object pluginObj : plugins) {
- Element pluginEle = (Element) pluginObj;
- PluginEntity plugin = new PluginEntity();
- plugin.setPluginName(pluginEle.elementText("name"));
- plugin.setJarPath(pluginEle.elementText("jar"));
- plugin.setClassName(pluginEle.elementText("class"));
- list.add(plugin);
- }
- return list;
- }
-
- }
- <!-- plugin.xml -->
- <?xml version="1.0" encoding="UTF-8"?>
- <plugins>
- <plugin>
- <name>测试插件</name>
- <jar>plugins/PrinterPlugin-1.0-SNAPSHOT.jar</jar>
- <class>impl.MyPrinterPlugin</class>
- </plugin>
- <plugin>
- <name>测试插件</name>
- <jar>plugins/PrinterPlugin-1.0-SNAPSHOT.jar</jar>
- <class>impl.MyPrinterPlugin</class>
- </plugin>
- </plugins>
-
- package loader;
-
- import conf.PluginXmlParser;
- import entity.PluginEntity;
- import exception.PluginException;
- import service.IPluginService;
-
- import java.util.List;
-
- /**
- * 插件加载器
- *
- * @author yushanma
- * @since 2023/3/5 16:44
- */
- public class PluginLoader {
-
- public void run() throws PluginException {
- // 从配置文件加载插件
- List<PluginEntity> pluginList = PluginXmlParser.getPluginList();
- PluginManager pluginManager = new PluginManager(pluginList);
-
- for (PluginEntity plugin : pluginList) {
- IPluginService pluginService = pluginManager.getInstance(plugin.getClassName());
- System.out.println("开始执行[" + plugin.getPluginName() + "]插件...");
- // 调用插件
- pluginService.run();
- System.out.println("[" + plugin.getPluginName() + "]插件执行完成");
- }
-
- // 动态加载插件
- // PluginEntity plugin = new PluginEntity();
- // plugin.setPluginName("");
- // plugin.setJarPath("");
- // plugin.setClassName("");
- // pluginManager.initPlugin(plugin);
- // IPluginService pluginService = pluginManager.getInstance("");
- // pluginService.run();
- }
- }
-
- import exception.PluginException;
- import loader.PluginLoader;
-
- /**
- * desc
- *
- * @author yushanma
- * @since 2023/3/5 16:44
- */
- public class DemoMain {
- public static void main(String[] args) throws PluginException {
- PluginLoader loader = new PluginLoader();
- loader.run();
- }
- }
通过 libloader 库可以调用动态链接库函数,需要 FFI 支持。
-
- cargo new --lib mydll
- // 有参数没有返回值
- #[no_mangle]
- pub fn println(str: &str) {
- println!("{}", str);
- }
-
- // 有参数有返回值
- #[no_mangle]
- pub fn add(a: usize, b: usize) -> usize {
- a + b
- }
-
- // 没有参数没有返回值
- #[no_mangle]
- pub fn print_hello() {
- println!("Hello");
- }
-
- // 字符串类型
- #[no_mangle]
- pub fn return_str(s1: &str) -> &str{
- s1
- }
- [package]
- name = "mydll"
- version = "0.1.0"
- edition = "2021"
-
- # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
- [dependencies]
-
-
- # rlib:Rust库,这是cargo new默认的种类,只能被Rust调用;
- # dylib:Rust规范的动态链接库,windows上编译成.dll,linux上编译成.so,也只能被Rust调用;
- # cdylib:满足C语言规范的动态链接库,windows上编译成.dll,linux上编译成.so,可以被其他语言调用
- # staticlib:静态库,windows上编译成.lib,linux上编译成.a,可以被其他语言调用
-
- [lib]
- crate-type = ["cdylib"]
cargo build
可以看到,所有的函数都被正常导出,具体原理请参考:So you want to live-reload Rust
- use cstr::cstr;
- use libloader::*;
- use std::{ffi::CStr,os::raw::c_char};
-
- fn main() {
- get_libfn!("dll/mydll.dll", "println", println, (), s: &str);
- println("你好");
-
- get_libfn!("dll/mydll.dll", "add", add, usize, a: usize, b: usize);
- println!(" 1 + 2 = {}", add(1, 2));
-
- get_libfn!("dll/mydll.dll", "print_hello", print_hello, bool);
- print_hello();
-
- get_libfn!("dll/mydll.dll","return_str", return_str,*const c_char, s: *const c_char);
- let str = unsafe { CStr::from_ptr(return_str(cstr!("你好 ").as_ptr())) };
- print!("out {}", str.to_str().unwrap());
- }
-
- namespace PluginInterface
- {
-
- public interface IPlugin
- {
- // 获取插件名字
- public string GetName();
-
- // 获取插件所提供的功能列表
- public string[] GetFunction();
-
- // 执行插件某个功能
- public bool Execute(string fn);
- }
-
- }
-
- using PluginInterface;
- using System;
- using System.Linq;
-
- namespace MyPlugin
- {
-
- public class PrinterPlugin : IPlugin
- {
- private static readonly string PLUGIN_NAME = "PrinterPlugin";
-
- // 获取插件名字
- public string GetName()
- {
- return PLUGIN_NAME;
- }
-
- // 获取插件所提供的功能列表
- public string[] GetFunction()
- {
- return PrinterFunc.FuncDics.Keys.ToArray();
- }
-
- // 执行插件某个功能
- public bool Execute(string fn)
- {
- return PrinterFunc.Run(fn);
- }
-
- // 传参功能
- public static object PrintLabel(string sn)
- {
- Console.WriteLine($"打印标签{sn}...DONE");
- return true;
- }
- }
- }
-
- using System;
- using System.Collections.Generic;
-
- namespace MyPlugin
- {
- // 封装打印机支持的功能
- internal class PrinterFunc
- {
- // 功能字典
- public static Dictionary<string, Func<bool>> FuncDics = new Dictionary<string, Func<bool>>
- {
- {"PrintPhoto",PrintPhoto },
- {"PrintDoc",PrintDoc }
- };
- // 执行某个功能
- public static bool Run(string name)
- {
- if (!FuncDics.ContainsKey(name))
- {
- return false;
- }
-
- return (bool)FuncDics[name].Invoke();
- }
- // 打印照片
- public static bool PrintPhoto()
- {
- Console.WriteLine("打印照片...DONE");
- return true;
- }
- // 打印文档
- public static bool PrintDoc()
- {
- Console.WriteLine("打印文档...DONE");
- return true;
- }
- }
-
- }
-
- using PluginInterface;
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Reflection;
-
- namespace CLI.Loader
- {
- public class PluginLoader
- {
- // 初始化时加载插件
- public PluginLoader()
- {
- LoadPlugin();
- }
-
- public Dictionary<string, IPlugin> ListName = new Dictionary<string, IPlugin>();
-
- // 加载所有插件
- public void LoadPlugin()
- {
- try
- {
- // 清除所有插件缓存
- ListName.Clear();
- // 插件文件夹
- string fileName = "D:\\AwsomeWorkSpace\\CLI\\Plugins\\net5.0\\";
- // 获取所有插件文件
- DirectoryInfo info = new DirectoryInfo(fileName);
- FileInfo[] files = info.GetFiles();
- foreach (FileInfo file in files)
- {
- if (!file.FullName.EndsWith(".dll"))
- {
- continue;
- }
- // 通过反射机制创建插件实例
- Assembly assembly = Assembly.LoadFile(file.FullName);
- Type[] types = assembly.GetTypes();
- foreach (Type type in types)
- {
- // 如果某些类实现了预定义的插件接口,则认为该类适配与主程序(是主程序的插件)
-
- if (type.GetInterface("IPlugin") != null)
- {
- // 创建该类实例
- IPlugin plugin = assembly.CreateInstance(type.FullName) as IPlugin;
- if (plugin == null)
- {
- throw new Exception("插件错误");
- }
- ListName.Add(plugin.GetName(), plugin);
- // 调用插件的某个传参方法
- MethodInfo printLabel = type.GetMethod("PrintLabel");
- object res = printLabel.Invoke(plugin, parameters: new object[] { "HQ31122222222222" });
- Console.WriteLine(res?.ToString());
- // 调用插件内部的 Execute 方法
- MethodInfo execute = type.GetMethod("Execute");
- res = execute.Invoke(plugin, parameters: new object[] { "PrintPhoto" });
- Console.WriteLine(res?.ToString());
- res = execute.Invoke(plugin, parameters: new object[] { "PrintDoc" });
- Console.WriteLine(res?.ToString());
- }
- }
- }
- }
- catch (Exception e)
- {
- Console.WriteLine(e.Message);
- }
- }
-
- // 插件启动
- public void Start()
- {
- Console.WriteLine("==== 插件中心 ====");
- Console.WriteLine("1--加载插件列表");
- Console.WriteLine("2--重新刷新插件");
- int switchVal = int.Parse(Console.ReadLine());
- switch (switchVal)
- {
- case 1:
- GetPluginList();
- break;
- case 2:
- LoadPlugin();
- break; ;
- }
-
- }
-
- // 加载插件列表
- public void GetPluginList()
- {
- Console.WriteLine("--------插件列表--------");
- foreach (var VARIABLE in ListName.Keys)
- {
- Console.WriteLine($"----{VARIABLE}");
- }
- Console.WriteLine("--------请输入插件名--------");
- GetPluginFunc(Console.ReadLine());
- }
-
- // 加载插件功能
- public void GetPluginFunc(string pluginName)
- {
- if (!ListName.ContainsKey(pluginName))
- {
- return;
- }
- IPlugin plugin = ListName[pluginName];
- string[] funcList = plugin.GetFunction();
- for (int i = 0; i < funcList.Length; i++)
- {
- Console.WriteLine(funcList[i]);
- plugin.Execute(funcList[i]);
- }
- }
-
-
- }
- }
ok,可以看到,插件化开发的实现并不复杂,但是其中用到的反射机制会消耗部分性能,并且 dll 也会存在一些逆向工程或者反向注入等信安问题,需要谨慎使用。当然,框架的完善更是任重道远的过程。
环境:Visual Studio 2022 / .NET7
参考:https://github.com/dotnet/runtime/tree/main/src/coreclr/nativeaot/docs
-
- dotnet new classlib -o mydll -f net6.0
- <Project Sdk="Microsoft.NET.Sdk">
-
- <PropertyGroup>
- <TargetFramework>net7.0</TargetFramework>
- <ImplicitUsings>enable</ImplicitUsings>
- <Nullable>enable</Nullable>
- <PublishAot>true</PublishAot>
- </PropertyGroup>
-
- </Project>
-
- using System.Runtime.InteropServices;
- using Seagull.BarTender.Print;
-
- namespace ClassLibrary1
- {
- public class Class1
- {
- // 无参数有返回值
- [UnmanagedCallersOnly(EntryPoint = "IsOk")]
- public static bool IsOk()
- {
- return true;
- }
- // 有参数无返回值
- [UnmanagedCallersOnly(EntryPoint = "MyPrinter")]
- public static void MyPrinter(IntPtr pString)
- {
-
- try
- {
- if (pString != IntPtr.Zero)
- {
- string str = new(Marshal.PtrToStringAnsi(pString));
- Console.WriteLine(str);
- }
- }
- catch (Exception e)
- {
- Console.WriteLine(">>> Exception " + e.Message);
- }
- }
- // 有参数有返回值
- [UnmanagedCallersOnly(EntryPoint = "MyConcat")]
- public static IntPtr MyConcat(IntPtr pString1, IntPtr pString2)
- {
- string concat = "";
-
- try
- {
- if (pString1 != IntPtr.Zero && pString2 != IntPtr.Zero)
- {
- string str1 = new(Marshal.PtrToStringAnsi(pString1));
- string str2 = new(Marshal.PtrToStringAnsi(pString2));
- concat = string.Concat(str1, str2);
- }
- }
- catch (Exception e)
- {
- concat = e.Message;
- }
- return Marshal.StringToHGlobalAnsi(concat);
- }
- // 无参数无返回值
- [UnmanagedCallersOnly(EntryPoint = "PrintHello")]
- public static void PrintHello()
- {
- Console.WriteLine(">>> Hello");
- }
- }
- }
-
- dotnet publish /p:NativeLib=Shared /p:SelfContained=true -r win-x64 -c release
可以看到 native 、publish 文件夹,里面的 dll 文件
函数正常导出,最后一个是默认导出的函数。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。