秒懂 caddy 插件机制实现原理

2023-02-22 14:03:04 浏览数 (1)

前言

由于 Golang 是静态编译,所以 plugin 机制一直是一个难解的问题,官方提供的 plugin 机制又特别难用,但插件无疑是扩展原始功能的一种最方便的途径。于是乎,各路软件自家都有各种插件机制。caddy 使用 xcaddy 来实现插件机制,我们来看看它是如何做的。

结论

首先上来先给结论,它必须重新编译,没办法,这也是一个必然选择。很多人听到这,只能叹气一下。

使用方式

使用命令安装 xcaddy go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

然后使用命令选择需要的插件进行重新编译:

代码语言:javascript复制
xcaddy build master 
    --with github.com/caddyserver/ntlm-transport

其中 master 是指定 caddy 的版本

原理

其实原理非常简单,看源码一下就明白了

1. 生成 main.go

xcaddy 运行 build 命令的时候首先会创建一个临时文件夹,然后按照模板写入一个 main.go 文件:

代码语言:javascript复制
const mainModuleTemplate = `package main

import (
	caddycmd "{{.CaddyModule}}/cmd"

	// plug in Caddy modules here
	_ "{{.CaddyModule}}/modules/standard"
	{{- range .Plugins}}
	_ "{{.}}"
	{{- end}}
)

func main() {
	caddycmd.Main()
}
`

其中包含你需要打包进来的 moudles 放在 import 中,以匿名的方式引入。 并且在 main 函数中运行 caddycmd.Main() 也就是 caddy 主项目的主函数。

2. 运行 go mod tidy 和 go build

然后 xcaddy 就直接运行 go mod tidy 进行 go 相关依赖下载,然后使用 go build 根据相关参数进行构建打包。

3. 插件的注册原理

那为什么只需要引入包,即可完成插件的注册呢?它里面的注册原理其实也很简单:

代码语言:javascript复制
package mymodule

import "github.com/caddyserver/caddy/v2"

func init() {
	caddy.RegisterModule(Gizmo{})
}

// Gizmo is an example; put your own type here.
type Gizmo struct {
}

// CaddyModule returns the Caddy module information.
func (Gizmo) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "foo.gizmo",
		New: func() caddy.Module { return new(Gizmo) },
	}
}

插件注册是通过 caddy.RegisterModule() 来实现的,只需要将这个方法放在 init() 函数中,由于在第二步 import 语句中引入了插件,则运行时就会有依赖并执行 init 函数,从而实现注册。

4. 插件的使用原理

既然插件已经通过 RegisterModule 方法注册上了,那么如何使用对应的插件呢? 首先 caddy 定义了形如下面类似的接口:

代码语言:javascript复制
type Unmarshaler interface {
	UnmarshalCaddyfile(d *Dispenser) error
}

然后插件注册的方法 RegisterModule 中会将插件保存到 modules 这个 map 中

代码语言:javascript复制
func RegisterModule(instance Module) {
	mod := instance.CaddyModule()

	if mod.ID == "" {
		panic("module ID missing")
	}
	if mod.ID == "caddy" || mod.ID == "admin" {
		panic(fmt.Sprintf("module ID '%s' is reserved", mod.ID))
	}
	if mod.New == nil {
		panic("missing ModuleInfo.New")
	}
	if val := mod.New(); val == nil {
		panic("ModuleInfo.New must return a non-nil module instance")
	}

	modulesMu.Lock()
	defer modulesMu.Unlock()

	if _, ok := modules[string(mod.ID)]; ok {
		panic(fmt.Sprintf("module already registered: %s", mod.ID))
	}
	modules[string(mod.ID)] = mod
}

只要插件实现了对应的接口,并注册上去,caddy 会通过下面的方法获取:

代码语言:javascript复制
// GetModule returns module information from its ID (full name).
func GetModule(name string) (ModuleInfo, error) {
	modulesMu.RLock()
	defer modulesMu.RUnlock()
	m, ok := modules[name]
	if !ok {
		return ModuleInfo{}, fmt.Errorf("module not registered: %s", name)
	}
	return m, nil
}

然后通过反射进行类型推导,转换为对应的接口类型,从而进行使用:

代码语言:javascript复制
// 获取模块
mod, err := caddy.GetModule("http.matchers."   matcherName)
if err != nil {
    return fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
}

// 转换为对应接口类型
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
    return fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
}

// 调用接口对应的实现方法
err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens))
if err != nil {
    return err
}

设计思路

如果让我以这样的思路去设计插件,我一定会说,先把源码下载下来,然后再 main 函数中添加对应的模块依赖,然后重新编译。这样的问题是,你需要手动下载并编辑代码,不太友好和方便,而 caddy 提供了另一种解决思路,就是将依赖倒置给 xcaddy。由 xcaddy 生成一个新的项目,而新项目中依赖了 caddy 原始项目和 需要的插件。这样的好处是,用户不用关心源码,原项目不需要修改,只需关心插件即可。

总结

虽然 golang 中的插件思路往往都有些看着难办,但这也不失为一种解决方式。

0 人点赞