本部分将通过用例的方式, 展示Reflect
构造Go
装饰器的用例.
背景
在云环境
中存在若干租户(Project
)与若干虚拟机(VM
), 往往一个Project
名下存在多个VM
, 并且Project
仅能够操作自己名下的VM
.
代码示例如下:
租户与虚拟机
Model
示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package cloud
type VM struct {
ID string
Name string
CPU string
ProjectID string
}
type Project struct {
ID string
Name string
AZID string
ParentID string
}云环境内资源信息:
1
2
3
4
5
6
7
8package cloud
// ProjectToVMMap 租户与虚拟机的所属关系
var ProjectToVMMap = map[string]string{
"A": "001",
"B": "002",
}
// GlobleVMs 全局虚拟机信息
var GlobleVMs map[string]VM工具类方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30package cloud
import "fmt"
// PrintVMs 输出全局虚拟机信息
func PrintVMs() {
fmt.Printf("VMs num: %v\n", len(GlobleVMs))
for _, vm := range GlobleVMs {
fmt.Printf("VM info: %+v \n", vm)
}
fmt.Printf("\n")
}
// InitVMs 初始化全局虚拟机信息
func InitVMs() {
GlobleVMs = map[string]VM{
"001": {
ID: "001",
Name: "vm_001",
CPU: "x86",
ProjectID: "A",
},
"002": {
ID: "002",
Name: "vm_002",
CPU: "arm",
ProjectID: "B",
},
}
}代码中提供两个接口, 支持
Project
操作VM
:修改虚拟机名称:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package cloud
import "fmt"
type PutVMParams struct {
ProjectID string
VMID string
VMName string
}
func PutVM(params PutVMParams) error {
vm, ok := GlobleVMs[params.VMID]
if !ok {
fmt.Printf("VMID: %v missing \n", params.VMID)
return fmt.Errorf("VMID MISSING ERROR")
}
vm.Name = params.VMName
GlobleVMs[params.VMID] = vm
return nil
}删除虚拟机:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package cloud
import "fmt"
type DeleteVMParams struct {
ProjectID string
VMID string
}
func DeleteVM(params DeleteVMParams) error {
_, ok := GlobleVMs[params.VMID]
if !ok {
fmt.Printf("VMID: %v missing \n", params.VMID)
return fmt.Errorf("VMID MISSING ERROR")
}
delete(GlobleVMs, params.VMID)
return nil
}
主逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34package main
import (
"fmt"
"self/pkg/cloud"
)
func main() {
cloud.InitVMs()
cloud.PrintVMs()
putParams := cloud.PutVMParams{
ProjectID: "B",
VMID: "001",
VMName: "MyServer",
}
err := cloud.PutVM(putParams)
if err != nil {
fmt.Printf("Put vm failed, err: %v \n", err.Error())
return
}
cloud.PrintVMs()
deleteParams := cloud.DeleteVMParams{
ProjectID: "A",
VMID: "002",
}
err = cloud.DeleteVM(deleteParams)
if err != nil {
fmt.Printf("Delete vm failed, err: %v \n", err.Error())
return
}
cloud.PrintVMs()
}上面的代码中,
- 优先初始化了两个
VM
信息, 其中A
租户拥有VM 001
,B
租户拥有VM 002
, 并输出VM
信息查看下当前的VM
情况; - 其次
B
租户修改VM 001
名称, 然而其操作的是A
租户的VM
, 并输出VM
信息查看下当前的VM
情况; - 最后
A
租户删除VM 002
名称, 然而其操作的是B
租户的VM
, 并输出VM
信息查看下当前的VM
情况;
输出为:
1
2
3
4
5
6
7
8
9
10
11[root self]# go run ./cmd/main.go
VMs num: 2
VM info: {ID:002 Name:vm_002 CPU:arm ProjectID:B}
VM info: {ID:001 Name:vm_001 CPU:x86 ProjectID:A}
VMs num: 2
VM info: {ID:001 Name:MyServer CPU:x86 ProjectID:A}
VM info: {ID:002 Name:vm_002 CPU:arm ProjectID:B}
VMs num: 1
VM info: {ID:001 Name:MyServer CPU:x86 ProjectID:A}需求
- 优先初始化了两个
上面的业务代码PutVM, DeleteVM
中缺失了Project
权限检验逻辑, 导致任意Project
的权限泄露. 需要添加权限检验逻辑.
思路
业务嵌入法
可以直接在业务代码头部添加权限检验逻辑, 如下:
1 | // 权限检验逻辑 |
上面的代码中, 我直接选择在业务代码头部嵌入了权限检验逻辑, 其
- 优点: 逻辑简单
- 缺点: 代码耦合: 业务代码与权限代码耦合在一起;
输出为:
1 | [root@sangfor self]# go run ./cmd/main.go |
发现上面的代码修改已经达到了需求, 然而实际项目中的修改量往往不止于此, 可能存在n
个业务接口需要适配, 极易造成改动引发. 我们期望可以将业务代码与权限检验逻辑解耦, 这样即使出现了改动引发错误, 也不会改动到业务代码.
reflect
装饰器法
有一个很合适的解决方案:
Python 装饰器
1
Python 的装饰器是一种非常有用的功能,它允许你在不修改原有函数代码的情况下,给函数添加额外的功能。装饰器本质上是一个函数,它接受一个函数作为参数,并返回一个新的函数。当你将装饰器应用于某个函数时,你实际上是在该函数周围“包裹”了一层额外的逻辑。
然而我们使用的是
Go
; 此时就需要reflect
登场了.可行性, 在
Go
中Func
也是值(Value
), 既然是值那么就可以通过reflect
修改.
装饰器具体代码如下:
定义一个Model. 用于权限检验
1
2
3
4
5// decroateCheckVMParams 虚拟机权限检验Model
type decroateCheckVMParams struct {
ProjectID string
VMID string
}定义一个装饰函数, 嵌入权限检验逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// decorateCheckVM 装饰函数, 其会添加虚拟机权限检验逻辑到函数原代码中
func decorateCheckVM(newFunc, oldFunc interface{}, injectFunc func(in reflect.Value) *decroateCheckVMParams) {
var decoratedFunc, targetFunc reflect.Value
decoratedFunc = reflect.ValueOf(newFunc).Elem()
targetFunc = reflect.ValueOf(oldFunc)
v := reflect.MakeFunc(
targetFunc.Type(),
func(in []reflect.Value) (out []reflect.Value) {
param := injectFunc(in[0])
if !checkPermission(param.ProjectID, param.VMID) {
err := fmt.Errorf("Check permission failed")
out = []reflect.Value{reflect.ValueOf(err)}
return
}
out = targetFunc.Call(in)
return
},
)
decoratedFunc.Set(v)
}
// DecoratedPutVM 装饰器逻辑
func DecoratedPutVM() func(params PutVMParams) error {
decoratedHandleFunc := PutVM
decorateCheckVM(&decoratedHandleFunc, decoratedHandleFunc, convertToDecoratedCheckVM)
return decoratedHandleFunc
}decorateCheckVM
函数是一个装饰器工厂,它接受三个参数:newFunc
是一个将被装饰的函数的引用,oldFunc
是原始的未装饰函数,injectFunc
是一个将reflect.Value
转换为*decroateCheckVMParams
的函数。decorateCheckVM
函数的目的是在oldFunc
的调用前插入权限检查逻辑。DecoratedPutVM
函数是一个具体的装饰器实现,它使用decorateCheckVM
来装饰PutVM
函数(假设PutVM
是一个已经定义的函数,它接受PutVMParams
类型的参数并返回一个error
)。DecoratedPutVM
函数返回一个新的函数,这个新函数在调用PutVM
之前会执行权限检查。这里有几个需要注意的地方:
decorateCheckVM
函数中的decoratedFunc
和targetFunc
是通过反射创建的函数值,decoratedFunc
是一个指向newFunc
的指针,而targetFunc
是oldFunc
的值。reflect.MakeFunc
用于创建一个新的函数,这个函数在调用时会先执行injectFunc
来获取参数,然后检查权限,如果权限检查失败,它会返回一个错误;如果权限检查通过,它会调用原始的targetFunc
函数。
这段代码的目的是通过反射来动态地添加权限检查逻辑到现有的函数中,而不需要修改原始函数的代码。这是一种常见的装饰器模式的实现方式,它允许开发者在不改变原始代码的情况下,为函数添加额外的功能。
定义一个注入函数, 实现将业务参数转换为权限检验参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// convertToDecoratedCheckVM 参数转换逻辑, 将业务参数转换为虚拟机权限检验参数
func convertToDecoratedCheckVM(in reflect.Value) *decroateCheckVMParams {
var param *decroateCheckVMParams
switch in.Interface().(type) {
case PutVMParams:
_p, _ := in.Interface().(PutVMParams)
param = &decroateCheckVMParams{
ProjectID: _p.ProjectID,
VMID: _p.VMID,
}
case DeleteVMParams:
_p, _ := in.Interface().(DeleteVMParams)
param = &decroateCheckVMParams{
ProjectID: _p.ProjectID,
VMID: _p.VMID,
}
default:
fmt.Println("Unimplement param type, build check project id param failed")
}
return param
}convertToDecoratedCheckVM
函数接受一个reflect.Value
类型的参数,并根据这个反射值的实际类型,将其转换为一个指向decroateCheckVMParams
结构体的指针。这个函数检查
reflect.Value
中的实际类型,如果是PutVMParams
或DeleteVMParams
类型,它就创建一个新的decroateCheckVMParams
实例,并从输入参数中复制ProjectID
和VMID
字段。如果输入的类型不是这两种之一,它将打印一条错误消息。适配主函数, 查看效果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package main
import (
"fmt"
"self/pkg/cloud"
)
func main() {
// ...
putVMFunc := cloud.DecoratedPutVM()
// err := cloud.PutVM(putParams)
err := putVMFunc(putParams)
if err != nil {
fmt.Printf("Put vm failed, err: %v \n", err.Error())
return
}
// ...
}发现输出一致:
1
2
3
4
5
6[root@ self]# go run ./cmd/main.go
VMs num: 2
VM info: {ID:001 Name:vm_001 CPU:x86 ProjectID:A}
VM info: {ID:002 Name:vm_002 CPU:arm ProjectID:B}
Put vm failed, err: Check permission failed这种方式的特点为:其利用反射与闭包的方式, 实现了类似于Python装饰器的效果,使得在进入业务逻辑前进行动态的权限检验;通过依赖注入方式, 将业务参数修改为由业务控制, 实现权限检验与业务参数解耦.
扩展性良好, 如果我们想实现DeleteVM
逻辑, 只需要添加一段简短的逻辑即可:
1 | // DecoratedDeleteVM 装饰器逻辑 |
读者可以自行检验, 效果.