上期实现了用户的注册登录效果,这期则是实现插件化开发,可以让你的作品更“灵活”。还有xml的相关配置
和上期用户注册登录相比,就多了个prepare.go,主要负责项目启动后的初始化,读取配置文件啊什么的
还有就是server.go增加了一个新的路由组(Extensiongroup)
var Extensiongroup = engine.Group("/extensions/")
用到的包仍然是老三样(这里不够加密,就只用两个)
github.com/gin-gonic/gin
github.com/oswaldoooo/octools@v1.1
首先是需要用到的全局变量
//这里可以换成os.Getenv(变量名),然后提前将你项目根目录设置为变量名,这里为了方便直接上具体路径了
var ROOTPATH = "/Users/oswaldo/dev/golang/newstart"
//默认监听端口,如果配置文件中的端口有问题,就用默认端口
var port = 8001
//插件匹配字符串表,避免用相同的pattern,导致冲突,后面用空struct是为了省空间
var ExtensionPatterMap = make(map[string]struct{})
//octools/toolsbox初始化日志文件,第一个参数为prefix,后面为日志文件的地址,请确保日志文件的目录存在,日志文件若没有会自己创建,但目录不存在就会报错(但不会退出程序)
var errorlog = toolsbox.LogInit("error", ROOTPATH+"/logs/error.log")
xml的struct和xml的格式
type cnfinfo struct {
XMLName xml.Name `xml:"studentmanager"`
Port int `xml:"port"`
Plugins plugin_info `xml:"plugin"`
}
type plugin_info struct {
XMLName xml.Name `xml:"plugin"`
Plugin_Info []struct {
XMLName xml.Name `xml:"plugin_info"`
ClassName string `xml:"classname"`
Plugin_Name string `xml:"plugin_name"`
} `xml:"plugin_info"`
}
这里缩进了就是儿子,没缩进就是兄弟,前提是要被包进去,port, plugin是studentmanager的儿子,两个plugin_info是plugin的儿子,以此类推。这里从上面struct可以看出plugin_info那里是数组,所以xml这里的plugin_info可以有多个
<studentmanager>
<port>9001port>
<plugin>
<plugin_info>
<classname>extensionclassname>
<plugin_name>pluginnameplugin_name>
plugin_info>
<plugin_info>
<classname>extensionclassname>
<plugin_name>pluginnameplugin_name>
plugin_info>
plugin>
studentmanager>
初始化
func init() {
content, err := ioutil.ReadFile(ROOTPATH + "/conf/site.xml")
if err == nil {
cnf := new(cnfinfo)//保险起见,一定new,来的快,避免使用 var cnf cnfinfo,搞不好就是空指针报错 :-/
err = xml.Unmarshal(content, cnf)
if err == nil {
//端口合理就设置配置文件端口为监听端口,不合理就继续用默认
if cnf.Port > 0 {
port = cnf.Port
}
bad := 0//配置文件中的无效插件配置数量
var pluginer *plugin.Plugin
fmt.Println("=====start read plugin info=====")
for _, ve := range cnf.Plugins.Plugin_Info {
//把classname全部转为小写可以为你以后是否大小写classname解忧
switch strings.ToLower(ve.ClassName) {
//这里是匹配支持的插件接口的插件类,这里后面就会用特制的解析器去解析这类插件
case "extension": //octools/toolsbox插件扫描业务,插件名可以带后缀.so也可以不带
pluginer, err = toolsbox.ScanPluginByName(ve.Plugin_Name, ROOTPATH+"/plugins/")
if err == nil {
//使用1v1设计的解析器来解析符合条件的插件
err = lookupextension(pluginer)
}
if err != nil {
//解析插件不符合要求,将具体错误写入开局初始化好的errorlog中
errorlog.Println(err.Error())
bad++
}
default:
bad++
}
}
fmt.Printf("read %v plugin info,%v bad info\n", len(cnf.Plugins.Plugin_Info), bad)
} else {
fmt.Println("unmarshal site.xml failed >>", err)
}
} else {
fmt.Println("read site.xml failed,error>>", err)
os.Exit(1)
}
}
extension解析器,和下文试例插件对比对比,就知道其中奥秘了
func lookupextension(pluginer *plugin.Plugin) (err error) {
srm, err := pluginer.Lookup("Pattern")
if err == nil {
pattern := *srm.(*string)
//pattern存在就说明其他插件已经注册了这个路由了,所以你就该换一个pattern了
if _, ok := ExtensionPatterMap[pattern]; ok {
err = errors.New(pattern + " is exist")
return
}
srm, err = pluginer.Lookup("Method")
if err == nil {
method := *srm.(*string)
srm, err = pluginer.Lookup("ExtensionFunc")
if err == nil {
resfunc := srm.(func(*gin.Context))
//根据匹配到的方法进行请求方式设置
switch strings.ToLower(method) {
case "get":
Extensiongroup.GET(pattern, resfunc)
case "post":
Extensiongroup.POST(pattern, resfunc)
case "put":
Extensiongroup.PUT(pattern, resfunc)
case "delete":
Extensiongroup.DELETE(pattern, resfunc)
default:
err = errors.New("unkonwn request method")
}
if err == nil {
ExtensionPatterMap[pattern] = struct{}{}
}
}
}
}
return
}
试例插件
package main
//package 必须main!!!
import "github.com/gin-gonic/gin"
var Pattern = "greet"
var Method = "get"
func ExtensionFunc(ctx *gin.Context) {
ctx.JSON(200, gin.H{"msg": "hello,im a extension :-)"})
}
插件编译(如果你记不住,或时间久了容易忘记,也可用我自制shell脚本)
go build --buildmode=plugin -o pluginname.so pluginname.go
#拉取脚本,此脚本是编译当前目录下所有.go文件为.so插件
curl https://brotherhoodhk.org/products/shells/build-all.sh -O
#此脚本是编译当前目录下指定.go文件为.so插件(使用时只需输入文件名但别带文件后缀名!!!,例如,greet.go输入文件名就是greet)
curl https://brotherhoodhk.org/products/shells/build.sh -O
现在,我们通过外挂插件的方式,就可以日后在本体基础上新增加其他玩法,无需动本体自身,只需要单独的插件开发编译后,修改修改配置文件就ok了