完善Dubbo-go框架对Java枚举类可变长参数的支持

一、背景介绍

目前公司正在进行技术架构转型,原来的Java服务要改用Go来实现。重构后的Go服务必须可以完全替换掉原来的Java服务,这就要实现Java服务与Go服务的互通。考虑到Java服务使用了Dubbo框架,团队在经过探讨调研后,决定用Dubbgo-go框架来完成这一历史重任,理由也很简单,同源,这样可以完美实现服务互通。

Dubbo框架介绍

Dubbo框架是基于Java语言开发的高性能开源RPC框架。由Alibaba开发,最早只是在内部使用,在2008年捐献开源,于2017年正式捐赠给Apache,在Github上有极高的关注度,目前star已达38k。

主要用于解决微服务架构下的服务治理与通信问题,目前官方已有Java、go等多语言的实现版本,其中又以Java和Go两种语言的实现使用最为广泛。

Dubbo框架有如下一些特点:

基于 HTTP/2 的 Triple 协议以及面向代理 API 的编程体验。
强大的流量治理能力,如地址发现、负载均衡、路由选址、动态配置等。
多语言 SDK 实现,涵盖 Java、Golang、Javascript 等,更多语言实现将会陆续发布。
灵活的适配与扩展能力,可轻松与微服务体系其他组件如 Tracing、Transaction 等适配。
Dubbo Mesh 解决方案,同时支持 Sidecar、Proxyless 等灵活的 Mesh 部署方案。

Dubbo-go框架介绍

Dubbo-go框架是一款分布式RPC框架。是Dubbo框架的Go语言实现。项目在2016年创立,2018年开始创建开源社区,2019年项目进入Apache软件基金会,于2021年底正式发了3.0版本。

虽然Dubbo-go框架发展的时间不长,但是得益于项目充分借鉴了Dubbo框架的优秀设计,使得项目发布以来,一直有很高的热度,并且目前已经有了众多的使用者,如携程、小米、高德、汽车之家、多点等。

由于和Dubbo框架同出一脉,这让Dubbo-go框架在和使用了Dubbo框架的Java服务互通上有着天然的优势,在拥抱云原生的过程中,也有着更大的潜力。

环境

Go 1.16、Dubbo-go 3.0.1、dubbo-go-hessian2 1.11.1

JDK: 8、Dubbo 2.5.4

Zookeeper

Hessian2协议

Dubbo2框架的默认序列化协议,在Dubbo3框架中已经改用Triple协议。

Dubbgo-go框架中也已经实现了对此协议的支持。传送门

二、问题描述

在重构Java服务过程中,不可避免的要处理数据结构相互转换的问题。Go服务中的结构体要和Java服务中的类一一对应,Java中的枚举类也是如此,在大部分情况下,通过框架对hessian2协议的支持,都可以很好的实现。

比如上面的getBooksByStatuses1方法,在Go服务中就很容易的实现。

先基于hessian.JavaEnum自定义BookStatusTypeEnum类型,同时通过JavaClassName方法将其与Java服务中的com.zongheng.service.book.model.BOOK_STATUS枚举类关联,再使用hessian.RegisterJavaEnum()方法将其注册到hessian包中,这样Go服务就可以正常解析了。

但是在getBooksByStatuses2方法中,Java服务中的方法含有枚举类可变长参数,Go服务中的同名实现就出现了问题。在被其它Java服务调用后,会报下图中的错误,不能正常提供服务。

三、排查分析

通过调试发现问题出在github.com/apache/dubbo-go-hessian2@v1.11.1/list.go:382行。

进一步调试后发现aryValue是[]*BookStatusTypeEnum类型,而EnsureRawValue(it)方法的返回值是hessian.JavaEnum类型,两边类型不匹配,所以在赋值时这里就panic了。

难道Dubbo-go框架中不支持可变长参数,带着这个疑问,我决定用saveBooks方法做下测试,这个方法中也有可变长参数,稍有区别的是这里不是枚举类,试验结果则一切正常。Go服务中的同名方法,可以正常处理其它Java服务的调用,没有报错。

这证明了Dubbo-go框架中实现了对可变长参数的支持,只不过,对枚举类可变长参数的支持是有问题的。

要修复这里的问题很简单,只要统一两边的类型即可,但是具体要怎么改,一时还不能确定下来,那就先看看hessian2协议再做决定。协议地址传送门

这里重点看了hessian2协议中关于list的说明。不过理论终归还是要结合实际情况。所以我又分别抓取了getBooksByStatuses1、getBooksByStatuses2以及saveBooks三个方法hessian2协议序列化后的数据来分析。

为了便于阅读这里对原始协议数据做了处理,一部分协议数据使用十六进制,并配了说明,另外一部分则直接转换成字符。

先看getBooksByStatuses1方法的序列化数据,分析后可知,hessian2协议使用固定长度的无类型链表类型来序列化Java枚举类列表参数数据。

再看getBooksByStatuses2方法的序列化数据,分析后可知,hessian2协议使用固定长度的有类型链表类型来序列化Java枚举类可变长参数数据。

最后看saveBooks方法的序列化数据,分析后可知,hessian2协议使用固定长度的有类型链表类型来序列化Java类可变长参数数据。

四、解决方案

初版方案

在参考了框架中对Java枚举类列表参数数据的处理后,决定将接参的变量类型改为[]interface{},这样就可以解决当前的问题。

在进一步查看源码后,发现可以通过getListType方法来实现,此方法会返回用来接参的切片元素类型,如果返回类型是nil,就会生成一个interface{}类型的切片。

再继续看getListType方法内部实现,这里可以增加一个判断,如果当前的类型是枚举类型,那么就直接返回nil。

这样在上层调用方法readTypedListValue中就会生成一个[]interface{}类型的变量来接参了,问题也就解决了。

但是这版方案不是很优雅,虽然可以解决问题,但是破坏了协议对有类型链表类型的约定。需要一个更完美的解决方案。

最终方案

在发现这个问题后,我在github上提了一个issue。同时联系到项目的负责人,经过一番沟通和讨论后,就有了最终的这一版更优雅的解决方案pr传送门

这版方案里,遵守协议约定使用[]*BookStatusTypeEnum来接参,具体改动如下:

至此,问题解决。

展示评论