在Java里动态代理的实现有多种不同的方式,多是通过字节码增强方式进行实现的,在java包里本身也带有Proxy的实现方式。 各种方式实现的细节差异,导致性能上也各自有着不同的差异,可以根据不同的场景进行选择。java由于底层有虚拟机的一个环境,class编译为字节码在虚拟机上层运行,所以也就产生了java里动态代理方式的百花齐放, 但是golang里没有虚拟机这个概念,代码直接编译为可直接执行的二进制文件,这也是golang在执行性能上比java优秀很多的一点(当然golang的执行效率之高,并不完全表现在这里,这里不展开了),所以在真正实现动态代理的方法上就很大的制约了, 比较常用的是pig和monkey这两个代表,他们大概的方式简单归纳为都是类似于通过对编译后的汇编语言进行一定的侵入,也就是在二进制文件级别进行打桩的操作,可以理解为动态hook,这里对汇编有比较了解的,可以结合着go的知识,对源码进行了解了。这种方式有比较强暴直接,有一定的适用性,但是由于毕竟是在底层进行处理的,需要了解golang的编译原理,以及本身这两种产品的深入认识,才比较好能用, 基于此,目前大多数还是使用在测试打桩的场景里。
今天这里介绍的方式,是在golang的语言范围里的,通过golang提供的反射的基础上进行实现的,纯golang代码级别的实现,所以在性能上可能无法和上述方法进行比较,纯属是多年的java代码的理念和情节,java里的很多架构都是使用动态代理类的方式实现,使的架构使用和代码扩展上非常的方便,spring/springboot里就大量使用了动态代理, 所以一直就想着在golang里写这样个东西。
在写的过程中,由于golang提供的反射机制的一些局限, 比如struct只能属性,interface只能方法(这点是局限,但是个人认为也是一个比较强大的地方,也有很好的用处), 所以开始的考虑是通过makefunc直接修改反射后interface里的方法对应的地址,一直没有成功,这里和golang的编译方式和golang的运行方式有关,自己有一些个人的理解,还需要深挖golang的内存结构才能去得到结论, 由于此路不通,所以就换成了目前的这样的方式。
定义接口结构体
type Hello struct {
SayHello func() string
}
这里用interface进行定义的,注意是用struct来定义接口的内容,其中的func属性即是需要代理的方法
通过函数进行代理
func TestAOPProxy(t *testing.T) {
proxy := InvocationProxy.NewProxyInstance(&Hello{}, func(obj any, method InvocationMethod, args []reflect.Value) []reflect.Value {
return []reflect.Value{reflect.ValueOf("This is a proxy function")}
}).(*Hello)
fmt.Println(proxy.SayHello())
}
动态的用一个function来代理了原有的SayHello的方法
=== RUN TestAOPProxy
This is a proxy function
--- PASS: TestAOPProxy (0.00s)
通过一个结构体来进行代理, 先在接口里多加一个方法
type Hello struct {
SayHello func() string
SetWord func(word string)
}
具体实现类
type HelloWorld struct {
Word string
}
func (h *HelloWorld) SayHello() string {
return h.Word
}
func (h *HelloWorld) SetWord(word string) {
h.Word = word
}
进行代理
func TestAOPProxyWithClass(t *testing.T) {
impl := &HelloWorld{Word: "Hello world"}
proxy := InvocationProxy.NewProxyInstance(&Hello{}, func(obj any, method InvocationMethod, args []reflect.Value) []reflect.Value {
return method.Invoke(impl, args)
}).(*Hello)
proxy.SetWord("This is a proxy by HelloWorld")
fmt.Println(proxy.SayHello())
}
程序输出
=== RUN TestAOPProxyWithClass
This is a proxy by HelloWorld
--- PASS: TestAOPProxyWithClass (0.00s)
实现一个简单的动态代理工厂BeanFactory
type SimpleBeanFactory struct {
}
func (sbf SimpleBeanFactory) NewInstance(itf any, target any) any {
proxy := InvocationProxy.NewProxyInstance(itf, func(obj any, method InvocationMethod, args []reflect.Value) []reflect.Value {
return method.Invoke(target, args)
})
return proxy
}
通过这样的方式,就可以实现类似java里的一些注入代理的实现了,比如mybatis里的Mapper代理, 以及feignClient里的Client的动态代理了,等等....
Benchmark testing数据
直接调用
goos: windows
goarch: amd64
pkg: github.com/gohutool/boot4go-proxy/examples
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
BenchmarkWithoutProxy
BenchmarkWithoutProxy-8 385053259 3.189 ns/op
PASS
通过函数进行代理调用
goos: windows
goarch: amd64
pkg: github.com/gohutool/boot4go-proxy/examples
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
BenchmarkWithoutFuncProxy
BenchmarkWithoutFuncProxy-8 2781796 376.8 ns/op
PASS
通过结构体进行代理调用
goos: windows
goarch: amd64
pkg: github.com/gohutool/boot4go-proxy/examples
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
BenchmarkWithProxy
BenchmarkWithProxy-8 643676 1735 ns/op
PASS
相差还是很大的,不过和java比起来,非常的优秀了。 通过函数代理比通过结构体代理要优越很多,是在通过结构体代理的时候,又多了一层golang里反射的调用,可见本身的golang的反射调用和直接调用也是有一定性能上的差距的,不过还是相当优秀了。
Github地址