序言
背景概述
公司目前 Java 項(xiàng)目提供服務(wù)都是基于 Dubbo 框架的萌衬,而且 Dubbo 框架已經(jīng)成為大部分國(guó)內(nèi)互聯(lián)網(wǎng)公司選擇的一個(gè)基礎(chǔ)組件。
在日常項(xiàng)目協(xié)作過程中哑子,其實(shí)會(huì)碰到服務(wù)不穩(wěn)定、不滿足需求場(chǎng)景等情況,很多開發(fā)都會(huì)通過在本地使用 Mocktio 等單測(cè)工具作為自測(cè)輔助炭序。那么,在聯(lián)調(diào)苍日、測(cè)試等協(xié)作過程中怎么處理惭聂?
其實(shí),Dubbo 開發(fā)者估計(jì)也是遇到了這樣的問題相恃,所以提供了一個(gè)提供泛化服務(wù)注冊(cè)的入口辜纲。但是在服務(wù)發(fā)現(xiàn)的時(shí)候有個(gè)弊端,就說通過服務(wù)發(fā)現(xiàn)去請(qǐng)求這個(gè) Mock 服務(wù)的話拦耐,在注冊(cè)中心必須只有一個(gè)服務(wù)有效耕腾,否則消費(fèi)者會(huì)請(qǐng)求到其他非Mock服務(wù)上去。
為了解決這個(gè)問題杀糯,Dubbo 開發(fā)者又提供了泛化調(diào)用的入口扫俺。既支持通過注冊(cè)中心發(fā)現(xiàn)服務(wù),又支持通過 IP+PORT 去直接調(diào)用服務(wù)火脉,這樣就能保證消費(fèi)者調(diào)用的是 Mock 出來的服務(wù)了牵舵。
以上泛化服務(wù)注冊(cè)和泛化服務(wù)調(diào)用結(jié)合起來柒啤,看似已經(jīng)是一個(gè)閉環(huán),可以解決 Dubbo 服務(wù)的 Mock 問題畸颅。但是担巩,結(jié)合日常工作使用時(shí),會(huì)出現(xiàn)一些麻煩的問題:
服務(wù)提供方使用公用的注冊(cè)中心没炒,消費(fèi)方無法準(zhǔn)確調(diào)用
消費(fèi)者不可能更改代碼涛癌,去直連 Mock 服務(wù)
使用私有注冊(cè)中心能解決以上問題,但是 Mock 最小緯度為 Method送火,一個(gè) Service 中被 Mock 的 Method 會(huì)正常處理拳话,沒有被 Mock 的 Method 會(huì)異常,導(dǎo)致服務(wù)方需要 Mock Service 的全部方法
在解決以上麻煩的前提下种吸,為了能快速注冊(cè)一個(gè)需要的 Dubbo 服務(wù)弃衍,提高項(xiàng)目協(xié)作過程中的工作效率,開展了 Mock 工廠的設(shè)計(jì)與實(shí)現(xiàn)坚俗。
功能概述
Mock Dubbo 服務(wù)
單個(gè)服務(wù)器镜盯,支持部署多個(gè)相同和不同的 Service
動(dòng)態(tài)上、下線服務(wù)
非 Mock 的 Method 透?jìng)鞯交A(chǔ)服務(wù)
一猖败、方案探索
1.1 基于 Service Chain 選擇 Mock 服務(wù)的實(shí)現(xiàn)方式
1.1.1 Service Chain 簡(jiǎn)單介紹
在業(yè)務(wù)發(fā)起的源頭添加 Service Chain 標(biāo)識(shí)速缆,這些標(biāo)識(shí)會(huì)在接下來的跨應(yīng)用遠(yuǎn)程調(diào)用中一直透?jìng)鞑⑶一谶@些標(biāo)識(shí)進(jìn)行路由,這樣我們只需要把涉及到需求變更的應(yīng)用的實(shí)例單獨(dú)部署恩闻,并添加到 Service Chain 的數(shù)據(jù)結(jié)構(gòu)定義里面艺糜,就可以虛擬出一個(gè)邏輯鏈路,該鏈路從邏輯上與其他鏈路是完全隔離的幢尚,并且可以共享那些不需要進(jìn)行需求變更的應(yīng)用實(shí)例破停。根據(jù)當(dāng)前調(diào)用的透?jìng)鳂?biāo)識(shí)以及 Service Chain 的基礎(chǔ)元數(shù)據(jù)進(jìn)行路由,路由原則如下:
當(dāng)前調(diào)用包含 Service Chain 標(biāo)識(shí)侠草,則路由到歸屬于該 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)識(shí)边涕,則排除掉所有隸屬于 Service Chain 的服務(wù)節(jié)點(diǎn)之后路由到任意服務(wù)節(jié)點(diǎn)
當(dāng)前調(diào)用包含 Service Chain 標(biāo)識(shí)晤碘,并且當(dāng)前應(yīng)用也屬于某個(gè) Service Chain 時(shí),如果兩者不等則拋出路由異常
以 Dubbo 框架為例功蜓,給出了一個(gè) Service Chain 實(shí)現(xiàn)架構(gòu)圖(下圖來自有贊架構(gòu)團(tuán)隊(duì))
1.1.2 Mock 服務(wù)實(shí)現(xiàn)設(shè)計(jì)方案
方案一园爷、基于 GenericService 生成需要 Mock 接口的泛化實(shí)現(xiàn),并注冊(cè)到 ETCD 上(主要實(shí)現(xiàn)思路如下圖所示)式撼。
方案二童社、使用 Javassist,生成需要mock接口的Proxy實(shí)現(xiàn)著隆,并注冊(cè)到 ETCD 上(主要實(shí)現(xiàn)思路如下圖所示)扰楼。
1.1.3 設(shè)計(jì)方案比較
方案一優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單呀癣,能滿足mock需求
繼承 GenericService蝙眶,只要實(shí)現(xiàn)一個(gè)$invoke( String methodName, String[]parameterTypes, Object[]objects )棺妓,可以根據(jù)具體請(qǐng)求參數(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í),是會(huì)同時(shí)返回正常服務(wù)和帶有 Service Chain 標(biāo)記的泛化服務(wù)旦装,所以必然存在兩種類型的服務(wù)页衙。導(dǎo)致帶有 Service Chain 標(biāo)記的消費(fèi)者在正常請(qǐng)求泛化服務(wù)時(shí)報(bào) no available invoke。
例:注冊(cè)了 2個(gè) HelloService:
正常的 :generic=false&interface=com.alia.api.HelloService&methods=doNothing,say,age
泛化的:generic=true&interface=com.alia.api.HelloService&methods=*
在服務(wù)發(fā)現(xiàn)的時(shí)候同辣,RegistryDirectory 中有個(gè) map拷姿,保存了所有 Service 的注冊(cè)信息。也就是說旱函, method=* 和正常 method=doNothing,say,age 被保存在了一起。
客戶端請(qǐng)求服務(wù)的時(shí)候描滔,優(yōu)先匹配到正常的服務(wù)的 method棒妨,而不會(huì)去調(diào)用泛化服務(wù)。
導(dǎo)致結(jié)果:訪問時(shí)含长,會(huì)跳過 genericFilter券腔,報(bào) no available invoke。
方案二優(yōu)點(diǎn):Proxy 實(shí)現(xiàn)拘泞,自動(dòng)生成一個(gè)正常的 Dubbo 接口實(shí)現(xiàn)
1.Javassist 有現(xiàn)成的方法生成接口實(shí)現(xiàn)字節(jié)碼纷纫,大大簡(jiǎn)化了對(duì)用戶代碼依賴。例如:
返回 String陪腌、Json 等辱魁,對(duì)單 method 的 mock 實(shí)現(xiàn),都無需用戶上傳實(shí)現(xiàn)類诗鸭。
透?jìng)鲿r(shí)統(tǒng)一由平臺(tái)控制染簇,不配置 mock 的方法默認(rèn)就會(huì)進(jìn)行透?jìng)鳎冶A?Service Chain 標(biāo)記强岸。
2.Mock 服務(wù)注冊(cè) method 信息完整锻弓。
3.生成接口 Proxy 對(duì)象時(shí),嚴(yán)格按照接口定義進(jìn)行生成蝌箍,返回?cái)?shù)據(jù)類型有保障青灼。
缺點(diǎn):
無優(yōu)先消費(fèi)選擇功能暴心。
字節(jié)碼后臺(tái)生成,不利于排查生成的 Proxy 中存在問題杂拨。
1.1.4 選擇結(jié)果
由于做為平臺(tái)专普,不僅僅需要滿足 mock 需求,還需要減少用戶操作扳躬,以及支持現(xiàn)有公司服務(wù)架構(gòu)體系脆诉,所以選擇設(shè)計(jì)方案二。
1.2 基于動(dòng)態(tài)代理結(jié)合 ServiceConfig 實(shí)現(xiàn)動(dòng)態(tài)上贷币、下線服務(wù)
1.2.1 Dubbo 暴露服務(wù)的過程介紹
上圖(來自 dubbo 開發(fā)者文檔)暴露服務(wù)時(shí)序圖: 首先 ServiceConfig 類拿到對(duì)外提供服務(wù)的實(shí)際類 ref(如:StudentInfoServiceImpl),然后通過 ProxyFactory 類的 getInvoker 方法使用 ref 生成一個(gè) AbstractProxyInvoker 實(shí)例击胜。到這一步就完成具體服務(wù)到 Invoker 的轉(zhuǎn)化。接下來就是 Invoker 轉(zhuǎn)換到 Exporter 的過程,Exporter 會(huì)通過轉(zhuǎn)化為 URL 的方式暴露服務(wù)役纹。 從 dubbo 源碼來看偶摔,dubbo 通過 Spring 框架提供的 Schema 可擴(kuò)展機(jī)制,擴(kuò)展了自己的配置支持促脉。dubbo-container 通過封裝 Spring 容器辰斋,來啟動(dòng)了 Spring 上下文,此時(shí)它會(huì)去解析 Spring 的 bean 配置文件(Spring 的 xml 配置文件)瘸味,當(dāng)解析 dubbo:service 標(biāo)簽時(shí)宫仗,會(huì)用 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
會(huì)將配置標(biāo)簽進(jìn)行解析,并生成對(duì)應(yīng)的
Javabean
藕夫,最終注冊(cè)到
Spring
Ioc
容器中。 對(duì)
ServiceBean
進(jìn)行注冊(cè)時(shí)枯冈,其
implements
InitializingBean
接口毅贮,當(dāng) bean 完成注冊(cè)后,會(huì)調(diào)用 afterPropertiesSet() 方法尘奏,該方法中調(diào)用
export
() 完成服務(wù)的注冊(cè)滩褥。在
ServiceConfig
中的 doExport() 方法中,會(huì)對(duì)服務(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
;
? ?}
注冊(cè)過程中會(huì)進(jìn)行判斷該實(shí)現(xiàn)類的類型瑰煎。其中如果實(shí)現(xiàn)了 GenericService 接口,那么會(huì)在暴露服務(wù)信息時(shí)琢感,將 generic 設(shè)置為 true丢间,暴露方法就為*。如果不是驹针,就會(huì)按正常服務(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)類完全一致,以及注冊(cè)的服務(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
[])
);
? ? ? ?}
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)類注入绿满,并完成注冊(cè),實(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ù)注冊(cè)喇颁。ref 的值已經(jīng)被塞進(jìn)來,并附帶 ServiceChain 標(biāo)記保存至 service 的 paramters 中嚎货。具體服務(wù)到 Invoker 的轉(zhuǎn)化以及 Invoker 轉(zhuǎn)換到 Exporter橘霎,Exporter 到 URL 的轉(zhuǎn)換都會(huì)附帶上 ServiceChain 標(biāo)記注冊(cè)到注冊(cè)中心。
1.2.2 生成實(shí)現(xiàn)類設(shè)計(jì)方案
方案一殖属、 支持指定?String(或 Json)?對(duì)單個(gè) method 進(jìn)行 mock姐叁。
功能介紹:根據(jù)入?yún)?String or Json,生成代理對(duì)象洗显。由 methodName 和 methodParams 獲取唯一 method 定義外潜。(指支持單個(gè)方法mock)。消費(fèi)者請(qǐng)求到Mock服務(wù)的對(duì)應(yīng)Mock Method時(shí)挠唆,Mock服務(wù)將保存的數(shù)據(jù)轉(zhuǎn)成對(duì)應(yīng)的返回類型处窥,并返回。
方案二玄组、 支持指定?String(或 Json)?對(duì)多個(gè) method生成 mock碧库。
功能介紹:根據(jù)入?yún)?String or Json,生成代理對(duì)象巧勤。method 對(duì)應(yīng)的 mock 數(shù)據(jù)由 methodMockMap 指定,由 methodName 獲取唯一 method 定義弄匕,所以被 mock 接口不能有重載方法(只支持多個(gè)不同方法 mock)颅悉。消費(fèi)者請(qǐng)求到 Mock 服務(wù)的對(duì)應(yīng) mock method 時(shí),Mock 服務(wù)將保存的數(shù)據(jù)轉(zhuǎn)成對(duì)應(yīng)的返回類型迁匠,并返回剩瓶。
方案三、 在使用?實(shí)現(xiàn)類(Impl)?的情況下城丧,支持傳入一個(gè)指定的 method 進(jìn)行 mock延曙。
功能介紹:根據(jù)入?yún)⒌膶?shí)現(xiàn)類,生成代理對(duì)象亡哄。由 methodName 和 methodParams 獲取唯一 method 定義枝缔。(支持 mock 一個(gè)方法)。消費(fèi)者請(qǐng)求到 Mock 服務(wù)的對(duì)應(yīng) mock method 時(shí),Mock 服務(wù)調(diào)用該實(shí)現(xiàn)類的對(duì)應(yīng)方法愿卸,并返回灵临。
方案四、 在使用?實(shí)現(xiàn)類(Impl)?的情況下趴荸,支持傳入多個(gè) method 進(jìn)行 mock儒溉。
功能介紹:根據(jù)入?yún)⒌膶?shí)現(xiàn)類,生成代理對(duì)象发钝。由 methodName 獲取唯一 method 定義顿涣,所以被 mock 接口不能有重載方法(只支持一個(gè)實(shí)現(xiàn)類 mock 多個(gè)方法)。消費(fèi)者請(qǐng)求到 Mock 服務(wù)的對(duì)應(yīng) mock method 時(shí)酝豪,Mock 服務(wù)調(diào)用該實(shí)現(xiàn)類的對(duì)應(yīng)方法涛碑,并返回。
方案五寓调、 使用?Custom Reference?對(duì)多個(gè) method 進(jìn)行 mock锌唾。
功能介紹:根據(jù)入?yún)?ServiceReference,生成代理對(duì)象夺英。method 對(duì)應(yīng)的自定義 ServiceReference 由 methodMockMap 指定晌涕,由 methodName 獲取唯一method定義,所以被 mock 接口不能有重載方法(只支持多個(gè)不同方法 mock)痛悯。消費(fèi)者請(qǐng)求到 Mock 服務(wù)的對(duì)應(yīng) mock method 時(shí)余黎,Mock 服務(wù)會(huì)主動(dòng)請(qǐng)求自定義的 Dubbo 服務(wù)。
1.2.3 設(shè)計(jì)方案選擇
以上五種方案载萌,其實(shí)就是整個(gè) Mock 工廠實(shí)現(xiàn)的一個(gè)迭代過程惧财。在每個(gè)方案的嘗試中,發(fā)現(xiàn)各自的弊端然后出現(xiàn)了下一種方案扭仁。目前垮衷,在結(jié)合各種使用場(chǎng)景后,選擇了方案二乖坠、方案五搀突。
方案三、方案四被排除的主要原因:Dubbo 對(duì)已經(jīng)發(fā)布的 Service 保存了實(shí)現(xiàn)類的 ClassLoader熊泵,相同 className 的類一旦注冊(cè)成功后仰迁,會(huì)將實(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í)需要做特殊處理。平臺(tái)中本人使用了最笨的枚舉處理方法冤议,如果有使用 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 透?jìng)鞯交A(chǔ)服務(wù)
1.3.1 Dubbo 服務(wù)消費(fèi)的過程介紹
在消費(fèi)端:Spring 解析 dubbo:reference 時(shí),Dubbo 首先使用?com.alibaba.dubbo.config.spring.schema.NamespaceHandler?注冊(cè)解析器义矛,當(dāng) Spring 解析 xml 配置文件時(shí)就會(huì)調(diào)用這些解析器生成對(duì)應(yīng)的 BeanDefinition 交給 Spring 管理发笔。Spring 在初始化 IOC 容器時(shí)會(huì)利用這里注冊(cè)的 BeanDefinitionParser 的 parse 方法獲取對(duì)應(yīng)的 ReferenceBean 的 BeanDefinition 實(shí)例,由于 ReferenceBean 實(shí)現(xiàn)了 InitializingBean 接口凉翻,在設(shè)置了 Bean 的所有屬性后會(huì)調(diào)用 afterPropertiesSet 方法筐咧。afterPropertiesSet 方法中的 getObject 會(huì)調(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 !=
&& 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 =
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ù)請(qǐng)求設(shè)計(jì)方案
方案一侧巨、捕獲請(qǐng)求信息
服務(wù)提供方和服務(wù)消費(fèi)方調(diào)用過程攔截舅锄,Dubbo 本身的大多功能均基于此擴(kuò)展點(diǎn)實(shí)現(xiàn),每次遠(yuǎn)程方法執(zhí)行司忱,該攔截都會(huì)被執(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 的作用是判斷是否是回聲測(cè)試請(qǐng)求幔荒,是的話直接返回內(nèi)容∈崦担回聲測(cè)試用于檢測(cè)服務(wù)是否可用爹梁,回聲測(cè)試按照正常請(qǐng)求流程執(zhí)行,能夠測(cè)試整個(gè)調(diào)用是否通暢汽纠,可用于監(jiān)控卫键。ClassLoaderFilter 則只是在主功能上添加了功能,更改當(dāng)前線程的 ClassLoader虱朵。
在 ServiceConfig 繼承 AbstractInterfaceConfig莉炉,中有 filter 屬性。以此為切入點(diǎn)碴犬,給每個(gè) Mock 服務(wù)添加 filter,記錄每次 dubbo 服務(wù)請(qǐng)求信息(接口絮宁、方法、入?yún)⒎⒎祷厣馨骸㈨憫?yīng)時(shí)長(zhǎng))。
方案二偿荷、記錄請(qǐng)求信息
將請(qǐng)求信息保存在內(nèi)存中窘游,一個(gè)接口的每個(gè)被 Mock 的方法保存近 10次 記錄信息。使用二級(jí)緩存保存跳纳,緩存代碼如下:
@Singleton
(lazy =
true
)
class
CacheUtil
{
private
static
final
Object
PRESENT =
new
Object
()
private
int
maxInterfaceSize =
10000
// 最大接口緩存數(shù)量
private
int
maxRequestSize =
10
// 最大請(qǐng)求緩存數(shù)量
private
Cache
<
String
,
Cache
<
RequestDO
,
Object
>> caches =
CacheBuilder
.newBuilder()
? ? ? ? ? ? ? ?.maximumSize(maxInterfaceSize)
? ? ? ? ? ? ? ?.expireAfterAccess(
7
,
TimeUnit
.DAYS) ? ?
// 7天未被請(qǐng)求的接口忍饰,緩存回收
? ? ? ? ? ? ? ?.build()
? ?} ?
如上代碼所示,二級(jí)緩存中的一個(gè) Object 是被浪費(fèi)的內(nèi)存空間寺庄,但是由于想不到其他更好的方案艾蓝,所以暫時(shí)保留該設(shè)計(jì)力崇。
1.3.3 遇到的坑
泛化調(diào)用時(shí)參數(shù)對(duì)象轉(zhuǎn)換
使用 ReferenceConfig 進(jìn)行服務(wù)直接調(diào)用,繞過了對(duì)一個(gè)接口方法簽名的校驗(yàn)赢织,所以在進(jìn)行泛化調(diào)用時(shí)亮靴,最大的問題就是 Object[] 內(nèi)的參數(shù)類型了。每次當(dāng)遇到數(shù)據(jù)類型問題時(shí)于置,本人只會(huì)用最笨的辦法茧吊,枚舉解決。代碼如下:
/** 參數(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)成對(duì)應(yīng)的包裝類型尿孔。當(dāng)接口的簽名如果為 int,那么入?yún)?duì)象是 Integer 也是可以的俊柔。因?yàn)?invoke(StringmethodName,String[]paramsTypes,Object[]objects),是由 paramsTypes 檢查方法簽名活合,然后再將 objects 傳入具體服務(wù)中進(jìn)行調(diào)用雏婶。
ReferenceConfig 初始化優(yōu)先設(shè)置 initialize 為 true
使用泛化調(diào)用發(fā)起遠(yuǎn)程 Dubbo 服務(wù)請(qǐng)求,在發(fā)起 invoke 前白指,有GenericServicegenericService=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 ==
|| interfaceName.length() ==
0
) {
throw
new
IllegalStateException
(
"<dubbo:reference interface=\"\" /> interface not allow null!"
);
? ? ? ?}
// 獲取消費(fèi)者全局配置
? ? ? ?checkDefault();
? ? ? ?appendProperties(
this
);
if
(getGeneric() ==
&& getConsumer() !=
) {
? ? ? ? ? ?setGeneric(getConsumer().getGeneric());
? ? ? ?}
? ? ? ?...
? ?}
結(jié)果導(dǎo)致:由于第一次初始化的時(shí)候,先把 initialize 設(shè)置為 true橄唬,但是后面未獲取到有效的 genericService赋焕,導(dǎo)致后面即使 Dubbo 服務(wù)起來后,也會(huì)泛化調(diào)用失敗仰楚。
解決方案:泛化調(diào)用就是使用 genericService 執(zhí)行 invoke 調(diào)用隆判,所以每次請(qǐng)求都使用一個(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 !=
&& 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
(
!= genericService) {
? ? ? ? ? ? ? ?hasInit =
true
? ? ? ? ? ?}
? ? ? ?}
? ?}
1.4 單個(gè)服務(wù)器,支持部署多個(gè)相同和不同的Service
根據(jù)需求笆豁,需要解決兩個(gè)問題:1.服務(wù)器運(yùn)行過程中郎汪,外部API的Jar包加載問題;2.注冊(cè)多個(gè)相同接口服務(wù)時(shí)闯狱,名稱相同的問題煞赢。
1.4.1 動(dòng)態(tài)外部Jar包加載的設(shè)計(jì)方案
方案一、為外部 Jar 包生成單獨(dú)的 URLClassLoader,然后在泛化注冊(cè)時(shí)使用保存的 ClassLoader哄孤,在回調(diào)時(shí)進(jìn)行切換 currentThread 的 ClassLoader照筑,進(jìn)行相同 API 接口不同版本的 Mock。
不可用原因:
JavassistProxyFactory 中finalWrapperwrapper=Wrapper.getWrapper(proxy.getClass().getName().indexOf('$')<0?proxy.getClass():type);wapper 獲取的時(shí)候瘦陈,使用的 makeWrapper 中默認(rèn)使用的是 ClassHelper.getClassLoader(c);導(dǎo)致一直會(huì)使用 AppClassLoader凝危。API 信息會(huì)保存在一個(gè) WapperMap 中,當(dāng)消費(fèi)者請(qǐng)求過來的時(shí)候晨逝,會(huì)優(yōu)先取這個(gè) Map 找對(duì)應(yīng)的 API 信息蛾默。
導(dǎo)致結(jié)果:
1.由于使用泛化注冊(cè),所以 class 不在 AppClassLoader 中捉貌。設(shè)置了 currentThread 的 ClassLoader 不生效支鸡。
2.由于 dubbo 保存 API 信息只有一個(gè) Map,所以導(dǎo)致發(fā)布的服務(wù)的 API 也只能有一套趁窃。
解決方案:
使用自定義 ClassLoader 進(jìn)行加載外部 Jar 包中的 API 信息牧挣。
一臺(tái) Mock 終端存一套 API 信息,更新 API 時(shí)需要重啟服務(wù)器醒陆。
方案二瀑构、在程序啟動(dòng)時(shí),使用自定義 TestPlatformClassLoader刨摩。還是給每個(gè) Jar 包生成對(duì)應(yīng)的 ApiClassLoader寺晌,由 TestPlatformClassLoader 統(tǒng)一管理。
不可用原因:
在 Mock 終端部署時(shí)码邻,使用 -Djava.system.class.loader設(shè)置 ClassLoader 時(shí)折剃,JVM 啟動(dòng)參數(shù)不可用。因?yàn)橄裎荩琓estPlatformClassLoader 不存在于當(dāng)前 JVM 中怕犁,而是在工程代碼中。詳細(xì)參數(shù)如下:?-Djava.system.class.loader= com.youzan.test.mocker.internal.classloader.TestPlatformClassLoader
解決方案:(由架構(gòu)師汪興提供)
使用自定義 Runnable()己莺,保存程序啟動(dòng)需要的 ClassLoader奏甫、啟動(dòng)參數(shù)、mainClass 信息凌受。
在程序啟動(dòng)時(shí)阵子,新起一個(gè) Thread,傳入自定義 Runnable()胜蛉,然后將該線程啟動(dòng)挠进。
方案三色乾、使用自定義容器啟動(dòng)服務(wù)
應(yīng)用啟動(dòng)流程,如下圖所示(下圖來自有贊架構(gòu)團(tuán)隊(duì))
Java 的類加載遵循雙親委派的設(shè)計(jì)模式领突,從 AppClassLoader 開始自底向上尋找暖璧,并自頂向下加載,所以在沒有自定義 ClassLoader 時(shí)君旦,應(yīng)用的啟動(dòng)是通過 AppClassLoader 去加載 Main 啟動(dòng)類去運(yùn)行澎办。
自定義 ClassLoader 后,系統(tǒng) ClassLoader 將被設(shè)置成容器自定義的 ClassLoader金砍,自定義 ClassLoader 重新去加載 Main 啟動(dòng)類運(yùn)行局蚀,此時(shí)后續(xù)所有的類加載都會(huì)先去自定義的 ClassLoader 里查找。
難點(diǎn):應(yīng)用默認(rèn)系統(tǒng)類加載器是 AppClassLoader恕稠,在 New 對(duì)象時(shí)不會(huì)經(jīng)過自定義的 ClassLoader琅绅。
巧妙之處:Main 函數(shù)啟動(dòng)時(shí),AppClassLoader 加載 Main 和容器谱俭,容器獲取到 Main class奉件,用自定義 ClassLoader 重新加載Main,設(shè)置系統(tǒng)類加載器為自定義類加載器昆著,此時(shí) New 對(duì)象都會(huì)經(jīng)過自定義的 ClassLoader县貌。
1.4.2 設(shè)計(jì)方案選擇
以上三個(gè)方案,其實(shí)是實(shí)踐過程中的一個(gè)迭代凑懂。最終結(jié)果:
方案一煤痕、保留為外部Jar包生成單獨(dú)的 URLClassLoader。
方案二接谨、保留自定義 TestPlatformClassLoader摆碉,使用 TestPlatformClassLoader 保存每個(gè) Jar 包中 API 與其 ClassLoader 的對(duì)應(yīng)關(guān)系。
方案三脓豪、采用自定義容器啟動(dòng)巷帝,新起一個(gè)線程,并設(shè)置其 concurrentThreadClassLoader 為 TestPlatformClassLoader扫夜,用該線程啟動(dòng) Main.class楞泼。
1.4.3 遇到的坑
使用 Javassist 生成的 Class 名稱相同
使用 Javassist 生成的 Class,每個(gè) Class 有單獨(dú)的 ClassName 以 Service Chain + className 組成笤闯。在重新生成相同名字的 class 時(shí)堕阔,即使使用 newClassPool()也不能完全隔離。因?yàn)樯?Class 的時(shí)候 Class<?>clazz=ctClass.toClass()默認(rèn)使用的是同一個(gè) ClassLoader颗味,所以會(huì)報(bào)“attempted duplicate class definition for name:**”超陆。
解決方案:基于 ClassName 不是隨機(jī)生成的,所以只能基于之前的 ClassLoader 生成一個(gè)新的 SecureClassLoader(ClassLoader parent) 加載新的 class浦马,舊的 ClassLoader 靠 Java 自動(dòng) GC时呀。代碼如下:?Class<?>clazz=ctClass.toClass(newSecureClassLoader(clz.classLoader))
PS:該方案目前沒有做過壓測(cè)张漂,不知道會(huì)不會(huì)導(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ù)注冊(cè)時(shí)序圖
三鹃锈、支持場(chǎng)景
3.1 元素及名詞解釋
上圖所示為基本元素組成,相關(guān)名詞解釋如下:
消費(fèi)者:調(diào)用方發(fā)起 DubboRequest
Base 服務(wù):不帶 Service Chain 標(biāo)識(shí)的正常服務(wù)
Mock 服務(wù):通過 Mock 工廠生成的 dubbo 服務(wù)
ETCD:注冊(cè)中心瞧预,此處同時(shí)注冊(cè)著 Base 服務(wù)和 Mock 服務(wù)
默認(rèn)服務(wù)透?jìng)鳎簩?duì)接口中不需要 Mock 的方法,直接泛化調(diào)用 Base 服務(wù)
自定義服務(wù)(CF):用戶自己起一個(gè)泛化 dubbo 服務(wù)(PS:不需要注冊(cè)到注冊(cè)中心仅政,也不需要 Service Chain 標(biāo)識(shí))
3.2 支持場(chǎng)景簡(jiǎn)述
場(chǎng)景1:不帶 Service Chain 請(qǐng)求(不使用 Mock 服務(wù)時(shí))
消費(fèi)者從注冊(cè)中心獲取到 Base 環(huán)境服務(wù)的 IP+PORT垢油,直接請(qǐng)求 Base 環(huán)境的服務(wù)。
場(chǎng)景2圆丹、帶 Service Chain 請(qǐng)求滩愁、Mock 服務(wù)采用 JSON 返回實(shí)現(xiàn)
消費(fèi)者從注冊(cè)中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT;2.帶 Service Chain 標(biāo)記服務(wù)(Mock服務(wù))的 IP+PORT辫封。根據(jù) Service Chain 調(diào)用路由硝枉,去請(qǐng)求 Mock 服務(wù)中的該方法,并返回 Mock 數(shù)據(jù)倦微。
場(chǎng)景3妻味、帶 Service Chain 請(qǐng)求、Mock 服務(wù)沒有該方法實(shí)現(xiàn)
消費(fèi)者從注冊(cè)中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT欣福;2.帶 Service Chain 標(biāo)記服務(wù)(Mock 服務(wù))的 IP+PORT责球。根據(jù) Service Chain 調(diào)用路由,去請(qǐng)求 Mock 服務(wù)拓劝。由于 Mock 服務(wù)中該方法是默認(rèn)服務(wù)透?jìng)鞒猓杂?Mock 服務(wù)直接泛化調(diào)用 Base 服務(wù),并返回?cái)?shù)據(jù)郑临。
場(chǎng)景4栖博、帶 Service Chain 請(qǐng)求頭、Mock 服務(wù)采用自定義服務(wù)(CR)實(shí)現(xiàn)
消費(fèi)者從注冊(cè)中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT;2.帶 Service Chain 標(biāo)記服務(wù)(Mock 服務(wù))的 IP+PORT。根據(jù) Service Chain 調(diào)用路由昌妹,去請(qǐng)求Mock服務(wù)廓鞠。由于 Mock 服務(wù)中該方法是自定義服務(wù)(CF),所以由 Mock 服務(wù)調(diào)用用戶的 dubbo 服務(wù)唆阿,并返回?cái)?shù)據(jù)。
場(chǎng)景5、帶 Service Chain 請(qǐng)求頭蠢正、Mock 服務(wù)沒有該方法實(shí)現(xiàn)、該方法又調(diào)用帶 Service Chain 的 InterfaceB 的方法
消費(fèi)者調(diào)用 InterfaceA 的 Method3 時(shí)省店,從注冊(cè)中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT嚣崭;2.帶 Service Chain 標(biāo)記服務(wù)(Mock 服務(wù))的 IP+PORT笨触。根據(jù) Service Chain 調(diào)用路由,去請(qǐng)求 InterfaceA 的 Mock 服務(wù)雹舀。由于 Mock 服務(wù)中該方法是默認(rèn)服務(wù)透?jìng)髀樱杂?Mock 服務(wù)直接泛化調(diào)用 InterfaceA 的 Base 服務(wù)的Method3。
但是说榆,由于 InterfaceA 的 Method3 是調(diào)用 InterfaceB 的 Method2虚吟,從注冊(cè)中心獲取到兩個(gè)地址:1.Base 環(huán)境服務(wù)的 IP+PORT;2.帶 Service Chain 標(biāo)記服務(wù)(Mock 服務(wù))的 IP+PORT签财。由于 Service Chain 標(biāo)識(shí)在整個(gè)請(qǐng)求鏈路中是一直被保留的串慰,所以根據(jù)Service Chain調(diào)用路由,最終請(qǐng)求到 InterfaceB 的 Mock 服務(wù)唱蒸,并返回?cái)?shù)據(jù)邦鲫。
場(chǎng)景6、帶 Service Chain 請(qǐng)求頭神汹、Mock已經(jīng)存在的 Service Chain 服務(wù)
由于不能同時(shí)存在兩個(gè)相同的 Service Chain 服務(wù)庆捺,所以需要降原先的 Service Chain 服務(wù)進(jìn)行只訂閱、不注冊(cè)的操作屁魏。然后將Mock服務(wù)的透?jìng)鞯刂诽弦裕渲脼樵?Service Chain 服務(wù)(即訂閱)。
消費(fèi)者在進(jìn)行請(qǐng)求時(shí)蚁堤,只會(huì)從 ETCD 發(fā)現(xiàn) Mock 服務(wù)醉者,其他同場(chǎng)景2、3披诗、4撬即、5。
四呈队、結(jié)束語
Mock平臺(tái)實(shí)踐過程中剥槐,遇到很多的難題.
歡迎工作一到五年的Java工程師朋友們加入Java程序員開發(fā): 854393687
群內(nèi)提供免費(fèi)的Java架構(gòu)學(xué)習(xí)資料(里面有高可用、高并發(fā)宪摧、高性能及分布式粒竖、Jvm性能調(diào)優(yōu)、Spring源碼几于,MyBatis蕊苗,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個(gè)知識(shí)點(diǎn)的架構(gòu)資料)合理利用自己每一分每一秒的時(shí)間來學(xué)習(xí)提升自己,不要再用"沒有時(shí)間“來掩飾自己思想上的懶惰沿彭!趁年輕朽砰,使勁拼,給未來的自己一個(gè)交代!