原文鏈接:https://tech.youzan.com/ji-yu-dong-tai-dai-li-mock-dubbofu-wu-de-shi-xian-fang-an/
序言
背景概述
公司目前 Java 項目提供服務都是基于 Dubbo 框架的,而且 Dubbo 框架已經(jīng)成為大部分國內(nèi)互聯(lián)網(wǎng)公司選擇的一個基礎組件。
在日常項目協(xié)作過程中,其實會碰到服務不穩(wěn)定、不滿足需求場景等情況,很多開發(fā)都會通過在本地使用 Mocktio 等單測工具作為自測輔助。那么,在聯(lián)調(diào)、測試等協(xié)作過程中怎么處理?
其實,Dubbo 開發(fā)者估計也是遇到了這樣的問題,所以提供了一個提供泛化服務注冊的入口。但是在服務發(fā)現(xiàn)的時候有個弊端,就說通過服務發(fā)現(xiàn)去請求這個 Mock 服務的話,在注冊中心必須只有一個服務有效,否則消費者會請求到其他非Mock服務上去。
為了解決這個問題,Dubbo 開發(fā)者又提供了泛化調(diào)用的入口。既支持通過注冊中心發(fā)現(xiàn)服務,又支持通過 IP+PORT 去直接調(diào)用服務,這樣就能保證消費者調(diào)用的是 Mock 出來的服務了。
以上泛化服務注冊和泛化服務調(diào)用結合起來,看似已經(jīng)是一個閉環(huán),可以解決 Dubbo 服務的 Mock 問題。但是,結合日常工作使用時,會出現(xiàn)一些麻煩的問題:
- 服務提供方使用公用的注冊中心,消費方無法準確調(diào)用
- 消費者不可能更改代碼,去直連 Mock 服務
- 使用私有注冊中心能解決以上問題,但是 Mock 最小緯度為 Method,一個 Service 中被 Mock 的 Method 會正常處理,沒有被 Mock 的 Method 會異常,導致服務方需要 Mock Service 的全部方法
在解決以上麻煩的前提下,為了能快速注冊一個需要的 Dubbo 服務,提高項目協(xié)作過程中的工作效率,開展了 Mock 工廠的設計與實現(xiàn)。
功能概述
- Mock Dubbo 服務
- 單個服務器,支持部署多個相同和不同的 Service
- 動態(tài)上、下線服務
- 非 Mock 的 Method 透傳到基礎服務
一、方案探索
1.1 基于 Service Chain 選擇 Mock 服務的實現(xiàn)方式
1.1.1 Service Chain 簡單介紹
在業(yè)務發(fā)起的源頭添加 Service Chain 標識,這些標識會在接下來的跨應用遠程調(diào)用中一直透傳并且基于這些標識進行路由,這樣我們只需要把涉及到需求變更的應用的實例單獨部署,并添加到 Service Chain 的數(shù)據(jù)結構定義里面,就可以虛擬出一個邏輯鏈路,該鏈路從邏輯上與其他鏈路是完全隔離的,并且可以共享那些不需要進行需求變更的應用實例。
根據(jù)當前調(diào)用的透傳標識以及 Service Chain 的基礎元數(shù)據(jù)進行路由,路由原則如下:
- 當前調(diào)用包含 Service Chain 標識,則路由到歸屬于該 Service Chain 的任意服務節(jié)點,如果沒有歸屬于該
- Service Chain 的服務節(jié)點,則排除掉所有隸屬于 Service Chain 的服務節(jié)點之后路由到任意服務節(jié)點
- 當前調(diào)用沒有包含 Service Chain 標識,則排除掉所有隸屬于 Service Chain 的服務節(jié)點之后路由到任意服務節(jié)點
- 當前調(diào)用包含 Service Chain 標識,并且當前應用也屬于某個 Service Chain 時,如果兩者不等則拋出路由異常
以 Dubbo 框架為例,給出了一個 Service Chain 實現(xiàn)架構圖(下圖來自有贊架構團隊)
[圖片上傳失敗...(image-cbfbd7-1617204457869)]
1.1.2 Mock 服務實現(xiàn)設計方案
方案一、基于 GenericService 生成需要 Mock 接口的泛化實現(xiàn),并注冊到 ETCD 上(主要實現(xiàn)思路如下圖所示)。

1.1.3 設計方案比較
方案一優(yōu)點:實現(xiàn)簡單,能滿足mock需求
- 繼承 GenericService,只要實現(xiàn)一個 $invoke(String methodName, String[] parameterTypes, Object[] objects),可以根據(jù)具體請求參數(shù)做出自定義返回信息。
- 接口信息只要知道接口名、protocol 即可。
- 即使該服務已經(jīng)存在,也能因為 generic 字段,讓消費者優(yōu)先消費該 mock service。
缺點:與公司的服務發(fā)現(xiàn)機制沖突
由于有贊服務背景,在使用 Haunt 服務發(fā)現(xiàn)時,是會同時返回正常服務和帶有 Service Chain 標記的泛化服務,所以必然存在兩種類型的服務。導致帶有 Service Chain 標記的消費者在正常請求泛化服務時報 no available invoke。 例:注冊了 2個 HelloService:
- 正常的 :generic=false&interface=com.alia.api.HelloService&methods=doNothing,say,age
- 泛化的:generic=true&interface=com.alia.api.HelloService&methods=*

客戶端請求服務的時候,優(yōu)先匹配到正常的服務的 method,而不會去調(diào)用泛化服務。 導致結果:訪問時,會跳過 genericFilter,報 no available invoke。
方案二優(yōu)點:Proxy 實現(xiàn),自動生成一個正常的 Dubbo 接口實現(xiàn)
1.Javassist 有現(xiàn)成的方法生成接口實現(xiàn)字節(jié)碼,大大簡化了對用戶代碼依賴。例如:
- 返回 String、Json 等,對單 method 的 mock 實現(xiàn),都無需用戶上傳實現(xiàn)類。
- 透傳時統(tǒng)一由平臺控制,不配置 mock 的方法默認就會進行透傳,而且保留 Service Chain 標記。
2.Mock 服務注冊 method 信息完整。
3.生成接口 Proxy 對象時,嚴格按照接口定義進行生成,返回數(shù)據(jù)類型有保障。
缺點:
- 無優(yōu)先消費選擇功能。
- 字節(jié)碼后臺生成,不利于排查生成的 Proxy 中存在問題。
1.1.4 選擇結果
由于做為平臺,不僅僅需要滿足 mock 需求,還需要減少用戶操作,以及支持現(xiàn)有公司服務架構體系,所以選擇設計方案二。
1.2 基于動態(tài)代理結合 ServiceConfig 實現(xiàn)動態(tài)上、下線服務
1.2.1 Dubbo 暴露服務的過程介紹

上圖(來自 dubbo 開發(fā)者文檔)暴露服務時序圖: 首先 ServiceConfig 類拿到對外提供服務的實際類 ref(如:StudentInfoServiceImpl),然后通過 ProxyFactory 類的 getInvoker 方法使用 ref 生成一個 AbstractProxyInvoker 實例。到這一步就完成具體服務到 Invoker 的轉化。接下來就是 Invoker 轉換到 Exporter 的過程,Exporter 會通過轉化為 URL 的方式暴露服務。 從 dubbo 源碼來看,dubbo 通過 Spring 框架提供的 Schema 可擴展機制,擴展了自己的配置支持。dubbo-container 通過封裝 Spring 容器,來啟動了 Spring 上下文,此時它會去解析 Spring 的 bean 配置文件(Spring 的 xml 配置文件),當解析 dubbo:service 標簽時,會用 dubbo 自定義 BeanDefinitionParser 進行解析。dubbo 的 BeanDefinitonParser 實現(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 會將配置標簽進行解析,并生成對應的 Javabean,最終注冊到 Spring Ioc 容器中。 對 ServiceBean 進行注冊時,其 implements InitializingBean 接口,當 bean 完成注冊后,會調(diào)用 afterPropertiesSet() 方法,該方法中調(diào)用 export() 完成服務的注冊。在 ServiceConfig 中的 doExport() 方法中,會對服務的各個參數(shù)進行校驗。
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;
}
注冊過程中會進行判斷該實現(xiàn)類的類型。其中如果實現(xiàn)了 GenericService 接口,那么會在暴露服務信息時,將 generic 設置為 true,暴露方法就為*。如果不是,就會按正常服務進行添加服務的方法。此處就是我們可以實現(xiàn) Mock 的切入點,使用 Javassist 根據(jù)自定義的 Mock 信息,寫一個實現(xiàn)類的 class 文件并生成一個實例注入到 ServiceConfig 中。生成 class 實例如下所示,與一個正常的實現(xiàn)類完全一致,以及注冊的服務跟正常服務也完全一致。
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 將自定義的實現(xiàn)類注入,并完成注冊,實現(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)完成實現(xiàn)類的注入,最終通過service.export()完成服務注冊。ref 的值已經(jīng)被塞進來,并附帶 ServiceChain 標記保存至 service 的 paramters 中。具體服務到 Invoker 的轉化以及 Invoker 轉換到 Exporter,Exporter 到 URL 的轉換都會附帶上 ServiceChain 標記注冊到注冊中心。
1.2.2 生成實現(xiàn)類設計方案
方案一、 支持指定 String(或 Json) 對單個 method 進行 mock。
功能介紹:根據(jù)入?yún)?String or Json,生成代理對象。由 methodName 和 methodParams 獲取唯一 method 定義。(指支持單個方法mock)。消費者請求到Mock服務的對應Mock Method時,Mock服務將保存的數(shù)據(jù)轉成對應的返回類型,并返回。
方案二、 支持指定 String(或 Json) 對多個 method生成 mock。
功能介紹:根據(jù)入?yún)?String or Json,生成代理對象。method 對應的 mock 數(shù)據(jù)由 methodMockMap 指定,由 methodName 獲取唯一 method 定義,所以被 mock 接口不能有重載方法(只支持多個不同方法 mock)。消費者請求到 Mock 服務的對應 mock method 時,Mock 服務將保存的數(shù)據(jù)轉成對應的返回類型,并返回。
方案三、 在使用 實現(xiàn)類(Impl) 的情況下,支持傳入一個指定的 method 進行 mock。
功能介紹:根據(jù)入?yún)⒌膶崿F(xiàn)類,生成代理對象。由 methodName 和 methodParams 獲取唯一 method 定義。(支持 mock 一個方法)。消費者請求到 Mock 服務的對應 mock method 時,Mock 服務調(diào)用該實現(xiàn)類的對應方法,并返回。
方案四、 在使用 實現(xiàn)類(Impl) 的情況下,支持傳入多個 method 進行 mock。
功能介紹:根據(jù)入?yún)⒌膶崿F(xiàn)類,生成代理對象。由 methodName 獲取唯一 method 定義,所以被 mock 接口不能有重載方法(只支持一個實現(xiàn)類 mock 多個方法)。消費者請求到 Mock 服務的對應 mock method 時,Mock 服務調(diào)用該實現(xiàn)類的對應方法,并返回。
方案五、 使用 Custom Reference 對多個 method 進行 mock。
功能介紹:根據(jù)入?yún)?ServiceReference,生成代理對象。method 對應的自定義 ServiceReference 由 methodMockMap 指定,由 methodName 獲取唯一method定義,所以被 mock 接口不能有重載方法(只支持多個不同方法 mock)。消費者請求到 Mock 服務的對應 mock method 時,Mock 服務會主動請求自定義的 Dubbo 服務。
1.2.3 設計方案選擇
以上五種方案,其實就是整個 Mock 工廠實現(xiàn)的一個迭代過程。在每個方案的嘗試中,發(fā)現(xiàn)各自的弊端然后出現(xiàn)了下一種方案。目前,在結合各種使用場景后,選擇了方案二、方案五。
方案三、方案四被排除的主要原因:Dubbo 對已經(jīng)發(fā)布的 Service 保存了實現(xiàn)類的 ClassLoader,相同 className 的類一旦注冊成功后,會將實現(xiàn)類的 ClassLoader 保存到內(nèi)存中,很難被刪除。所以想要使用這兩種方案的話,需要頻繁變更實現(xiàn)類的 className,大大降低了一個工具的易用性。改用自定義 Dubbo 服務(方案五),替代自定義實現(xiàn)類,但是需要使用者自己起一個 Dubbo 服務,并告知 IP+PORT。
方案一其實是方案二的補集,能支持 Service 重載方法的 Mock。由于在使用時,需要傳入具體 Method 的簽名信息,增加了用戶操作成本。由于公司內(nèi)部保證一個 Service 不可能有重載方法,且為了提高使用效率,不開放該方案。后期如果出現(xiàn)這樣的有重載方法的情況,再進行開放。
1.2.4 遇到的坑
基礎數(shù)據(jù)類型需要特殊處理
使用 Javassist 根據(jù)接口 class 寫一個實現(xiàn)類的 class 文件,遇到最讓人頭疼的就是方法簽名和返回值。如果方法的簽名和返回值為基礎數(shù)據(jù)類型時,那在傳參和返回時需要做特殊處理。平臺中本人使用了最笨的枚舉處理方法,如果有使用 Javassist 的高手,有好的建議麻煩不吝賜教。代碼如下:
/** 參數(shù)存在基本數(shù)據(jù)類型時,默認使用基本數(shù)據(jù)類型
* 基本類型包含:
* 實數(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 透傳到基礎服務
1.3.1 Dubbo 服務消費的過程介紹

在消費端:Spring 解析 dubbo:reference 時,Dubbo 首先使用 com.alibaba.dubbo.config.spring.schema.NamespaceHandler 注冊解析器,當 Spring 解析 xml 配置文件時就會調(diào)用這些解析器生成對應的 BeanDefinition 交給 Spring 管理。Spring 在初始化 IOC 容器時會利用這里注冊的 BeanDefinitionParser 的 parse 方法獲取對應的 ReferenceBean 的 BeanDefinition 實例,由于 ReferenceBean 實現(xiàn)了 InitializingBean 接口,在設置了 Bean 的所有屬性后會調(diào)用 afterPropertiesSet 方法。afterPropertiesSet 方法中的 getObject 會調(diào)用父類 ReferenceConfig 的 init 方法完成組裝。ReferenceConfig 類的 init 方法調(diào)用 Protocol 的 refer 方法生成 Invoker 實例,這是服務消費的關鍵。接下來把 Invoker 轉換為客戶端需要的接口(如: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è)務 DTO 類型,泛化調(diào)用結果非僅結果數(shù)據(jù),還包含 DTO 的 class 信息,需要特殊處理結果,取出需要的結果進行返回。
1.3.2 記錄dubbo服務請求設計方案
方案一、捕獲請求信息
服務提供方和服務消費方調(diào)用過程攔截,Dubbo 本身的大多功能均基于此擴展點實現(xiàn),每次遠程方法執(zhí)行,該攔截都會被執(zhí)行。Provider 提供的調(diào)用鏈,具體的調(diào)用鏈代碼是在 ProtocolFilterWrapper 的 buildInvokerChain 完成的,具體是將注解中含有 group=provider 的 Filter 實現(xiàn),按照 order 排序,最后的調(diào)用順序是 EchoFilter->ClassLoaderFilter->GenericFilter->ContextFilter->ExceptionFilter->TimeoutFilter->MonitorFilter->TraceFilter。 其中:EchoFilter 的作用是判斷是否是回聲測試請求,是的話直接返回內(nèi)容?;芈暅y試用于檢測服務是否可用,回聲測試按照正常請求流程執(zhí)行,能夠測試整個調(diào)用是否通暢,可用于監(jiān)控。ClassLoaderFilter 則只是在主功能上添加了功能,更改當前線程的 ClassLoader。
在 ServiceConfig 繼承 AbstractInterfaceConfig,中有 filter 屬性。以此為切入點,給每個 Mock 服務添加 filter,記錄每次 dubbo 服務請求信息(接口、方法、入?yún)?、返回、響應時長)。
方案二、記錄請求信息
將請求信息保存在內(nèi)存中,一個接口的每個被 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()
}
如上代碼所示,二級緩存中的一個 Object 是被浪費的內(nèi)存空間,但是由于想不到其他更好的方案,所以暫時保留該設計。
1.3.3 遇到的坑
泛化調(diào)用時參數(shù)對象轉換
使用 ReferenceConfig 進行服務直接調(diào)用,繞過了對一個接口方法簽名的校驗,所以在進行泛化調(diào)用時,最大的問題就是 Object[] 內(nèi)的參數(shù)類型了。每次當遇到數(shù)據(jù)類型問題時,本人只會用最笨的辦法,枚舉解決。代碼如下:
/** 參數(shù)存在基本數(shù)據(jù)類型時,默認使用基本數(shù)據(jù)類型
* 基本類型包含:
* 實數(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) // 轉成JSONObject
return jsonObject
}
}
如以上代碼所示,是將傳入?yún)?shù)轉成對應的包裝類型。當接口的簽名如果為 int,那么入?yún)ο笫?Integer 也是可以的。因為 $invoke(String methodName, String[] paramsTypes, Object[] objects),是由 paramsTypes 檢查方法簽名,然后再將 objects 傳入具體服務中進行調(diào)用。
ReferenceConfig 初始化優(yōu)先設置 initialize 為 true
使用泛化調(diào)用發(fā)起遠程 Dubbo 服務請求,在發(fā)起 invoke 前,有 GenericService genericService = referenceConfig.get() 操作。當 Dubbo 服務沒有起來,此時首次發(fā)起調(diào)用后,進行 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!");
}
// 獲取消費者全局配置
checkDefault();
appendProperties(this);
if (getGeneric() == null && getConsumer() != null) {
setGeneric(getConsumer().getGeneric());
}
...
}
結果導致:由于第一次初始化的時候,先把 initialize 設置為 true,但是后面未獲取到有效的 genericService,導致后面即使 Dubbo 服務起來后,也會泛化調(diào)用失敗。
解決方案:泛化調(diào)用就是使用 genericService 執(zhí)行 invoke 調(diào)用,所以每次請求都使用一個新的 ReferenceConfig,當初始化進行 get() 操作時報異?;蚍祷貫?null 時,不保存;直到初始化進行 get() 操作時獲取到有效的 genericService 時,將該 genericService 保存起來。實現(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 單個服務器,支持部署多個相同和不同的Service
根據(jù)需求,需要解決兩個問題:1.服務器運行過程中,外部API的Jar包加載問題;2.注冊多個相同接口服務時,名稱相同的問題。
1.4.1 動態(tài)外部Jar包加載的設計方案
方案一、為外部 Jar 包生成單獨的 URLClassLoader,然后在泛化注冊時使用保存的 ClassLoader,在回調(diào)時進行切換 currentThread 的 ClassLoader,進行相同 API 接口不同版本的 Mock。
不可用原因: JavassistProxyFactory 中 final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
wapper 獲取的時候,使用的 makeWrapper 中默認使用的是ClassHelper.getClassLoader(c);導致一直會使用 AppClassLoader。API 信息會保存在一個 WapperMap 中,當消費者請求過來的時候,會優(yōu)先取這個 Map 找對應的 API 信息。
導致結果:
- 1.由于使用泛化注冊,所以 class 不在 AppClassLoader 中。設置了 currentThread 的 ClassLoader 不生效。
- 2.由于 dubbo 保存 API 信息只有一個 Map,所以導致發(fā)布的服務的 API 也只能有一套。
解決方案:
- 使用自定義 ClassLoader 進行加載外部 Jar 包中的 API 信息。
- 一臺 Mock 終端存一套 API 信息,更新 API 時需要重啟服務器。
方案二、在程序啟動時,使用自定義 TestPlatformClassLoader。還是給每個 Jar 包生成對應的 ApiClassLoader,由 TestPlatformClassLoader 統(tǒng)一管理。
不可用原因:
在 Mock 終端部署時,使用 -Djava.system.class.loader 設置 ClassLoader 時,JVM 啟動參數(shù)不可用。因為,TestPlatformClassLoader 不存在于當前 JVM 中,而是在工程代碼中。詳細參數(shù)如下:
-Djava.system.class.loader=com.youzan.test.mocker.internal.classloader.TestPlatformClassLoader
解決方案:(由架構師汪興提供)
- 使用自定義 Runnable(),保存程序啟動需要的 ClassLoader、啟動參數(shù)、mainClass 信息。
- 在程序啟動時,新起一個 Thread,傳入自定義 Runnable(),然后將該線程啟動。
方案三、使用自定義容器啟動服務
應用啟動流程,如下圖所示(下圖來自有贊架構團隊)

Java 的類加載遵循雙親委派的設計模式,從 AppClassLoader 開始自底向上尋找,并自頂向下加載,所以在沒有自定義 ClassLoader 時,應用的啟動是通過 AppClassLoader 去加載 Main 啟動類去運行。
自定義 ClassLoader 后,系統(tǒng) ClassLoader 將被設置成容器自定義的 ClassLoader,自定義 ClassLoader 重新去加載 Main 啟動類運行,此時后續(xù)所有的類加載都會先去自定義的 ClassLoader 里查找。
難點:應用默認系統(tǒng)類加載器是 AppClassLoader,在 New 對象時不會經(jīng)過自定義的 ClassLoader。
巧妙之處:Main 函數(shù)啟動時,AppClassLoader 加載 Main 和容器,容器獲取到 Main class,用自定義 ClassLoader 重新加載Main,設置系統(tǒng)類加載器為自定義類加載器,此時 New 對象都會經(jīng)過自定義的 ClassLoader。
1.4.2 設計方案選擇
以上三個方案,其實是實踐過程中的一個迭代。最終結果:
- 方案一、保留為外部Jar包生成單獨的 URLClassLoader。
- 方案二、保留自定義 TestPlatformClassLoader,使用 TestPlatformClassLoader 保存每個 Jar 包中 API 與其 ClassLoader 的對應關系。
- 方案三、采用自定義容器啟動,新起一個線程,并設置其 concurrentThreadClassLoader 為 TestPlatformClassLoader,用該線程啟動 Main.class。
1.4.3 遇到的坑
使用 Javassist 生成的 Class 名稱相同
使用 Javassist 生成的 Class,每個 Class 有單獨的 ClassName 以 Service Chain + className 組成。在重新生成相同名字的 class 時,即使使用 new ClassPool() 也不能完全隔離。因為生成 Class 的時候 Class<?> clazz = ctClass.toClass() 默認使用的是同一個 ClassLoader,所以會報“attempted duplicate class definition for name:****”。
解決方案:基于 ClassName 不是隨機生成的,所以只能基于之前的 ClassLoader 生成一個新的 SecureClassLoader(ClassLoader parent) 加載新的 class,舊的 ClassLoader 靠 Java 自動 GC。代碼如下:
Class<?> clazz = ctClass.toClass(new SecureClassLoader(clz.classLoader))
PS:該方案目前沒有做過壓測,不知道會不會導致內(nèi)存溢出。
二、方案實現(xiàn)
2.1 Mock 工廠整體設計架構

2.2 Mocker 容器設計圖

2.3 二方包管理時序圖

2.4 Mocker 容器服務注冊時序圖

三、支持場景
3.1 元素及名詞解釋

上圖所示為基本元素組成,相關名詞解釋如下:
- 消費者:調(diào)用方發(fā)起 DubboRequest
- Base 服務:不帶 Service Chain 標識的正常服務
- Mock 服務:通過 Mock 工廠生成的 dubbo 服務
- ETCD:注冊中心,此處同時注冊著 Base 服務和 Mock 服務
- 默認服務透傳:對接口中不需要 Mock 的方法,直接泛化調(diào)用 Base 服務
- 自定義服務(CF):用戶自己起一個泛化 dubbo 服務(PS:不需要注冊到注冊中心,也不需要 Service Chain 標識)
3.2 支持場景簡述
場景1:不帶 Service Chain 請求(不使用 Mock 服務時)
消費者從注冊中心獲取到 Base 環(huán)境服務的 IP+PORT,直接請求 Base 環(huán)境的服務。
場景2、帶 Service Chain 請求、Mock 服務采用 JSON 返回實現(xiàn)
消費者從注冊中心獲取到兩個地址:1.Base 環(huán)境服務的 IP+PORT;2.帶 Service Chain 標記服務(Mock服務)的 IP+PORT。根據(jù) Service Chain 調(diào)用路由,去請求 Mock 服務中的該方法,并返回 Mock 數(shù)據(jù)。
場景3、帶 Service Chain 請求、Mock 服務沒有該方法實現(xiàn)
消費者從注冊中心獲取到兩個地址:1.Base 環(huán)境服務的 IP+PORT;2.帶 Service Chain 標記服務(Mock 服務)的 IP+PORT。根據(jù) Service Chain 調(diào)用路由,去請求 Mock 服務。由于 Mock 服務中該方法是默認服務透傳,所以由 Mock 服務直接泛化調(diào)用 Base 服務,并返回數(shù)據(jù)。
場景4、帶 Service Chain 請求頭、Mock 服務采用自定義服務(CR)實現(xiàn)
消費者從注冊中心獲取到兩個地址:1.Base 環(huán)境服務的 IP+PORT;2.帶 Service Chain 標記服務(Mock 服務)的 IP+PORT。根據(jù) Service Chain 調(diào)用路由,去請求Mock服務。由于 Mock 服務中該方法是自定義服務(CF),所以由 Mock 服務調(diào)用用戶的 dubbo 服務,并返回數(shù)據(jù)。
場景5、帶 Service Chain 請求頭、Mock 服務沒有該方法實現(xiàn)、該方法又調(diào)用帶 Service Chain 的 InterfaceB 的方法
消費者調(diào)用 InterfaceA 的 Method3 時,從注冊中心獲取到兩個地址:1.Base 環(huán)境服務的 IP+PORT;2.帶 Service Chain 標記服務(Mock 服務)的 IP+PORT。根據(jù) Service Chain 調(diào)用路由,去請求 InterfaceA 的 Mock 服務。由于 Mock 服務中該方法是默認服務透傳,所以由 Mock 服務直接泛化調(diào)用 InterfaceA 的 Base 服務的Method3。
但是,由于 InterfaceA 的 Method3 是調(diào)用 InterfaceB 的 Method2,從注冊中心獲取到兩個地址:1.Base 環(huán)境服務的 IP+PORT;2.帶 Service Chain 標記服務(Mock 服務)的 IP+PORT。由于 Service Chain 標識在整個請求鏈路中是一直被保留的,所以根據(jù)Service Chain調(diào)用路由,最終請求到 InterfaceB 的 Mock 服務,并返回數(shù)據(jù)。
場景6、帶 Service Chain 請求頭、Mock已經(jīng)存在的 Service Chain 服務
由于不能同時存在兩個相同的 Service Chain 服務,所以需要降原先的 Service Chain 服務進行只訂閱、不注冊的操作。然后將Mock服務的透傳地址,配置為原 Service Chain 服務(即訂閱)。 消費者在進行請求時,只會從 ETCD 發(fā)現(xiàn) Mock 服務,其他同場景2、3、4、5。