Golang使用动态代理完成无侵入式增加服务中间件

77 阅读5分钟

反射动态代理某一个服务方法

首先描述场景:我要写一个中间件比如缓存模块或者数据操作模块,期望能够像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