插件化开发入门
创始人
2024-05-29 02:03:01
0

一、背景

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

二、实现思路

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

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

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

1、创建插件接口

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

package service;/*** 通用插件接口** @author yushanma* @since 2023/3/5 16:36*/
public interface IPluginService {/*** 执行插件*/public void run();
}

2、实现插件接口

package impl;
import service.IPluginService;/*** 打印插件** @author yushanma* @since 2023/3/5 16:37*/
public class MyPrinterPlugin implements IPluginService {@Overridepublic void run() {System.out.println("执行插件方法...");}
}

3、插件中心

管理与加载插件。

Step 1、插件实体类封装

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包路径、字节码路径

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

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> clazzMap = new HashMap<>();public PluginManager(List 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 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;}
}

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

        org.dom4jdom4j2.1.1
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 getPluginList() throws PluginException {List 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;}}


测试插件plugins/PrinterPlugin-1.0-SNAPSHOT.jarimpl.MyPrinterPlugin测试插件plugins/PrinterPlugin-1.0-SNAPSHOT.jarimpl.MyPrinterPlugin

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

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 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();}
}

4、测试效果

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();}
}

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

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

Step 1、创建 lib

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
}

Step 2、toml 配置编译类型

[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"]

Step 3、编译为 dll

cargo build

可以看到,所有的函数都被正常导出,具体原理请参考:https://fasterthanli.me/articles/so-you-want-to-live-reload-rust

Step 4、动态加载 dll

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());
}

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

Step 1、定义插件接口

namespace PluginInterface
{public interface IPlugin{// 获取插件名字public string GetName();// 获取插件所提供的功能列表public string[] GetFunction();// 执行插件某个功能public bool Execute(string fn);}}

Step 2、实现插件接口

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> FuncDics = new Dictionary>{{"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;}}}

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

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 ListName = new Dictionary();// 加载所有插件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 也会存在一些逆向工程或者反向注入等信安问题,需要谨慎使用。当然,框架的完善更是任重道远的过程。

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

环境:Visual Studio 2022 / .NET7

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

Step 1、创建类库项目

dotnet new classlib -o mydll -f net6.0

Step 2、配置 AOT Native

net7.0enableenabletrue

Step 3、导出非托管函数

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");}}
}

Step 4、查看导出结果

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

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

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

相关内容

热门资讯

苏轼较有名诗词 苏轼较有名诗词  苏轼是宋代文学最高成就的代表,并在诗、词、散文、书、画等方面取得了很高的成就。下面...
重阳节的经典诗句古诗 重阳节古诗是一种语言文字艺术,它有艺术的基本特点,但又区别于书法、绘画、雕塑等视觉艺术。下面是小编给...
描写四季的古诗 描写四季的古诗集锦  在日常的学习、工作、生活中,许多人都接触过一些比较经典的古诗吧,古诗可分为古体...
描写荷花的超经典诗句 描写荷花的超经典诗句  荷花,我要赞美的不是你的美艳与色彩。我想说,不管气候多么炎热,地势多么险恶,...
描写昆明大观楼的诗 描写昆明大观楼的诗  在学习、工作乃至生活中,大家对古诗都再熟悉不过了吧,古诗作为一种诗歌体裁,指的...
离别祝福的诗句 离别祝福的诗句  人生路漫漫,你我相遇又分离。相聚总是短暂,分别却是久长,唯愿彼此的心儿能紧紧相随,...
赞美南街村的诗句参考 赞美南街村的诗句参考  (第一首)  春冬替换亦葱笼,奔富唯凭集体功。  全面腾飞强政企,亿元积累脱...
“昨夜星辰昨夜风,画楼西畔桂... “昨夜星辰昨夜风,画楼西畔桂堂东”这两句是用追忆的形式写出夜晚情侣相聚之况——昨天晚上,满天星斗,微...
赞美老师的经典诗句 赞美老师的经典诗句赞美老师的'经典诗句1  落红不是无情物,化作春泥更护花。  春蚕到死丝方尽,蜡炬...
诗经:頍弁   《诗经:頍弁》  有頍者弁,实维伊何?  尔酒既旨,尔肴既嘉。  岂伊异人?兄弟匪他。  茑与女...
表示感谢的诗句 表示感谢的诗句  在平日的学习、工作和生活里,大家都收藏过令自己印象深刻的诗句吧,诗句具有精炼含蓄的...
元宵佳节诗句 元宵佳节诗句  导语:送你无数灯笼,带去无尽的如意吉祥.送你一个睛空,好让你此生一路顺风.送你一轮圆...
写思念家乡的诗句 写思念家乡的诗句  为了生计我们常常选择背井离乡,而那份思乡之情也悄然而生。身在异地,最牵挂的`是那...
回到唐朝作文 回到唐朝作文  篇一:回到唐朝  虽然我现在生活在多姿多彩的科技时代里,但是我仍然向往繁盛的唐朝,因...
赞叹女子舞姿优美的古诗句 赞叹女子舞姿优美的古诗句  在平凡的学习、工作、生活中,许多人对一些广为流传的.古诗都不陌生吧,古诗...
举杯邀明月,对影成三人诗句出... 举杯邀明月,对影成三人诗句出处及意思  【诗句】举杯邀明月,对影成三人。  【出处】唐·李白《月下独...
形容失去爱情的诗句 形容失去爱情的诗句  待得百花成蜜后,为谁辛苦为谁甜。  东边日出西边雨,道是无晴却有晴。  关关雎...
描写植树节的诗句 关于描写植树节的诗句  用一棵幼苗,两瓢清水,三铲泥土,四分清风,五缕阳光,六两雨露,七钱星辉,八抹...
蓼莪,蓼莪诗经,蓼莪的意思,... 蓼莪,蓼莪诗经,蓼莪的意思,蓼莪赏析 -诗词大全 蓼莪 作者:诗经朝代:先秦 蓼蓼者莪,匪...
《静夜思》古诗原文 《静夜思》古诗原文  《静夜思》是唐代作者李白所作一首五言古诗。此诗描写了秋日夜晚,作者于屋内抬头望...