原文鏈接:https://tech.youzan.com/ji-yu-dong-tai-dai-li-mock-dubbofu-wu-de-shi-xian-fang-an/
序言
背景概述
公司目前 Java 項(xiàng)目提供服務(wù)都是基于 Dubbo 框架的,而且 Dubbo 框架已經(jīng)成為大部分國內(nèi)互聯(lián)網(wǎng)公司選擇的一個(gè)基礎(chǔ)組件稚照。
在日常項(xiàng)目協(xié)作過程中,其實(shí)會碰到服務(wù)不穩(wěn)定、不滿足需求場景等情況针贬,很多開發(fā)都會通過在本地使用 Mocktio 等單測工具作為自測輔助。那么蝗蛙,在聯(lián)調(diào)、測試等協(xié)作過程中怎么處理衫仑?
其實(shí),Dubbo 開發(fā)者估計(jì)也是遇到了這樣的問題堕花,所以提供了一個(gè)提供泛化服務(wù)注冊的入口文狱。但是在服務(wù)發(fā)現(xiàn)的時(shí)候有個(gè)弊端,就說通過服務(wù)發(fā)現(xiàn)去請求這個(gè) Mock 服務(wù)的話缘挽,在注冊中心必須只有一個(gè)服務(wù)有效瞄崇,否則消費(fèi)者會請求到其他非Mock服務(wù)上去。
為了解決這個(gè)問題壕曼,Dubbo 開發(fā)者又提供了泛化調(diào)用的入口苏研。既支持通過注冊中心發(fā)現(xiàn)服務(wù),又支持通過 IP+PORT 去直接調(diào)用服務(wù)腮郊,這樣就能保證消費(fèi)者調(diào)用的是 Mock 出來的服務(wù)了摹蘑。
以上泛化服務(wù)注冊和泛化服務(wù)調(diào)用結(jié)合起來,看似已經(jīng)是一個(gè)閉環(huán)轧飞,可以解決 Dubbo 服務(wù)的 Mock 問題衅鹿。但是,結(jié)合日常工作使用時(shí)过咬,會出現(xiàn)一些麻煩的問題:
- 服務(wù)提供方使用公用的注冊中心大渤,消費(fèi)方無法準(zhǔn)確調(diào)用
- 消費(fèi)者不可能更改代碼,去直連 Mock 服務(wù)
- 使用私有注冊中心能解決以上問題掸绞,但是 Mock 最小緯度為 Method兼犯,一個(gè) Service 中被 Mock 的 Method 會正常處理,沒有被 Mock 的 Method 會異常集漾,導(dǎo)致服務(wù)方需要 Mock Service 的全部方法
在解決以上麻煩的前提下切黔,為了能快速注冊一個(gè)需要的 Dubbo 服務(wù),提高項(xiàng)目協(xié)作過程中的工作效率具篇,開展了 Mock 工廠的設(shè)計(jì)與實(shí)現(xiàn)纬霞。
功能概述
- Mock Dubbo 服務(wù)
- 單個(gè)服務(wù)器,支持部署多個(gè)相同和不同的 Service
- 動態(tài)上驱显、下線服務(wù)
- 非 Mock 的 Method 透傳到基礎(chǔ)服務(wù)
一诗芜、方案探索
1.1 基于 Service Chain 選擇 Mock 服務(wù)的實(shí)現(xiàn)方式
1.1.1 Service Chain 簡單介紹
在業(yè)務(wù)發(fā)起的源頭添加 Service Chain 標(biāo)識,這些標(biāo)識會在接下來的跨應(yīng)用遠(yuǎn)程調(diào)用中一直透傳并且基于這些標(biāo)識進(jìn)行路由埃疫,這樣我們只需要把涉及到需求變更的應(yīng)用的實(shí)例單獨(dú)部署伏恐,并添加到 Service Chain 的數(shù)據(jù)結(jié)構(gòu)定義里面,就可以虛擬出一個(gè)邏輯鏈路栓霜,該鏈路從邏輯上與其他鏈路是完全隔離的翠桦,并且可以共享那些不需要進(jìn)行需求變更的應(yīng)用實(shí)例。
根據(jù)當(dāng)前調(diào)用的透傳標(biāo)識以及 Service Chain 的基礎(chǔ)元數(shù)據(jù)進(jìn)行路由,路由原則如下:
- 當(dāng)前調(diào)用包含 Service Chain 標(biāo)識销凑,則路由到歸屬于該 Service Chain 的任意服務(wù)節(jié)點(diǎn)丛晌,如果沒有歸屬于該
- Service Chain 的服務(wù)節(jié)點(diǎn),則排除掉所有隸屬于 Service Chain 的服務(wù)節(jié)點(diǎn)之后路由到任意服務(wù)節(jié)點(diǎn)
- 當(dāng)前調(diào)用沒有包含 Service Chain 標(biāo)識斗幼,則排除掉所有隸屬于 Service Chain 的服務(wù)節(jié)點(diǎn)之后路由到任意服務(wù)節(jié)點(diǎn)
- 當(dāng)前調(diào)用包含 Service Chain 標(biāo)識澎蛛,并且當(dāng)前應(yīng)用也屬于某個(gè) Service Chain 時(shí),如果兩者不等則拋出路由異常
以 Dubbo 框架為例蜕窿,給出了一個(gè) Service Chain 實(shí)現(xiàn)架構(gòu)圖(下圖來自有贊架構(gòu)團(tuán)隊(duì))
[圖片上傳失敗...(image-cbfbd7-1617204457869)]
1.1.2 Mock 服務(wù)實(shí)現(xiàn)設(shè)計(jì)方案
方案一谋逻、基于 GenericService 生成需要 Mock 接口的泛化實(shí)現(xiàn),并注冊到 ETCD 上(主要實(shí)現(xiàn)思路如下圖所示)桐经。1.1.3 設(shè)計(jì)方案比較
方案一優(yōu)點(diǎn):實(shí)現(xiàn)簡單瓷叫,能滿足mock需求
- 繼承 GenericService屯吊,只要實(shí)現(xiàn)一個(gè) $invoke(String methodName, String[] parameterTypes, Object[] objects),可以根據(jù)具體請求參數(shù)做出自定義返回信息摹菠。
- 接口信息只要知道接口名盒卸、protocol 即可。
- 即使該服務(wù)已經(jīng)存在次氨,也能因?yàn)?generic 字段蔽介,讓消費(fèi)者優(yōu)先消費(fèi)該 mock service。
缺點(diǎn):與公司的服務(wù)發(fā)現(xiàn)機(jī)制沖突
由于有贊服務(wù)背景煮寡,在使用 Haunt 服務(wù)發(fā)現(xiàn)時(shí)虹蓄,是會同時(shí)返回正常服務(wù)和帶有 Service Chain 標(biāo)記的泛化服務(wù),所以必然存在兩種類型的服務(wù)幸撕。導(dǎo)致帶有 Service Chain 標(biāo)記的消費(fèi)者在正常請求泛化服務(wù)時(shí)報(bào) no available invoke薇组。 例:注冊了 2個(gè) HelloService:
- 正常的 :generic=false&interface=com.alia.api.HelloService&methods=doNothing,say,age
- 泛化的:generic=true&interface=com.alia.api.HelloService&methods=*
客戶端請求服務(wù)的時(shí)候炭菌,優(yōu)先匹配到正常的服務(wù)的 method,而不會去調(diào)用泛化服務(wù)逛漫。 導(dǎo)致結(jié)果:訪問時(shí)黑低,會跳過 genericFilter,報(bào) no available invoke酌毡。
方案二優(yōu)點(diǎn):Proxy 實(shí)現(xiàn)投储,自動生成一個(gè)正常的 Dubbo 接口實(shí)現(xiàn)
1.Javassist 有現(xiàn)成的方法生成接口實(shí)現(xiàn)字節(jié)碼第练,大大簡化了對用戶代碼依賴。例如:
- 返回 String玛荞、Json 等娇掏,對單 method 的 mock 實(shí)現(xiàn),都無需用戶上傳實(shí)現(xiàn)類勋眯。
- 透傳時(shí)統(tǒng)一由平臺控制婴梧,不配置 mock 的方法默認(rèn)就會進(jìn)行透傳,而且保留 Service Chain 標(biāo)記客蹋。
2.Mock 服務(wù)注冊 method 信息完整塞蹭。
3.生成接口 Proxy 對象時(shí),嚴(yán)格按照接口定義進(jìn)行生成讶坯,返回?cái)?shù)據(jù)類型有保障番电。
缺點(diǎn):
- 無優(yōu)先消費(fèi)選擇功能。
- 字節(jié)碼后臺生成辆琅,不利于排查生成的 Proxy 中存在問題漱办。
1.1.4 選擇結(jié)果
由于做為平臺,不僅僅需要滿足 mock 需求婉烟,還需要減少用戶操作娩井,以及支持現(xiàn)有公司服務(wù)架構(gòu)體系,所以選擇設(shè)計(jì)方案二似袁。
1.2 基于動態(tài)代理結(jié)合 ServiceConfig 實(shí)現(xiàn)動態(tài)上洞辣、下線服務(wù)
1.2.1 Dubbo 暴露服務(wù)的過程介紹
上圖(來自 dubbo 開發(fā)者文檔)暴露服務(wù)時(shí)序圖: 首先 ServiceConfig 類拿到對外提供服務(wù)的實(shí)際類 ref(如:StudentInfoServiceImpl),然后通過 ProxyFactory 類的 getInvoker 方法使用 ref 生成一個(gè) AbstractProxyInvoker 實(shí)例。到這一步就完成具體服務(wù)到 Invoker 的轉(zhuǎn)化昙衅。接下來就是 Invoker 轉(zhuǎn)換到 Exporter 的過程,Exporter 會通過轉(zhuǎn)化為 URL 的方式暴露服務(wù)扬霜。 從 dubbo 源碼來看,dubbo 通過 Spring 框架提供的 Schema 可擴(kuò)展機(jī)制而涉,擴(kuò)展了自己的配置支持畜挥。dubbo-container 通過封裝 Spring 容器,來啟動了 Spring 上下文婴谱,此時(shí)它會去解析 Spring 的 bean 配置文件(Spring 的 xml 配置文件)蟹但,當(dāng)解析 dubbo:service 標(biāo)簽時(shí),會用 dubbo 自定義 BeanDefinitionParser 進(jìn)行解析谭羔。dubbo 的 BeanDefinitonParser 實(shí)現(xiàn)為 DubboBeanDefinitionParser华糖。 Spring.handlers 文件:http://code.alibabatech.com/schema/dubbo=com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler
public class DubboNamespaceHandler extends NamespaceHandlerSupport {
public DubboNamespaceHandler() {
}
public void init() {
this.registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
this.registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
this.registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
this.registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
this.registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
this.registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
this.registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
this.registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
this.registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
this.registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
}
static {
Version.checkDuplicate(DubboNamespaceHandler.class);
}
}
DubboBeanDefinitionParser 會將配置標(biāo)簽進(jìn)行解析,并生成對應(yīng)的 Javabean,最終注冊到 Spring Ioc 容器中瘟裸。 對 ServiceBean 進(jìn)行注冊時(shí)客叉,其 implements InitializingBean 接口,當(dāng) bean 完成注冊后,會調(diào)用 afterPropertiesSet() 方法兼搏,該方法中調(diào)用 export() 完成服務(wù)的注冊卵慰。在 ServiceConfig 中的 doExport() 方法中,會對服務(wù)的各個(gè)參數(shù)進(jìn)行校驗(yàn)佛呻。
if(this.ref instanceof GenericService) {
this.interfaceClass = GenericService.class;
this.generic = true;
} else {
try {
this.interfaceClass = Class.forName(this.interfaceName, true, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException var5) {
throw new IllegalStateException(var5.getMessage(), var5);
}
this.checkInterfaceAndMethods(this.interfaceClass, this.methods);
this.checkRef();
this.generic = false;
}
注冊過程中會進(jìn)行判斷該實(shí)現(xiàn)類的類型裳朋。其中如果實(shí)現(xiàn)了 GenericService 接口,那么會在暴露服務(wù)信息時(shí)吓著,將 generic 設(shè)置為 true鲤嫡,暴露方法就為*。如果不是绑莺,就會按正常服務(wù)進(jìn)行添加服務(wù)的方法暖眼。此處就是我們可以實(shí)現(xiàn) Mock 的切入點(diǎn),使用 Javassist 根據(jù)自定義的 Mock 信息纺裁,寫一個(gè)實(shí)現(xiàn)類的 class 文件并生成一個(gè)實(shí)例注入到 ServiceConfig 中诫肠。生成 class 實(shí)例如下所示,與一個(gè)正常的實(shí)現(xiàn)類完全一致欺缘,以及注冊的服務(wù)跟正常服務(wù)也完全一致栋豫。
package 123.com.youzan.api;
import com.youzan.api.StudentInfoService;
import com.youzan.pojo.Pojo;
import com.youzan.test.mocker.internal.common.reference.ServiceReference;
public class StudentInfoServiceImpl implements StudentInfoService {
private Pojo getNoValue0;
private Pojo getNoValue1;
private ServiceReference service;
public void setgetNoValue0(Pojo var1) {
this.getNoValue0 = var1;
}
public void setgetNoValue1(Pojo var1) {
this.getNoValue1 = var1;
}
public Pojo getNo(int var1) {
return var1 == 1 ? this.getNoValue0 : this.getNoValue1;
}
public void setService(ServiceReference var1) {
this.service = var1;
}
public double say() {
return (Double)this.service.reference("say", "", (Object[])null);
}
public void findInfo(String var1, long var2) {
this.service.reference("findInfo", "java.lang.String,long", new Object[]{var1, new Long(var2)});
}
public StudentInfoServiceImpl() {}
}
使用 ServiceConfig 將自定義的實(shí)現(xiàn)類注入,并完成注冊浪南,實(shí)現(xiàn)如下:
void registry(Object T, String sc) {
service.setFilter("request")
service.setRef(T)
service.setParameters(new HashMap<String, String>())
service.getParameters().put(Constants.SERVICE_CONFIG_PARAMETER_SERVICE_CHAIN_NAME, sc)
service.export()
if (service.isExported()) {
log.warn "發(fā)布成功 : ${sc}-${service.interface}"
} else {
log.error "發(fā)布失敗 : ${sc}-${service.interface}"
}
}
通過service.setRef(genericService)
完成實(shí)現(xiàn)類的注入,最終通過service.export()
完成服務(wù)注冊漱受。ref 的值已經(jīng)被塞進(jìn)來络凿,并附帶 ServiceChain 標(biāo)記保存至 service 的 paramters 中。具體服務(wù)到 Invoker 的轉(zhuǎn)化以及 Invoker 轉(zhuǎn)換到 Exporter昂羡,Exporter 到 URL 的轉(zhuǎn)換都會附帶上 ServiceChain 標(biāo)記注冊到注冊中心絮记。
1.2.2 生成實(shí)現(xiàn)類設(shè)計(jì)方案
方案一、 支持指定 String(或 Json) 對單個(gè) method 進(jìn)行 mock虐先。
功能介紹:根據(jù)入?yún)?String or Json怨愤,生成代理對象。由 methodName 和 methodParams 獲取唯一 method 定義蛹批。(指支持單個(gè)方法mock)撰洗。消費(fèi)者請求到Mock服務(wù)的對應(yīng)Mock Method時(shí),Mock服務(wù)將保存的數(shù)據(jù)轉(zhuǎn)成對應(yīng)的返回類型腐芍,并返回差导。
方案二、 支持指定 String(或 Json) 對多個(gè) method生成 mock猪勇。
功能介紹:根據(jù)入?yún)?String or Json设褐,生成代理對象。method 對應(yīng)的 mock 數(shù)據(jù)由 methodMockMap 指定,由 methodName 獲取唯一 method 定義助析,所以被 mock 接口不能有重載方法(只支持多個(gè)不同方法 mock)犀被。消費(fèi)者請求到 Mock 服務(wù)的對應(yīng) mock method 時(shí),Mock 服務(wù)將保存的數(shù)據(jù)轉(zhuǎn)成對應(yīng)的返回類型外冀,并返回寡键。
方案三、 在使用 實(shí)現(xiàn)類(Impl) 的情況下锥惋,支持傳入一個(gè)指定的 method 進(jìn)行 mock昌腰。
功能介紹:根據(jù)入?yún)⒌膶?shí)現(xiàn)類,生成代理對象膀跌。由 methodName 和 methodParams 獲取唯一 method 定義遭商。(支持 mock 一個(gè)方法)。消費(fèi)者請求到 Mock 服務(wù)的對應(yīng) mock method 時(shí)捅伤,Mock 服務(wù)調(diào)用該實(shí)現(xiàn)類的對應(yīng)方法劫流,并返回。
方案四丛忆、 在使用 實(shí)現(xiàn)類(Impl) 的情況下祠汇,支持傳入多個(gè) method 進(jìn)行 mock。
功能介紹:根據(jù)入?yún)⒌膶?shí)現(xiàn)類熄诡,生成代理對象可很。由 methodName 獲取唯一 method 定義,所以被 mock 接口不能有重載方法(只支持一個(gè)實(shí)現(xiàn)類 mock 多個(gè)方法)凰浮。消費(fèi)者請求到 Mock 服務(wù)的對應(yīng) mock method 時(shí)我抠,Mock 服務(wù)調(diào)用該實(shí)現(xiàn)類的對應(yīng)方法,并返回袜茧。
方案五菜拓、 使用 Custom Reference 對多個(gè) method 進(jìn)行 mock。
功能介紹:根據(jù)入?yún)?ServiceReference笛厦,生成代理對象纳鼎。method 對應(yīng)的自定義 ServiceReference 由 methodMockMap 指定,由 methodName 獲取唯一method定義裳凸,所以被 mock 接口不能有重載方法(只支持多個(gè)不同方法 mock)贱鄙。消費(fèi)者請求到 Mock 服務(wù)的對應(yīng) mock method 時(shí),Mock 服務(wù)會主動請求自定義的 Dubbo 服務(wù)姨谷。
1.2.3 設(shè)計(jì)方案選擇
以上五種方案贰逾,其實(shí)就是整個(gè) Mock 工廠實(shí)現(xiàn)的一個(gè)迭代過程。在每個(gè)方案的嘗試中菠秒,發(fā)現(xiàn)各自的弊端然后出現(xiàn)了下一種方案疙剑。目前氯迂,在結(jié)合各種使用場景后,選擇了方案二言缤、方案五嚼蚀。
方案三、方案四被排除的主要原因:Dubbo 對已經(jīng)發(fā)布的 Service 保存了實(shí)現(xiàn)類的 ClassLoader管挟,相同 className 的類一旦注冊成功后轿曙,會將實(shí)現(xiàn)類的 ClassLoader 保存到內(nèi)存中,很難被刪除僻孝。所以想要使用這兩種方案的話导帝,需要頻繁變更實(shí)現(xiàn)類的 className,大大降低了一個(gè)工具的易用性穿铆。改用自定義 Dubbo 服務(wù)(方案五)您单,替代自定義實(shí)現(xiàn)類,但是需要使用者自己起一個(gè) Dubbo 服務(wù)荞雏,并告知 IP+PORT虐秦。
方案一其實(shí)是方案二的補(bǔ)集,能支持 Service 重載方法的 Mock凤优。由于在使用時(shí)悦陋,需要傳入具體 Method 的簽名信息,增加了用戶操作成本筑辨。由于公司內(nèi)部保證一個(gè) Service 不可能有重載方法俺驶,且為了提高使用效率,不開放該方案棍辕。后期如果出現(xiàn)這樣的有重載方法的情況暮现,再進(jìn)行開放。
1.2.4 遇到的坑
基礎(chǔ)數(shù)據(jù)類型需要特殊處理
使用 Javassist 根據(jù)接口 class 寫一個(gè)實(shí)現(xiàn)類的 class 文件痢毒,遇到最讓人頭疼的就是方法簽名和返回值送矩。如果方法的簽名和返回值為基礎(chǔ)數(shù)據(jù)類型時(shí)蚕甥,那在傳參和返回時(shí)需要做特殊處理哪替。平臺中本人使用了最笨的枚舉處理方法,如果有使用 Javassist 的高手菇怀,有好的建議麻煩不吝賜教凭舶。代碼如下:
/** 參數(shù)存在基本數(shù)據(jù)類型時(shí),默認(rèn)使用基本數(shù)據(jù)類型
* 基本類型包含:
* 實(shí)數(shù):double爱沟、float
* 整數(shù):byte帅霜、short、int呼伸、long
* 字符:char
* 布爾值:boolean
* */
private static CtClass getParamType(ClassPool classPool, String paramType) {
switch (paramType) {
case "char":
return CtClass.charType
case "byte":
return CtClass.byteType
case "short":
return CtClass.shortType
case "int":
return CtClass.intType
case "long":
return CtClass.longType
case "float":
return CtClass.floatType
case "double":
return CtClass.doubleType
case "boolean":
return CtClass.booleanType
default:
return classPool.get(paramType)
}
}
1.3 非 Mock 的 Method 透傳到基礎(chǔ)服務(wù)
1.3.1 Dubbo 服務(wù)消費(fèi)的過程介紹
在消費(fèi)端:Spring 解析 dubbo:reference 時(shí)身冀,Dubbo 首先使用 com.alibaba.dubbo.config.spring.schema.NamespaceHandler 注冊解析器钝尸,當(dāng) Spring 解析 xml 配置文件時(shí)就會調(diào)用這些解析器生成對應(yīng)的 BeanDefinition 交給 Spring 管理。Spring 在初始化 IOC 容器時(shí)會利用這里注冊的 BeanDefinitionParser 的 parse 方法獲取對應(yīng)的 ReferenceBean 的 BeanDefinition 實(shí)例搂根,由于 ReferenceBean 實(shí)現(xiàn)了 InitializingBean 接口珍促,在設(shè)置了 Bean 的所有屬性后會調(diào)用 afterPropertiesSet 方法。afterPropertiesSet 方法中的 getObject 會調(diào)用父類 ReferenceConfig 的 init 方法完成組裝剩愧。ReferenceConfig 類的 init 方法調(diào)用 Protocol 的 refer 方法生成 Invoker 實(shí)例猪叙,這是服務(wù)消費(fèi)的關(guān)鍵。接下來把 Invoker 轉(zhuǎn)換為客戶端需要的接口(如:StudentInfoService)仁卷。由 ReferenceConfig 切入穴翩,通過 API 方式使用 Dubbo 的泛化調(diào)用,代碼如下:
Object reference(String s, String paramStr, Object[] objects) {
if (StringUtils.isEmpty(serviceInfoDO.interfaceName) || serviceInfoDO.interfaceName.length() <= 0) {
throw new NullPointerException("The 'interfaceName' should not be ${serviceInfoDO.interfaceName}, please make sure you have the correct 'interfaceName' passed in")
}
// set interface name
referenceConfig.setInterface(serviceInfoDO.interfaceName)
referenceConfig.setApplication(serviceInfoDO.applicationConfig)
// set version
if (serviceInfoDO.version != null && serviceInfoDO.version != "" && serviceInfoDO.version.length() > 0) {
referenceConfig.setVersion(serviceInfoDO.version)
}
if (StringUtils.isEmpty(serviceInfoDO.refUrl) || serviceInfoDO.refUrl.length() <= 0) {
throw new NullPointerException("The 'refUrl' should not be ${serviceInfoDO.refUrl} , please make sure you have the correct 'refUrl' passed in")
}
//set refUrl
referenceConfig.setUrl(serviceInfoDO.refUrl)
reference.setGeneric(true)// 聲明為泛化接口
//使用com.alibaba.dubbo.rpc.service.GenericService可以代替所有接口引用
GenericService genericService = reference.get()
String[] strs = null
if(paramStr != ""){
strs = paramStr.split(",")
}
Object result = genericService.$invoke(s, strs, objects)
// 返回值類型不定锦积,需要做特殊處理
if (result.getClass().isAssignableFrom(HashMap.class)) {
Class dtoClass = Class.forName(result.get("class"))
result.remove("class")
String resultJson = JSON.toJSONString(result)
return JSON.parseObject(resultJson, dtoClass)
}
return result
}
如上代碼所示芒帕,具體業(yè)務(wù) DTO 類型,泛化調(diào)用結(jié)果非僅結(jié)果數(shù)據(jù)充包,還包含 DTO 的 class 信息副签,需要特殊處理結(jié)果,取出需要的結(jié)果進(jìn)行返回基矮。
1.3.2 記錄dubbo服務(wù)請求設(shè)計(jì)方案
方案一淆储、捕獲請求信息
服務(wù)提供方和服務(wù)消費(fèi)方調(diào)用過程攔截,Dubbo 本身的大多功能均基于此擴(kuò)展點(diǎn)實(shí)現(xiàn)家浇,每次遠(yuǎn)程方法執(zhí)行本砰,該攔截都會被執(zhí)行。Provider 提供的調(diào)用鏈钢悲,具體的調(diào)用鏈代碼是在 ProtocolFilterWrapper 的 buildInvokerChain 完成的点额,具體是將注解中含有 group=provider 的 Filter 實(shí)現(xiàn),按照 order 排序莺琳,最后的調(diào)用順序是 EchoFilter->ClassLoaderFilter->GenericFilter->ContextFilter->ExceptionFilter->TimeoutFilter->MonitorFilter->TraceFilter还棱。 其中:EchoFilter 的作用是判斷是否是回聲測試請求,是的話直接返回內(nèi)容惭等≌涫郑回聲測試用于檢測服務(wù)是否可用,回聲測試按照正常請求流程執(zhí)行辞做,能夠測試整個(gè)調(diào)用是否通暢琳要,可用于監(jiān)控。ClassLoaderFilter 則只是在主功能上添加了功能秤茅,更改當(dāng)前線程的 ClassLoader稚补。
在 ServiceConfig 繼承 AbstractInterfaceConfig,中有 filter 屬性框喳。以此為切入點(diǎn)课幕,給每個(gè) Mock 服務(wù)添加 filter,記錄每次 dubbo 服務(wù)請求信息(接口厦坛、方法、入?yún)⒄Ь⒎祷胤喟恪㈨憫?yīng)時(shí)長)。
方案二污桦、記錄請求信息
將請求信息保存在內(nèi)存中亩歹,一個(gè)接口的每個(gè)被 Mock 的方法保存近 10次 記錄信息。使用二級緩存保存凡橱,緩存代碼如下:
@Singleton(lazy = true)
class CacheUtil {
private static final Object PRESENT = new Object()
private int maxInterfaceSize = 10000 // 最大接口緩存數(shù)量
private int maxRequestSize = 10 // 最大請求緩存數(shù)量
private Cache<String, Cache<RequestDO, Object>> caches = CacheBuilder.newBuilder()
.maximumSize(maxInterfaceSize)
.expireAfterAccess(7, TimeUnit.DAYS) // 7天未被請求的接口小作,緩存回收
.build()
}
如上代碼所示,二級緩存中的一個(gè) Object 是被浪費(fèi)的內(nèi)存空間稼钩,但是由于想不到其他更好的方案顾稀,所以暫時(shí)保留該設(shè)計(jì)。
1.3.3 遇到的坑
泛化調(diào)用時(shí)參數(shù)對象轉(zhuǎn)換
使用 ReferenceConfig 進(jìn)行服務(wù)直接調(diào)用坝撑,繞過了對一個(gè)接口方法簽名的校驗(yàn)静秆,所以在進(jìn)行泛化調(diào)用時(shí),最大的問題就是 Object[] 內(nèi)的參數(shù)類型了巡李。每次當(dāng)遇到數(shù)據(jù)類型問題時(shí)抚笔,本人只會用最笨的辦法,枚舉解決侨拦。代碼如下:
/** 參數(shù)存在基本數(shù)據(jù)類型時(shí)殊橙,默認(rèn)使用基本數(shù)據(jù)類型
* 基本類型包含:
* 實(shí)數(shù):double、float
* 整數(shù):byte狱从、short膨蛮、int、long
* 字符:char
* 布爾值:boolean
* */
private Object getInstance(String paramType, String value) {
switch (paramType) {
case "java.lang.String":
return value
case "byte":
case "java.lang.Byte":
return Byte.parseByte(value)
case "short":
return Short.parseShort(value)
case "int":
case "java.lang.Integer":
return Integer.parseInt(value)
case "long":
case "java.lang.Long":
return Long.parseLong(value)
case "float":
case "java.lang.Float":
return Float.parseFloat(value)
case "double":
case "java.lang.Double":
return Double.parseDouble(value)
case "boolean":
case "java.lang.Boolean":
return Boolean.parseBoolean(value)
default:
JSONObject jsonObject = JSON.parseObject(value) // 轉(zhuǎn)成JSONObject
return jsonObject
}
}
如以上代碼所示季研,是將傳入?yún)?shù)轉(zhuǎn)成對應(yīng)的包裝類型敞葛。當(dāng)接口的簽名如果為 int,那么入?yún)ο笫?Integer 也是可以的。因?yàn)?$invoke(String methodName, String[] paramsTypes, Object[] objects)与涡,是由 paramsTypes 檢查方法簽名惹谐,然后再將 objects 傳入具體服務(wù)中進(jìn)行調(diào)用。
ReferenceConfig 初始化優(yōu)先設(shè)置 initialize 為 true
使用泛化調(diào)用發(fā)起遠(yuǎn)程 Dubbo 服務(wù)請求递沪,在發(fā)起 invoke 前豺鼻,有 GenericService genericService = referenceConfig.get() 操作综液。當(dāng) Dubbo 服務(wù)沒有起來款慨,此時(shí)首次發(fā)起調(diào)用后,進(jìn)行 ref 初始化操作谬莹。ReferenceConfig 初始化 ref 代碼如下:
private void init() {
if (initialized) {
return;
}
initialized = true;
if (interfaceName == null || interfaceName.length() == 0) {
throw new IllegalStateException("<dubbo:reference interface=\"\" /> interface not allow null!");
}
// 獲取消費(fèi)者全局配置
checkDefault();
appendProperties(this);
if (getGeneric() == null && getConsumer() != null) {
setGeneric(getConsumer().getGeneric());
}
...
}
結(jié)果導(dǎo)致:由于第一次初始化的時(shí)候檩奠,先把 initialize 設(shè)置為 true桩了,但是后面未獲取到有效的 genericService,導(dǎo)致后面即使 Dubbo 服務(wù)起來后埠戳,也會泛化調(diào)用失敗井誉。
解決方案:泛化調(diào)用就是使用 genericService 執(zhí)行 invoke 調(diào)用,所以每次請求都使用一個(gè)新的 ReferenceConfig整胃,當(dāng)初始化進(jìn)行 get() 操作時(shí)報(bào)異晨攀ィ或返回為 null 時(shí),不保存屁使;直到初始化進(jìn)行 get() 操作時(shí)獲取到有效的 genericService 時(shí)在岂,將該 genericService 保存起來。實(shí)現(xiàn)代碼如下:
synchronized (hasInit) {
if (!hasInit) {
ReferenceConfig referenceConfig = new ReferenceConfig();
// set interface name
referenceConfig.setInterface(serviceInfoDO.interfaceName)
referenceConfig.setApplication(serviceInfoDO.applicationConfig)
// set version
if (serviceInfoDO.version != null && serviceInfoDO.version != "" && serviceInfoDO.version.length() > 0) {
referenceConfig.setVersion(serviceInfoDO.version)
}
if (StringUtils.isEmpty(serviceInfoDO.refUrl) || serviceInfoDO.refUrl.length() <= 0) {
throw new NullPointerException("The 'refUrl' should not be ${serviceInfoDO.refUrl} , please make sure you have the correct 'refUrl' passed in")
}
referenceConfig.setUrl(serviceInfoDO.refUrl)
referenceConfig.setGeneric(true)// 聲明為泛化接口
genericService = referenceConfig.get()
if (null != genericService) {
hasInit = true
}
}
}
1.4 單個(gè)服務(wù)器蛮寂,支持部署多個(gè)相同和不同的Service
根據(jù)需求蔽午,需要解決兩個(gè)問題:1.服務(wù)器運(yùn)行過程中,外部API的Jar包加載問題酬蹋;2.注冊多個(gè)相同接口服務(wù)時(shí)及老,名稱相同的問題。
1.4.1 動態(tài)外部Jar包加載的設(shè)計(jì)方案
方案一范抓、為外部 Jar 包生成單獨(dú)的 URLClassLoader,然后在泛化注冊時(shí)使用保存的 ClassLoader骄恶,在回調(diào)時(shí)進(jìn)行切換 currentThread 的 ClassLoader,進(jìn)行相同 API 接口不同版本的 Mock匕垫。
不可用原因: JavassistProxyFactory 中 final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
wapper 獲取的時(shí)候叠蝇,使用的 makeWrapper 中默認(rèn)使用的是ClassHelper.getClassLoader(c);
導(dǎo)致一直會使用 AppClassLoader。API 信息會保存在一個(gè) WapperMap 中年缎,當(dāng)消費(fèi)者請求過來的時(shí)候悔捶,會優(yōu)先取這個(gè) Map 找對應(yīng)的 API 信息。
導(dǎo)致結(jié)果:
- 1.由于使用泛化注冊单芜,所以 class 不在 AppClassLoader 中蜕该。設(shè)置了 currentThread 的 ClassLoader 不生效。
- 2.由于 dubbo 保存 API 信息只有一個(gè) Map洲鸠,所以導(dǎo)致發(fā)布的服務(wù)的 API 也只能有一套堂淡。
解決方案:
- 使用自定義 ClassLoader 進(jìn)行加載外部 Jar 包中的 API 信息。
- 一臺 Mock 終端存一套 API 信息扒腕,更新 API 時(shí)需要重啟服務(wù)器绢淀。
方案二、在程序啟動時(shí)瘾腰,使用自定義 TestPlatformClassLoader皆的。還是給每個(gè) Jar 包生成對應(yīng)的 ApiClassLoader,由 TestPlatformClassLoader 統(tǒng)一管理蹋盆。
不可用原因:
在 Mock 終端部署時(shí)费薄,使用 -Djava.system.class.loader 設(shè)置 ClassLoader 時(shí)硝全,JVM 啟動參數(shù)不可用。因?yàn)槔懵眨琓estPlatformClassLoader 不存在于當(dāng)前 JVM 中伟众,而是在工程代碼中。詳細(xì)參數(shù)如下:
-Djava.system.class.loader=com.youzan.test.mocker.internal.classloader.TestPlatformClassLoader
解決方案:(由架構(gòu)師汪興提供)
- 使用自定義 Runnable()召廷,保存程序啟動需要的 ClassLoader凳厢、啟動參數(shù)、mainClass 信息竞慢。
- 在程序啟動時(shí)数初,新起一個(gè) Thread,傳入自定義 Runnable()梗顺,然后將該線程啟動泡孩。
方案三、使用自定義容器啟動服務(wù)
應(yīng)用啟動流程寺谤,如下圖所示(下圖來自有贊架構(gòu)團(tuán)隊(duì))
Java 的類加載遵循雙親委派的設(shè)計(jì)模式仑鸥,從 AppClassLoader 開始自底向上尋找,并自頂向下加載变屁,所以在沒有自定義 ClassLoader 時(shí)眼俊,應(yīng)用的啟動是通過 AppClassLoader 去加載 Main 啟動類去運(yùn)行。
自定義 ClassLoader 后,系統(tǒng) ClassLoader 將被設(shè)置成容器自定義的 ClassLoader,自定義 ClassLoader 重新去加載 Main 啟動類運(yùn)行凉袱,此時(shí)后續(xù)所有的類加載都會先去自定義的 ClassLoader 里查找。
難點(diǎn):應(yīng)用默認(rèn)系統(tǒng)類加載器是 AppClassLoader澎灸,在 New 對象時(shí)不會經(jīng)過自定義的 ClassLoader。
巧妙之處:Main 函數(shù)啟動時(shí)遮晚,AppClassLoader 加載 Main 和容器性昭,容器獲取到 Main class,用自定義 ClassLoader 重新加載Main县遣,設(shè)置系統(tǒng)類加載器為自定義類加載器糜颠,此時(shí) New 對象都會經(jīng)過自定義的 ClassLoader。
1.4.2 設(shè)計(jì)方案選擇
以上三個(gè)方案萧求,其實(shí)是實(shí)踐過程中的一個(gè)迭代其兴。最終結(jié)果:
- 方案一、保留為外部Jar包生成單獨(dú)的 URLClassLoader夸政。
- 方案二元旬、保留自定義 TestPlatformClassLoader,使用 TestPlatformClassLoader 保存每個(gè) Jar 包中 API 與其 ClassLoader 的對應(yīng)關(guān)系。
- 方案三法绵、采用自定義容器啟動,新起一個(gè)線程酪碘,并設(shè)置其 concurrentThreadClassLoader 為 TestPlatformClassLoader朋譬,用該線程啟動 Main.class。
1.4.3 遇到的坑
使用 Javassist 生成的 Class 名稱相同
使用 Javassist 生成的 Class兴垦,每個(gè) Class 有單獨(dú)的 ClassName 以 Service Chain + className 組成徙赢。在重新生成相同名字的 class 時(shí),即使使用 new ClassPool()
也不能完全隔離探越。因?yàn)樯?Class 的時(shí)候 Class<?> clazz = ctClass.toClass()
默認(rèn)使用的是同一個(gè) ClassLoader狡赐,所以會報(bào)“attempted duplicate class definition for name:****”。
解決方案:基于 ClassName 不是隨機(jī)生成的钦幔,所以只能基于之前的 ClassLoader 生成一個(gè)新的 SecureClassLoader(ClassLoader parent) 加載新的 class枕屉,舊的 ClassLoader 靠 Java 自動 GC。代碼如下:
Class<?> clazz = ctClass.toClass(new SecureClassLoader(clz.classLoader))
PS:該方案目前沒有做過壓測鲤氢,不知道會不會導(dǎo)致內(nèi)存溢出搀擂。
二、方案實(shí)現(xiàn)
2.1 Mock 工廠整體設(shè)計(jì)架構(gòu)
2.2 Mocker 容器設(shè)計(jì)圖
2.3 二方包管理時(shí)序圖
2.4 Mocker 容器服務(wù)注冊時(shí)序圖
三卷玉、支持場景
3.1 元素及名詞解釋
上圖所示為基本元素組成哨颂,相關(guān)名詞解釋如下:
- 消費(fèi)者:調(diào)用方發(fā)起 DubboRequest
- Base 服務(wù):不帶 Service Chain 標(biāo)識的正常服務(wù)
- Mock 服務(wù):通過 Mock 工廠生成的 dubbo 服務(wù)
- ETCD:注冊中心,此處同時(shí)注冊著 Base 服務(wù)和 Mock 服務(wù)
- 默認(rèn)服務(wù)透傳:對接口中不需要 Mock 的方法相种,直接泛化調(diào)用 Base 服務(wù)
- 自定義服務(wù)(CF):用戶自己起一個(gè)泛化 dubbo 服務(wù)(PS:不需要注冊到注冊中心威恼,也不需要 Service Chain 標(biāo)識)
3.2 支持場景簡述
場景1:不帶 Service Chain 請求(不使用 Mock 服務(wù)時(shí))
消費(fèi)者從注冊中心獲取到 Base 環(huán)境服務(wù)的 IP+PORT,直接請求 Base 環(huán)境的服務(wù)寝并。場景2箫措、帶 Service Chain 請求、Mock 服務(wù)采用 JSON 返回實(shí)現(xiàn)
消費(fèi)者從注冊中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT衬潦;2.帶 Service Chain 標(biāo)記服務(wù)(Mock服務(wù))的 IP+PORT蒂破。根據(jù) Service Chain 調(diào)用路由,去請求 Mock 服務(wù)中的該方法别渔,并返回 Mock 數(shù)據(jù)附迷。場景3、帶 Service Chain 請求哎媚、Mock 服務(wù)沒有該方法實(shí)現(xiàn)
消費(fèi)者從注冊中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT喇伯;2.帶 Service Chain 標(biāo)記服務(wù)(Mock 服務(wù))的 IP+PORT。根據(jù) Service Chain 調(diào)用路由拨与,去請求 Mock 服務(wù)稻据。由于 Mock 服務(wù)中該方法是默認(rèn)服務(wù)透傳,所以由 Mock 服務(wù)直接泛化調(diào)用 Base 服務(wù),并返回?cái)?shù)據(jù)捻悯。場景4匆赃、帶 Service Chain 請求頭、Mock 服務(wù)采用自定義服務(wù)(CR)實(shí)現(xiàn)
消費(fèi)者從注冊中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT今缚;2.帶 Service Chain 標(biāo)記服務(wù)(Mock 服務(wù))的 IP+PORT算柳。根據(jù) Service Chain 調(diào)用路由,去請求Mock服務(wù)姓言。由于 Mock 服務(wù)中該方法是自定義服務(wù)(CF)瞬项,所以由 Mock 服務(wù)調(diào)用用戶的 dubbo 服務(wù),并返回?cái)?shù)據(jù)何荚。場景5囱淋、帶 Service Chain 請求頭、Mock 服務(wù)沒有該方法實(shí)現(xiàn)餐塘、該方法又調(diào)用帶 Service Chain 的 InterfaceB 的方法
消費(fèi)者調(diào)用 InterfaceA 的 Method3 時(shí)妥衣,從注冊中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT;2.帶 Service Chain 標(biāo)記服務(wù)(Mock 服務(wù))的 IP+PORT戒傻。根據(jù) Service Chain 調(diào)用路由称鳞,去請求 InterfaceA 的 Mock 服務(wù)。由于 Mock 服務(wù)中該方法是默認(rèn)服務(wù)透傳稠鼻,所以由 Mock 服務(wù)直接泛化調(diào)用 InterfaceA 的 Base 服務(wù)的Method3冈止。
但是,由于 InterfaceA 的 Method3 是調(diào)用 InterfaceB 的 Method2候齿,從注冊中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT熙暴;2.帶 Service Chain 標(biāo)記服務(wù)(Mock 服務(wù))的 IP+PORT。由于 Service Chain 標(biāo)識在整個(gè)請求鏈路中是一直被保留的慌盯,所以根據(jù)Service Chain調(diào)用路由周霉,最終請求到 InterfaceB 的 Mock 服務(wù),并返回?cái)?shù)據(jù)亚皂。