使用dubbo-go搭建dubbo接口測(cè)試平臺(tái)
背景
http接口測(cè)試只需要一個(gè)curl命令,但dubbo協(xié)議沒(méi)有這樣的現(xiàn)成接口測(cè)試工具。通常公司內(nèi)的dubbo控制臺(tái)或其他平臺(tái)會(huì)集成一個(gè)dubbo接口測(cè)試工具。
調(diào)用一個(gè)dubbo接口,需要知道服務(wù)名service、方法名method和參數(shù)args。
正常的調(diào)用,調(diào)用方需引入服務(wù)提供方定義的接口jar包。
作為接口測(cè)試平臺(tái),沒(méi)辦法引入所有提供方定義的接口jar包,可以有以下方案來(lái)解決:
dubbo支持telnet協(xié)議調(diào)用dubbo接口 dubbo的泛化調(diào)用可以在不引入提供方接口定義jar包的情況下對(duì)接口進(jìn)行調(diào)用
對(duì)于方案1,實(shí)現(xiàn)成本很低,甚至可以在服務(wù)器上直接用telnet測(cè)試

它也有缺點(diǎn)
調(diào)用無(wú)法經(jīng)過(guò)filter 無(wú)法攜帶隱式參數(shù)attachment
剛好我們把方案1的優(yōu)缺點(diǎn)都踩了,我們的dubbo控制臺(tái)是go語(yǔ)言編寫,短時(shí)間快速實(shí)現(xiàn),就采用了telnet的方式。
隨著業(yè)務(wù)的發(fā)展,流量染色,或標(biāo)簽路由等需要攜帶隱式參數(shù)。
沒(méi)有走自定義filter,導(dǎo)致業(yè)務(wù)接口執(zhí)行不符合預(yù)期等都迫使我們升級(jí)為泛化調(diào)用。
dubbo接口泛化調(diào)用在控制臺(tái)是go編寫的情況下也有兩個(gè)方案可選:
單獨(dú)起一個(gè)java進(jìn)程,暴露http端口,與go進(jìn)程進(jìn)行交互,泛化調(diào)用使用dubbo的java sdk進(jìn)行編寫 控制臺(tái)引入dubbo-go,使用dubbo-go進(jìn)行泛化調(diào)用
出于對(duì)dubbo java版本的了解,方案1肯定可行,只是架構(gòu)變得復(fù)雜。
而方案2由于dubbo-go還是比較新的項(xiàng)目,并不是很了解,所以不確定其可行性和兼容性,但如果能實(shí)現(xiàn),會(huì)大大降低架構(gòu)的復(fù)雜度。
dubbo-go介紹
dubbo-go是dubbo的golang實(shí)現(xiàn)版本,它出現(xiàn)的初衷是為了讓golang和java的dubbo生態(tài)互通。
如今dubbo-go支持provider和consumer端,可以作為一個(gè)獨(dú)立的rpc框架使用,同時(shí)社區(qū)也是dubbo生態(tài)中最火的一個(gè)。
如果要說(shuō)它的意義,我覺(jué)得除了和java互通外還有一點(diǎn)非常重要,那就是它能發(fā)揮golang協(xié)程的巨大作用,這一點(diǎn)可以用在dubbo網(wǎng)關(guān)上,如果用dubbo-go實(shí)現(xiàn)dubbo網(wǎng)關(guān),就無(wú)需糾結(jié)線程池、異步等問(wèn)題。
泛化調(diào)用的使用
首先provider端提供一個(gè)接口,這個(gè)不再贅述,非常簡(jiǎn)單,接口定義如下
package org.newboo.basic.api;
import org.newboo.basic.model.RpcResult;
import org.newboo.basic.model.User;
public interface MyDemoService {
RpcResult<String> call(User user);
}
package org.newboo.basic.model;
import java.io.Serializable;
public class User implements Serializable {
private String uid;
private String name;
private String remoteServiceTag;
...
}
再來(lái)編寫java版的泛化調(diào)用代碼,不引入provider方的jar包:
ReferenceConfig<GenericService> reference = new ReferenceConfig<>();
// ①引用服務(wù)名
reference.setInterface("org.newboo.basic.api.MyDemoService");
// ②設(shè)置泛化調(diào)用標(biāo)志
reference.setGeneric("true");
DubboBootstrap bootstrap = DubboBootstrap.getInstance();
bootstrap.application(new ApplicationConfig("dubbo-demo-api-consumer"))
.registry(new RegistryConfig("zookeeper://127.0.0.1:2181"))
.reference(reference)
.start();
GenericService genericService = ReferenceConfigCache.getCache().get(reference);
String[] ps = new String[1];
// ③參數(shù)類型
ps[0] = "org.newboo.basic.model.User";
Object[] ags = new Object[1];
// ④pojo參數(shù)使用map構(gòu)造
Map<String, String> user = new HashMap<>();
user.put("uid", "1");
user.put("name", "roshi");
user.put("remoteServiceTag", "tag");
ags[0] = user;
// ⑤發(fā)起調(diào)用
Object res = genericService.$invoke("call", ps, ags);
System.out.println(res);
關(guān)鍵的步驟已在代碼注釋中標(biāo)明
golang版本
直接修改的dubbo-go-samples代碼,參考https://github.com/apache/dubbo-go-samples 啟動(dòng)時(shí)需要設(shè)置配置文件路徑ENV
var (
appName = "UserConsumer"
referenceConfig = config.ReferenceConfig{
InterfaceName: "org.newboo.basic.api.MyDemoService",
Cluster: "failover",
// registry需要配置文件
Registry: "demoZk",
Protocol: dubbo.DUBBO,
Generic: true,
}
)
func init() {
referenceConfig.GenericLoad(appName) //appName is the unique identification of RPCService
time.Sleep(1 * time.Second)
}
// need to setup environment variable "CONF_CONSUMER_FILE_PATH" to "conf/client.yml" before run
func main() {
call()
}
func call() {
// 設(shè)置attachment
ctx := context.WithValue(context.TODO(), constant.AttachmentKey, map[string]string{"tag":"test"})
resp, err := referenceConfig.GetRPCService().(*config.GenericService).Invoke(
ctx,
[]interface{}{
"call",
[]string{"org.newboo.basic.model.User"},
[]interface{}{map[string]string{"uid":"111","name":"roshi","remoteServiceTag":"hello"}},
},
)
if err != nil {
panic(err)
}
gxlog.CInfo("success called res: %+v\n", resp)
}
這里我設(shè)置了一個(gè)attachment,也能正常被provider識(shí)別

泛化調(diào)用原理
泛化調(diào)用GenericService是dubbo默認(rèn)提供的一個(gè)服務(wù)。
其提供了一個(gè)名為$invoke的方法,該方法參數(shù)有三個(gè),第一個(gè)參數(shù)是真實(shí)要調(diào)用的方法名,第二個(gè)是參數(shù)類型數(shù)組,第三個(gè)是真實(shí)的參數(shù)數(shù)組,其定義為
public interface GenericService {
Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;
...
}
有了這三個(gè)參數(shù),利用反射就能調(diào)用到真實(shí)的接口了。
java版實(shí)現(xiàn)細(xì)節(jié)
實(shí)現(xiàn)這種泛化調(diào)用主要涉及到兩個(gè)filter:
consumer端的GenericImplFilter provider端的GenericFilter
consumer端的filter將generic標(biāo)志設(shè)置到attachment中,并封裝調(diào)用為GenericService.$invoke
provider端filter判斷請(qǐng)求是generic時(shí)進(jìn)行攔截,獲取調(diào)用方法名、參數(shù)、參數(shù)值,先序列化為pojo對(duì)象,再進(jìn)行反射調(diào)用真實(shí)接口。
dubbo-go版細(xì)節(jié)
與java實(shí)現(xiàn)基本一致,其中g(shù)eneric_filter充當(dāng)consumer端的filter,也是將調(diào)用封裝為GenericService.$invoke,其中還涉及到一個(gè)參數(shù)類型的轉(zhuǎn)換,將map轉(zhuǎn)換為dubbo-go-hessian2.Object,這樣provider端就可以將其反序列化為Object對(duì)象。
與其相關(guān)的版本變更如下
v1.3.0開(kāi)始支持泛化調(diào)用 v1.4.0開(kāi)始支持用戶設(shè)置attachement v1.5.1開(kāi)始支持動(dòng)態(tài)tag路由 v1.5.7-rc1修復(fù)了直連provider時(shí)無(wú)法走filter的bug
踩坑:v1.5.7-rc1 之前如果使用直連provider的方式,不會(huì)走filter,導(dǎo)致參數(shù)序列化出錯(cuò),provider端會(huì)報(bào)類型轉(zhuǎn)換異常
結(jié)論
dubbo-go的泛化調(diào)用推薦使用>=v1.5.7-rc1版本,其功能幾乎已和java版打平,甚至其實(shí)現(xiàn)都與java類似。
使用dubbo-go構(gòu)建網(wǎng)關(guān)、接口測(cè)試平臺(tái)、或者打通golang與java技術(shù)生態(tài),不失為一個(gè)好的選擇。
搜索關(guān)注微信公眾號(hào)"捉蟲大師",后端技術(shù)分享,架構(gòu)設(shè)計(jì)、性能優(yōu)化、源碼閱讀、問(wèn)題排查、踩坑實(shí)踐。
