反射动态代理某一个服务方法
首先描述场景:我要写一个中间件比如缓存模块或者数据操作模块,期望能够像web中间件一样有横向扩展能力如慢查询警报、监控埋点、限流降级等等,但是我又不想去管这个前置后置的链路绑定功能,因为横向扩展能力都比较固定。(就是既要又要还要)。
或者说,如何无侵入地为已有的模块包装前置后置的调用链路?
既然要无侵入式
应该采用类似装饰器模式的思路或者依赖注入的模式,通过某种初始化方式,将目标模块中的API包装上前置后置的逻辑,在用户使用API的时候自动进入中间链的过程,这样可以无侵入式完成”像调用原始方法一样调用包装后的方法“。这种在运行时将方法动态更改形成新的方法的行为,就叫动态代理。
但是,golang并不支持动态代理,在运行过程并不能创建新的类型或者新的变量。==> GAME OVER。
等等,“像调用原始API一样调用包装后的方法”听起来和“像调用本地服务一样调用远程——RPC”挺类似的,翻了rpcx框架的代码后发现,可以借助反射实现类似动态代理的效果。
代码演示
现在有一个服务A
type (
ServerA struct {
Id string
}
)
func (s *ServerA) GetId(name string) string {
fmt.Println("[GetId] ", name)
return s.Id
}
然后我期望能够有一种方式,不更改原始代码,通过注入中间链,然后动态代理掉ServerA的GetId方法,像这样子使用:
type (
FilterChain struct {
before xxxx
after xxxx
}
)
func TestS1(t *testing.T){
chain := &FilterChain{}
srv := &Server{
Id: "id_1"
}
// 插入调用链,动态代理
warpFn := CombineChain(chain , srv.GetId)
// 像调用原始方法一样调用
id := warpFn("my name sss")
fmt.Println(id)
}
如何实现?这里面有几个关注点:
-
如何做到动态代理,在运行时更改并包装方法?
golang中方法只是特殊的函数,可以当做函数操作,使用反射动态生成函数。
-
如何无侵入式使用?我可不想一直使用any而放弃了我的类型系统四处去类型断言,而是期望“像调用原始API一样调用包装后的方法”
利用函数+泛型,实现调用风格的统一。
func CombineChain[T any](filter *FilterChain, t T) T {
// t 传入的是要加中间件的服务方法
// 会将t动态代理,更改为带着上下文与前置后置逻辑的函数,然后返回
// 获取函数的反射值
fnValue := reflect.ValueOf(t)
typ := reflect.TypeOf(t)
if typ.Kind() != reflect.Func {
return t
}
// 创建一个新的函数
copiedFunc := reflect.MakeFunc(fnValue.Type(), func(args []reflect.Value) []reflect.Value {
// filter 前置
// 调用原始函数
res := fnValue.Call(args)
// filter 后置
return res
})
return copiedFunc.Interface().(T)
}
func TestS1(t *testing.T){
chain := &FilterChain{}
srv := &Server{
Id: "id_1"
}
// chain 自己管理前置后置的调用逻辑
// 插入调用链,动态代理
warpFn := CombineChain(chain , srv.GetId)
// 像调用原始方法一样调用
id := warpFn("my name sss")
fmt.Println(id)
}
这样就可以完成“像调用原始API一样调用包装后的方法”。
具体实现
-
FiterChain构造
type ( FilterHandle func(ctx *ChainContext) ValuerTransfer func(ctx *ChainContext, args []reflect.Value) error FilterChain struct { before []FilterHandle after []FilterHandle // 用于转换实际方法的入参,创建时赋值,流量进入时调用 makeArg ValuerTransfer // 用于转换实际方法的响应,创建时赋值,实际逻辑执行完成后调用 makeVal ValuerTransfer } ) -
FilterCtx构造
ChainContext struct { MethodName string // 方法入参 Args []any // 方法响应 Vals []any chain []FilterHandle curIndex int callResult []reflect.Value } -
中间链组装
中间件组链方式有多种:
- 像Gin一样通过ctx传递调用数组与index,一层层调用
- 使用闭包将所有中间件构造成责任链模式(通过顺序编排)
- 为了方便管理,采用Gin的中间件组链方案
-
动态代理
// CombineSrvChain 组合对应服务与中间件
// t 传入的是要加中间件的服务方法(需要为函数类型或者方法类型)
// 函数会将t动态更改为带着上下文ctx与前置后置逻辑的函数,然后按照完整的函数签名返回
func CombineSrvChain[T any](fil *FilterChain, t T) T {
// 获取函数的反射值并校验
// 构造调用ctx与构造新的代理函数
// 返回代理函数
fnValue := reflect.ValueOf(t)
typ := reflect.TypeOf(t)
if typ.Kind() != reflect.Func {
return t
}
// 创建新的函数(具有相同函数签名)
copiedFunc := reflect.MakeFunc(fnValue.Type(), func(args []reflect.Value) []reflect.Value {
ctx := fil.MakeChainCtx(nil, nil)
ctx.MethodName = fnValue.String()
if fil.makeArg != nil {
fil.makeArg(ctx, args)
}
ctx.chain = append(ctx.chain, fil.before...)
ctx.chain = append(ctx.chain, func(ctx *ChainContext) {
res := fnValue.Call(args)
if fil.makeVal != nil {
fil.makeVal(ctx, res)
}
ctx.callResult = res
ctx.Next()
})
ctx.chain = append(ctx.chain, fil.after...)
ctx.Next()
return ctx.callResult
})
return copiedFunc.Interface().(T)
}
完整的chain抽象与代码实现放在这里: github.com/zzjha-cn/gK… 可以直接使用,感兴趣的话可以看看。
拓展
当前的方案是通过将模块的方法动态代理为新的函数,进行函数调用封装。那么,如果一些场景,我在前置后置的操作中仍然需要原始的ServerA对象而不仅仅是函数,是否可以支持呢?
其实也可以,因为方法只是特殊的函数,这里只是提供思路:
- 反射的视角中,方法类型与方法函数类型是不同的类型。如果通过方法类型创建新函数,可以传入serverA这个调用者。
func TestObject(t *testing.T) {
s := &Server{
Id: "id2",
}
fnv := reflect.ValueOf(s).Method(0)
t1 := reflect.TypeOf(s).Method(0).Type
t2 := fnv.Type()
fmt.Println("结构体方法类型与值类型是否相等:", t1 == t2)
fmt.Println("方法类型", t1.String())
fmt.Println("值类型", t2.String())
fmt.Println("值调用")
v := reflect.ValueOf("666")
valRes := fnv.Call([]reflect.Value{v})
fmt.Println(valRes[0].Interface())
fmt.Println("用值的类型创建函数调用")
cfn := reflect.MakeFunc(fnv.Type(), func(args []reflect.Value) (results []reflect.Value) {
return fnv.Call(args)
})
valRes = cfn.Call([]reflect.Value{v})
fmt.Println(valRes[0].Interface())
}
out:
结构体方法类型与值类型是否相等: false
方法类型 func(*Server, string) string
值类型 func(string) string
值调用
[GetId] 666
id2
用值的类型创建函数调用
[GetId] 666
id2