GoLang 插件化开发
Golang 插件化开发
Golang官方提供了plugin
模块,该模块可以支持插件开发.
目前很多思路都是在开发过程中支持插件话,当主体程序写完后,不能够临时绑定插件.但是本文将带领你进行主体程序自动识别并加载、控制插件调用.
代码地址: https://github.com/A-Donga/PluginTest
文章目录
- Golang 插件化开发
- 基本思路
- 具有模拟业务的主体程序
- 主体代码
- 简单的插件
- 插件代码
- 修改的主体代码
- 插件进阶-批量化
- 批量化
- 自动读取文件夹下的插件
- MainFile更新代码如下
- 插件装载
- 插件支持代码如下
- 增加插件进行测试
- 插件进阶-流程控制、原程序的方法调用
- 流程控制
- 调用原有程序的API
- 插件进阶-传参、更加优雅的调用
- 1. 上下文
- 2. 参数写死
基本思路
插件化开发中,一定存在一个主体程序,对其他插件进行控制、处理、调度.
具有模拟业务的主体程序
我们首先开发一个简单的业务程序,进行两种输出.
- 当时间秒数为
奇数
的时候,输出hello
- 当时间秒数为
偶数
的时候,输出world
主体代码
代码有一定的冗余,是为了模拟业务之间的调度
主文件名:MainFile.go
package mainimport ("fmt""time"
)/*
@author: mxd
@create time: 2020/10/5
*/// main 主体程序入口
func main() {// time.Now().Second 将会返回当前秒数nowSecond := time.Now().Second()doPrint(nowSecond)fmt.Println("Process Stop ========")
}// 执行打印操作
func doPrint(nowSecond int) {if nowSecond%2 == 0 {printWorld() //偶数} else {printHello() //奇数}
}// 执行打印hello
func printHello() {fmt.Println("hello")
}// 执行打印world
func printWorld() {fmt.Println("world")
}// init 函数将于 main 函数之前运行
func init() {fmt.Println("Process On ==========")
}
输出如下:
简单的插件
然后我们编写一个插件代码
插件代码的入口package
也要为main
,但是可以不包含main
方法
设定插件逻辑为当当前秒数为奇数的时候,同时输出当前时间(与hello的判定不是一个时间)
插件文件名:HelloPlugin.go
插件代码
package mainimport ("fmt""time"
)/*
@author: mxd
@create time: 2020/10/5
*/// 打印当前时间
func PrintNowTime(){fmt.Println(time.Now().Second())
}
在当前目录下,执行插件生成指令:
$ go build --buildmode=plugin -o HelloPlugin.so HelloPlugin.go
当前目录下就会多出来一个文件HelloPlugin.so
然后,我们让主程序加载该插件
修改的主体代码
package mainimport ("fmt""plugin""time"
)/*
@author: mxd
@create time: 2020/10/5
*/// main 主体程序入口
func main() {// time.Now().Second 将会返回当前秒数nowSecond := time.Now().Second()doPrint(nowSecond)fmt.Println("Process Stop ========")
}// 执行打印操作
func doPrint(nowSecond int) {if nowSecond%2 == 0 {printWorld() //偶数} else {printHello() //奇数}
}// 执行打印hello
func printHello() {// 执行插件调用if pluginFunc != nil{//将存储的信息转换为函数if targetFunc, ok := pluginFunc.(func()); ok {targetFunc()}}fmt.Println("hello")
}// 执行打印world
func printWorld() {fmt.Println("world")
}// 定义插件信息
const pluginFile = "HelloPlugin.so"// 存储插件中将要被调用的方法或变量
var pluginFunc plugin.Symbol// init 函数将于 main 函数之前运行
func init() {// 查找插件文件pluginFile, err := plugin.Open(pluginFile)if err != nil {fmt.Println("An error occurred while opening the plug-in")} else{// 查找目标函数targetFunc, err := pluginFile.Lookup("PrintNowTime")if err != nil {fmt.Println("An error occurred while search target func")}pluginFunc = targetFunc}fmt.Println("Process On ==========")
}
运行效果如下
如上,我们的主体文件已经写好,我们不需要再修改生成后的可执行文件,如果需要扩展代码,仅需要修改插件代码,然后生成so
文件替换即可.
插件进阶-批量化
批量化
我们需要考虑到一个问题,如果我们要支持很多的插件,一个一个写的化,很容易导致我们的主体文件膨胀,因为我们将插件文件写死,无法完成自动识别,因此,我们要为主体文件提供自动识别的功能,自动加载插件
自动读取文件夹下的插件
我们可以单独设置一个名为plugins
的文件夹来保存所有插件
首先我么在项目根目录创建一个文件夹plugins
我们将刚刚写好的插件代码移动到 plugins
文件夹下,同时为了符合golang标准布局,我们将主文件移动到cmd
文件夹下. 此时项目目录如下:
然后,在项目跟(与cmd、plugins同级)目录下新建一个文件,用来处理与业务无关的util.go
代码.
package PluginTestimport ("fmt""io/ioutil""path"
)/*
@author: mxd
@create time: 2020/10/5
*///FindFile 将会打开指定目录,并返回该目录下的所有文件
func FindFile(directoryPath string) []string {// 尝试打开文件夹baseFile, err := ioutil.ReadDir(directoryPath)if err != nil {fmt.Println("An error occurred while open file :[" + directoryPath + "] .")fmt.Println(err)return nil}// 定义返回数据var res []stringfor _, fileItem := range baseFile {// 文件夹类型继续递归查找if fileItem.IsDir() {// 加上前缀路径,合成正确的相对或绝对路径innerFiles := FindFile(path.Join(directoryPath, fileItem.Name()))// 合并结果集res = append(res, innerFiles...)} else {// 这里可以添加过滤,但是会提高方法的复杂度/*if path.Ext(fileItem.Name()) == ".so"{...}*/res = append(res, path.Join(directoryPath, fileItem.Name()))}}return res
}
然后修改MainFile文件,让主文件读取插件文件夹
MainFile更新代码如下
package mainimport ("PluginTest""fmt""time"
)/*
@author: mxd
@create time: 2020/10/5
*/// main 主体程序入口
func main() {// time.Now().Second 将会返回当前秒数nowSecond := time.Now().Second()doPrint(nowSecond)fmt.Println("Process Stop ========")
}// 执行打印操作
func doPrint(nowSecond int) {if nowSecond%2 == 0 {printWorld() //偶数} else {printHello() //奇数}
}// 执行打印hello
func printHello() {fmt.Println("hello")
}// 执行打印world
func printWorld() {fmt.Println("world")
}// init 函数将于 main 函数之前运行
func init() {// 读取plugin文件夹pluginsFiles := PluginTest.FindFile("plugins")for _, pluginItem := range(pluginsFiles){fmt.Println(pluginItem)}fmt.Println("Process On ==========")
}
运行结果如下
插件装载
插件装载很简单,但是让插件运行需要们指定一个函数,所有插件都要必须实现该方法,但是如果批量后,我们无法确定插件的运行时机,因此我们会在装载后,直接运行插件,测试我们的批量装载是可行的.
首先我们需要创建一个单独处理插件的文件pluginSupport.go
插件支持代码如下
package PluginTestimport ("fmt""path""plugin"
)/*
@author: mxd
@create time: 2020/10/5
*/// PluginItem 存储着插件的信息
type PluginItem struct {Name stringTargetFunc plugin.Symbol
}// 所有插件必须实现该方法
const TargetFuncName = "TargetFunc"// LoadAllPlugin 将会过滤一次传入的targetFile,同时将so后缀的文件装载,并返回一个插件信息集合
func LoadAllPlugin(targetFile []string) []PluginItem {var res []PluginItemfor _, fileItem := range targetFile {// 过滤插件文件if path.Ext(fileItem) == "so" {pluginFile, err := plugin.Open(fileItem)if err != nil {fmt.Println("An error occurred while load plugin : [" + fileItem + "]")fmt.Println(err)}//查找指定函数或符号targetFunc, err := pluginFile.Lookup(TargetFuncName)if err != nil {fmt.Println("An error occurred while search target func : [" + fileItem + "]")fmt.Println(err)}//采集插件信息pluginInfo := PluginItem{Name: fileItem,TargetFunc: targetFunc,}// 进行调用if f, ok := targetFunc.(func()); ok {f()}res = append(res, pluginInfo)}}return res
}
修改main
函数,使主函数支持该逻辑调用
// init 函数将于 main 函数之前运行
func init() {// 读取plugin文件夹pluginsFiles := PluginTest.FindFile("plugins")// 装载插件pluginItems := PluginTest.LoadAllPlugin(pluginsFiles)fmt.Println(pluginItems)fmt.Println("Process On ==========")
}
修改插件代码,使其具有TargetFunc
方法
package mainimport ("fmt""time"
)/*
@author: mxd
@create time: 2020/10/5
*/func TargetFunc(){PrintNowTime()
}// 打印当前时间
func PrintNowTime(){fmt.Println(time.Now().Second())
}
生成so文件
$ cd plugins
$ go build --buildmode=plugin -o HelloPlugin.so HelloPlugin.go
然后运行主文件
增加插件进行测试
增加插件,但是不修改主代码逻辑
该插件实现逻辑如下:
- 当前时间秒数< 30 : 打印
你
- 当前时间秒数>=30: 打印
好
创建文件:TestPlugin.go
由于两个代码中都含有同样的方法TargetFunc
,编辑器会报错,所以将HelloPlugin.go
文件中的相关代码注释掉即可(不执行go build命令)
实际开发过程中,插件和主体程序是不会混在一起的,但是这里考虑方便才写到一起的
代码如下:
package mainimport ("fmt""time"
)/*
@author: mxd
@create time: 2020/10/5
*/func TargetFunc() {nowSecond := time.Now().Second()if nowSecond % 2 == 0{fmt.Println("好")} else {fmt.Println("你")}
}
然后生成so文件:
$ cd plugins
$ go build --buildmode=plugin -o TestPlugin.so TestPlugin.go
然后,不用修改任何代码,直接运行主文件
可以看到,插件已经加载成功,并被执行
致此,我们已经能够自动加载、执行插件了.
插件进阶-流程控制、原程序的方法调用
流程控制
上一进阶最后面,我们发现了一个问题,我们只能调用一个方法,而且无法控制插件的调用时机,那么我们在插件中,写入一些信息,让主体程序识别,然后在合适的时候进行调用.
首先,我们声明一个插件信息结构体,所有插件填写正确的插件信息,才能被调用
我们的主体应用流程如下:
- 获取当前时间
- 调用打印函数进行打印
- 分别打印
所以我们定义如下插件共享信息
package PluginTest/*
@author: mxd
@create time: 2020/10/5
*/
// 定义主体程序流程const (GetTimeActive = "get_time_active" //获取时间的流程DoPrintActive = "do_print_active" //执行打印的流程PrintItemActive = "print_item_active" //执行分别打印
)// 存储插件信息
type PluginBaseInfo struct {Name string // 插件名称ActiveFlag string // 插件执行的位置ActivePoint bool // 插件的执行点Functions string // 插件可用函数// 对于可用函数,可以写为数组,来暴露更多的方法,使用一些信息标注调用时间// 使用更细的粒度控制插件暴露的API
}
我们首先修改第一个插件信息(HelloPlugin.go)
代码如下:
package mainimport ("PluginTest""fmt""time"
)/*
@author: mxd
@create time: 2020/10/5
*/var PluginBaseInfo = PluginTest.PluginBaseInfo{Name: "hello plugin",ActiveFlag: PluginTest.PrintItemActive,ActivePoint: true, //目标任务执行前运行Functions: "PrintNowTime", //可用函数名
}// 打印当前时间
func PrintNowTime() {fmt.Println(time.Now().Second())
}
修改主体程序,使其支持插件运行控制
首先修改PluginSupport.go,使其能够获取插件更多的信息,同时添加一个方法,控制调用流程
package PluginTestimport ("fmt""path""plugin"
)/*
@author: mxd
@create time: 2020/10/5
*/// PluginItem 存储着插件的信息
type PluginItem struct {Name stringPluginBaseInfo PluginBaseInfoPluginItem *plugin.Plugin
}// 所有插件必须实现该方法
const BaseInfo = "PluginBaseInfo"// LoadAllPlugin 将会过滤一次传入的targetFile,同时将so后缀的文件装载,并返回一个插件信息集合
func LoadAllPlugin(targetFile []string) []PluginItem {var res []PluginItemfor _, fileItem := range targetFile {// 过滤插件文件if path.Ext(fileItem) == ".so" {pluginFile, err := plugin.Open(fileItem)if err != nil {fmt.Println("An error occurred while load plugin : [" + fileItem + "]")fmt.Println(err)}//查找指定函数或符号targetFunc, err := pluginFile.Lookup(BaseInfo)if err != nil {fmt.Println("An error occurred while search target info : [" + fileItem + "]")fmt.Println(err)}baseInfo, ok := targetFunc.(*PluginBaseInfo)if !ok {fmt.Println("Can find base info.")}//采集插件信息pluginInfo := PluginItem{Name: fileItem,PluginBaseInfo: *baseInfo,PluginItem: pluginFile,}res = append(res, pluginInfo)}}return res
}// DoInvokePlugin 会根据当前状态执行插件调用
func DoInvokePlugin(pluginsItems [] PluginItem, nowActive string, nowPoint bool){for _, pluginItem := range pluginsItems{// 判断流程if pluginItem.PluginBaseInfo.ActiveFlag == nowActive{// 判断执行点if nowPoint == pluginItem.PluginBaseInfo.ActivePoint{funcName := pluginItem.PluginBaseInfo.FunctionsfuncItem, err := pluginItem.PluginItem.Lookup(funcName)if err != nil{fmt.Println("Can't find target func in [" + pluginItem.Name +"].")continue}if f, ok := funcItem.(func()); ok{f()}}}}
}
修改主文件,添加流程定义,当然,你可以利用上下文让流程定更优雅些
package mainimport ("PluginTest""fmt""time"
)/*
@author: mxd
@create time: 2020/10/5
*/// 存储所有插件信息
var PluginItems []PluginTest.PluginItem// main 主体程序入口
func main() {// time.Now().Second 将会返回当前秒数PluginTest.DoInvokePlugin(PluginItems, PluginTest.GetTimeActive, true)nowSecond := time.Now().Second()PluginTest.DoInvokePlugin(PluginItems, PluginTest.GetTimeActive, false)PluginTest.DoInvokePlugin(PluginItems, PluginTest.DoPrintActive, true)doPrint(nowSecond)PluginTest.DoInvokePlugin(PluginItems, PluginTest.DoPrintActive, false)fmt.Println("Process Stop ========")
}// 执行打印操作
func doPrint(nowSecond int) {PluginTest.DoInvokePlugin(PluginItems, PluginTest.PrintItemActive, true)if nowSecond%2 == 0 {printWorld() //偶数} else {printHello() //奇数}PluginTest.DoInvokePlugin(PluginItems, PluginTest.PrintItemActive, false)
}// 执行打印hello
func printHello() {fmt.Println("hello")
}// 执行打印world
func printWorld() {fmt.Println("world")
}// init 函数将于 main 函数之前运行
func init() {// 读取plugin文件夹pluginsFiles := PluginTest.FindFile("plugins")// 装载插件PluginItems = PluginTest.LoadAllPlugin(pluginsFiles)fmt.Println("Process On ==========")
}
编译HelloPlugin.go插件
运行主文件
调用原有程序的API
直接import原代码,然后调用即可
我们尝试利用FindFile函数,输出当前目录下的所有文件
TestPlugin.go代码:
package mainimport ("PluginTest""fmt""time"
)/*
@author: mxd
@create time: 2020/10/5
*/var PluginBaseInfo = PluginTest.PluginBaseInfo{Name: "test plugin",ActiveFlag: PluginTest.DoPrintActive,ActivePoint: false, //目标任务执行前运行Functions: "TargetFunc", //可用函数名
}func TargetFunc() {nowSecond := time.Now().Second()if nowSecond%2 == 0 {fmt.Println("好")} else {fmt.Println("你")}files := PluginTest.FindFile("./")for _, fileItem := range files {fmt.Println(fileItem)}
}
编译、运行主文件
我们在此过程中,并未修改主要逻辑代码,却对行为进行了修改.
插件进阶-传参、更加优雅的调用
这里已经偏离的插件的控制了,已经涉及到模块的控制了.
对于不通的项目,已经无法依靠小幅度修改进行适配了,因此这里仅提供几种思路,不提供具体的逻辑实现.
1. 上下文
依靠程序上下文,我们可以做很多事情
我们让所有插件都接受上下文作为参数,而上下文对于插件和主体程序是共享的,因此可以依靠上下文传递变量、或者更多的信息.
日志体系完全可以依靠这种方式植入,同时,插件能够控制更多的行为和数据.
我们还可以依靠上下文控制插件能够调用的方法.
我们在每一次调用方法的时候,使用包装器或者其他手段让上下文自动更新,而上下文更新的同时去调用插件,这样,我们就和插件降耦了,而且,本身上下文也可以作为参数,提供给程序主体进行调用控制,所以我们是和上下文耦合的.
2. 参数写死
这样做的好处是,快速开发,如果我们按照方法1的方式进行开发,整个应用会变得特别臃肿:上下文、插件、流程、静态变量等众多模块将会被引入.
但是缺点也显而易见,不论是主体应用还是插件本身的维护成本很高.
GoLang 插件化开发相关推荐
- TinyFrame升级之八:实现简易插件化开发
本章主要讲解如何为框架新增插件化开发功能. 在.net 4.0中,我们可以在Application开始之前,通过PreApplicationStartMethod方法加载所需要的任何东西.那么今天我们 ...
- Android插件化开发之解决OpenAtlas组件在宿主的注冊问题
Android插件化开发之解决OpenAtlas组件在宿主的注冊问题 OpenAtlas有一个问题,就是四大组件必须在Manifest文件里进行注冊,那么就必定带来一个问题,插件中的组件都要反复在宿主 ...
- Android插件化开发之动态加载三个关键问题详解
本文摘选自任玉刚著<Android开发艺术探索>,介绍了Android插件化技术的原理和三个关键问题,并给出了作者自己发起的开源插件化框架. 动态加载技术(也叫插件化技术)在技术驱动型的公 ...
- Android插件化开发指南——Hook技术(一)【长文】
文章目录 1. 前言 2. 将外部dex加载到宿主app的dexElements中 3. 插件中四大组件的调用思路 4. Hook 2.1 对startActivity进行Hook 2.1.1 AMS ...
- Python为什么要使用包管理、插件化开发?
一.包管理 1.为什么使用包管理 目的是为了便于共享.为了更多项目调用使用,或者共享给别人,就需要打包,目的是为了复用. Pypi(Python Package Index)公共的模块存储中心.htt ...
- android中使用tmf框架插件化开发的问题
android中使用tmf框架插件化开发的问题 最近项目开发使用的是tmf框架,其中大多数都是通过源生和H5交互的方式来实现的,大体实现和别的三方框架是一样的,需要按照tmf的官方文档引入一些lib和 ...
- Android插件化开发之动态加载本地皮肤包进行换肤
Android插件化开发之动态加载本地皮肤包进行换肤 前言: 本文主要讲解如何用开源换肤框架 android-skin-loader-lib来实现加载本地皮肤包文件进行换肤,具体可自行参考框架原理进行 ...
- Android插件化开发指南——插件化技术简介
文章目录 1. 为什么需要插件化技术 2. 插件化技术的历史 3. 插件化实现思路 3.1 InfoQ:您在 GMTC 中的议题叫做<Android 插件化:从入门到放弃>,请问这个标题代 ...
- php 插件化开发模式,JavaScript_JavaScript插件化开发教程(六),一,开篇分析
今天这篇文章 - phpStudy...
JavaScript插件化开发教程(六) 一,开篇分析 今天这篇文章我们说点什么那?嘿嘿嘿.我们接着上篇文章对不足的地方进行重构,以深入浅出的方式来逐步分析,让大家有一个循序渐进提高的过程.废话少说, ...
- Android插件化开发指南——实践之仿酷狗音乐首页
文章目录 1. 前言 2. 布局分析 3. 底部导航栏的实现 4. 顶部导航栏和ViewPager+Fragment的关联 1. 前言 在Android插件化开发指南--2.15 实现一个音乐播放器A ...
最新文章
- 在苏州的一个超级棒的事情
- HDU 6155 Subsequence Count(矩阵乘法+线段树+基础DP)
- 【jmx】java jmx 获取 kafka topic的logStart LogEnd信息
- IE6 透明遮挡falsh解决方案
- 怎样区分直连串口线和交叉串口线?
- [Android Security] DEX文件格式分析
- modelsim 下载链接
- 设计模式23篇(VIP典藏版)
- 中国石油大学《化工原理二》第一阶段在线作业
- idea 行号栏太宽以及显示一些图标问题解决
- cox回归模型python实现_生存分析Cox回归模型(比例风险模型)的spss操作实例
- 中国计算机学会推荐学术会议/期刊(网络与信息安全部分)
- 深入浅出AT命令(5)-短信命令
- e-mobile服务器地址显示无法登陆,E-Mobile服务器安装设置手册.doc
- 790-C语言的数组元素下标为何从0开始?
- 忘记密码怎么启动计算机,电脑忘记密码如何重装系统?
- [算法学习]模拟退火算法(SA)、遗传算法(GA)、布谷鸟算法(CS)、人工蜂群算法(ABC)学习笔记---附MATLAB注释代码
- mac上 go build的二进制文件在Linux上运行提示cannot execute binary file或者-bash: ./sayHello: 无法执行二进制文件的解决方式
- WakeOnLAN(WOL)测试
- C# .CS后台调用JS函数