1. 程式人生 > >基於動態代理 Mock Dubbo 服務的實現方案

基於動態代理 Mock Dubbo 服務的實現方案

序言

背景概述

公司目前 Java 專案提供服務都是基於 Dubbo 框架的,而且 Dubbo 框架已經成為大部分國內網際網路公司選擇的一個基礎元件。

在日常專案協作過程中,其實會碰到服務不穩定、不滿足需求場景等情況,很多開發都會通過在本地使用 Mocktio 等單測工具作為自測輔助。那麼,在聯調、測試等協作過程中怎麼處理?

其實,Dubbo 開發者估計也是遇到了這樣的問題,所以提供了一個提供泛化服務註冊的入口。但是在服務發現的時候有個弊端,就說通過服務發現去請求這個 Mock 服務的話,在註冊中心必須只有一個服務有效,否則消費者會請求到其他非Mock服務上去。

為了解決這個問題,Dubbo 開發者又提供了泛化呼叫的入口。既支援通過註冊中心發現服務,又支援通過 IP+PORT 去直接呼叫服務,這樣就能保證消費者呼叫的是 Mock 出來的服務了。

以上泛化服務註冊和泛化服務呼叫結合起來,看似已經是一個閉環,可以解決 Dubbo 服務的 Mock 問題。但是,結合日常工作使用時,會出現一些麻煩的問題:

服務提供方使用公用的註冊中心,消費方無法準確呼叫

消費者不可能更改程式碼,去直連 Mock 服務

使用私有註冊中心能解決以上問題,但是 Mock 最小緯度為 Method,一個 Service 中被 Mock 的 Method 會正常處理,沒有被 Mock 的 Method 會異常,導致服務方需要 Mock Service 的全部方法

在解決以上麻煩的前提下,為了能快速註冊一個需要的 Dubbo 服務,提高專案協作過程中的工作效率,開展了 Mock 工廠的設計與實現。

功能概述

Mock Dubbo 服務

單個伺服器,支援部署多個相同和不同的 Service

動態上、下線服務

非 Mock 的 Method 透傳到基礎服務

一、方案探索

1.1 基於 Service Chain 選擇 Mock 服務的實現方式

1.1.1 Service Chain 簡單介紹

在業務發起的源頭新增 Service Chain 標識,這些標識會在接下來的跨應用遠端呼叫中一直透傳並且基於這些標識進行路由,這樣我們只需要把涉及到需求變更的應用的例項單獨部署,並新增到 Service Chain 的資料結構定義裡面,就可以虛擬出一個邏輯鏈路,該鏈路從邏輯上與其他鏈路是完全隔離的,並且可以共享那些不需要進行需求變更的應用例項。根據當前呼叫的透傳標識以及 Service Chain 的基礎元資料進行路由,路由原則如下:

當前呼叫包含 Service Chain 標識,則路由到歸屬於該 Service Chain 的任意服務節點,如果沒有歸屬於該

Service Chain 的服務節點,則排除掉所有隸屬於 Service Chain 的服務節點之後路由到任意服務節點

當前呼叫沒有包含 Service Chain 標識,則排除掉所有隸屬於 Service Chain 的服務節點之後路由到任意服務節點

當前呼叫包含 Service Chain 標識,並且當前應用也屬於某個 Service Chain 時,如果兩者不等則丟擲路由異常

以 Dubbo 框架為例,給出了一個 Service Chain 實現架構圖(下圖來自有贊架構團隊)

 

 

1.1.2 Mock 服務實現設計方案

方案一、基於 GenericService 生成需要 Mock 介面的泛化實現,並註冊到 ETCD 上(主要實現思路如下圖所示)。

 

 

方案二、使用 Javassist,生成需要mock介面的Proxy實現,並註冊到 ETCD 上(主要實現思路如下圖所示)。

 

 

1.1.3 設計方案比較

方案一優點:實現簡單,能滿足mock需求

繼承 GenericService,只要實現一個$invoke( String methodName, String[]parameterTypes, Object[]objects ),可以根據具體請求引數做出自定義返回資訊。

介面資訊只要知道介面名、protocol 即可。

即使該服務已經存在,也能因為 generic 欄位,讓消費者優先消費該 mock service。

缺點:與公司的服務發現機制衝突

由於有贊服務背景,在使用 Haunt 服務發現時,是會同時返回正常服務和帶有 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=*

在服務發現的時候,RegistryDirectory 中有個 map,儲存了所有 Service 的註冊資訊。也就是說, method=* 和正常 method=doNothing,say,age 被儲存在了一起。

 

 

客戶端請求服務的時候,優先匹配到正常的服務的 method,而不會去呼叫泛化服務。

導致結果:訪問時,會跳過 genericFilter,報 no available invoke。

方案二優點:Proxy 實現,自動生成一個正常的 Dubbo 介面實現

1.Javassist 有現成的方法生成介面實現位元組碼,大大簡化了對使用者程式碼依賴。例如:

返回 String、Json 等,對單 method 的 mock 實現,都無需使用者上傳實現類。

透傳時統一由平臺控制,不配置 mock 的方法預設就會進行透傳,而且保留 Service Chain 標記。

2.Mock 服務註冊 method 資訊完整。

3.生成介面 Proxy 物件時,嚴格按照介面定義進行生成,返回資料型別有保障。

缺點:

無優先消費選擇功能。

位元組碼後臺生成,不利於排查生成的 Proxy 中存在問題。

1.1.4 選擇結果

由於做為平臺,不僅僅需要滿足 mock 需求,還需要減少使用者操作,以及支援現有公司服務架構體系,所以選擇設計方案二。

1.2 基於動態代理結合 ServiceConfig 實現動態上、下線服務

1.2.1 Dubbo 暴露服務的過程介紹

 

 

上圖(來自 dubbo 開發者文件)暴露服務時序圖: 首先 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 實現為 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 完成註冊後,會呼叫 afterPropertiesSet() 方法,該方法中呼叫

export

() 完成服務的註冊。在

ServiceConfig

中的 doExport() 方法中,會對服務的各個引數進行校驗。

 

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

;

   }

註冊過程中會進行判斷該實現類的型別。其中如果實現了 GenericService 介面,那麼會在暴露服務資訊時,將 generic 設定為 true,暴露方法就為*。如果不是,就會按正常服務進行新增服務的方法。此處就是我們可以實現 Mock 的切入點,使用 Javassist 根據自定義的 Mock 資訊,寫一個實現類的 class 檔案並生成一個例項注入到 ServiceConfig 中。生成 class 例項如下所示,與一個正常的實現類完全一致,以及註冊的服務跟正常服務也完全一致。

 

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 將自定義的實現類注入,並完成註冊,實現如下:

 

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

"釋出成功 : ${sc}-${service.interface}"

       }

else

{

           log.error

"釋出失敗 : ${sc}-${service.interface}"

       }

   }

通過 service.setRef(genericService)完成實現類的注入,最終通過 service.export()完成服務註冊。ref 的值已經被塞進來,並附帶 ServiceChain 標記儲存至 service 的 paramters 中。具體服務到 Invoker 的轉化以及 Invoker 轉換到 Exporter,Exporter 到 URL 的轉換都會附帶上 ServiceChain 標記註冊到註冊中心。

1.2.2 生成實現類設計方案

方案一、 支援指定 String(或 Json) 對單個 method 進行 mock。

功能介紹:根據入參 String or Json,生成代理物件。由 methodName 和 methodParams 獲取唯一 method 定義。(指支援單個方法mock)。消費者請求到Mock服務的對應Mock Method時,Mock服務將儲存的資料轉成對應的返回型別,並返回。

方案二、 支援指定 String(或 Json) 對多個 method生成 mock。

功能介紹:根據入參 String or Json,生成代理物件。method 對應的 mock 資料由 methodMockMap 指定,由 methodName 獲取唯一 method 定義,所以被 mock 介面不能有過載方法(只支援多個不同方法 mock)。消費者請求到 Mock 服務的對應 mock method 時,Mock 服務將儲存的資料轉成對應的返回型別,並返回。

方案三、 在使用 實現類(Impl) 的情況下,支援傳入一個指定的 method 進行 mock。

功能介紹:根據入參的實現類,生成代理物件。由 methodName 和 methodParams 獲取唯一 method 定義。(支援 mock 一個方法)。消費者請求到 Mock 服務的對應 mock method 時,Mock 服務呼叫該實現類的對應方法,並返回。

方案四、 在使用 實現類(Impl) 的情況下,支援傳入多個 method 進行 mock。

功能介紹:根據入參的實現類,生成代理物件。由 methodName 獲取唯一 method 定義,所以被 mock 介面不能有過載方法(只支援一個實現類 mock 多個方法)。消費者請求到 Mock 服務的對應 mock method 時,Mock 服務呼叫該實現類的對應方法,並返回。

方案五、 使用 Custom Reference 對多個 method 進行 mock。

功能介紹:根據入參 ServiceReference,生成代理物件。method 對應的自定義 ServiceReference 由 methodMockMap 指定,由 methodName 獲取唯一method定義,所以被 mock 介面不能有過載方法(只支援多個不同方法 mock)。消費者請求到 Mock 服務的對應 mock method 時,Mock 服務會主動請求自定義的 Dubbo 服務。

1.2.3 設計方案選擇

以上五種方案,其實就是整個 Mock 工廠實現的一個迭代過程。在每個方案的嘗試中,發現各自的弊端然後出現了下一種方案。目前,在結合各種使用場景後,選擇了方案二、方案五。

方案三、方案四被排除的主要原因:Dubbo 對已經發布的 Service 儲存了實現類的 ClassLoader,相同 className 的類一旦註冊成功後,會將實現類的 ClassLoader 儲存到記憶體中,很難被刪除。所以想要使用這兩種方案的話,需要頻繁變更實現類的 className,大大降低了一個工具的易用性。改用自定義 Dubbo 服務(方案五),替代自定義實現類,但是需要使用者自己起一個 Dubbo 服務,並告知 IP+PORT。

方案一其實是方案二的補集,能支援 Service 過載方法的 Mock。由於在使用時,需要傳入具體 Method 的簽名信息,增加了使用者操作成本。由於公司內部保證一個 Service 不可能有過載方法,且為了提高使用效率,不開放該方案。後期如果出現這樣的有過載方法的情況,再進行開放。

1.2.4 遇到的坑

基礎資料型別需要特殊處理

使用 Javassist 根據介面 class 寫一個實現類的 class 檔案,遇到最讓人頭疼的就是方法簽名和返回值。如果方法的簽名和返回值為基礎資料型別時,那在傳參和返回時需要做特殊處理。平臺中本人使用了最笨的列舉處理方法,如果有使用 Javassist 的高手,有好的建議麻煩不吝賜教。程式碼如下:

 

/** 引數存在基本資料型別時,預設使用基本資料型別

    * 基本型別包含:

    * 實數:double、float

    * 整數: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 配置檔案時就會呼叫這些解析器生成對應的 BeanDefinition 交給 Spring 管理。Spring 在初始化 IOC 容器時會利用這裡註冊的 BeanDefinitionParser 的 parse 方法獲取對應的 ReferenceBean 的 BeanDefinition 例項,由於 ReferenceBean 實現了 InitializingBean 介面,在設定了 Bean 的所有屬性後會呼叫 afterPropertiesSet 方法。afterPropertiesSet 方法中的 getObject 會呼叫父類 ReferenceConfig 的 init 方法完成組裝。ReferenceConfig 類的 init 方法呼叫 Protocol 的 refer 方法生成 Invoker 例項,這是服務消費的關鍵。接下來把 Invoker 轉換為客戶端需要的介面(如:StudentInfoService)。由 ReferenceConfig 切入,通過 API 方式使用 Dubbo 的泛化呼叫,程式碼如下:

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

}

如上程式碼所示,具體業務 DTO 型別,泛化呼叫結果非僅結果資料,還包含 DTO 的 class 資訊,需要特殊處理結果,取出需要的結果進行返回。

1.3.2 記錄dubbo服務請求設計方案

方案一、捕獲請求資訊

服務提供方和服務消費方呼叫過程攔截,Dubbo 本身的大多功能均基於此擴充套件點實現,每次遠端方法執行,該攔截都會被執行。Provider 提供的呼叫鏈,具體的呼叫鏈程式碼是在 ProtocolFilterWrapper 的 buildInvokerChain 完成的,具體是將註解中含有 group=provider 的 Filter 實現,按照 order 排序,最後的呼叫順序是 EchoFilter->ClassLoaderFilter->GenericFilter->ContextFilter->ExceptionFilter->TimeoutFilter->MonitorFilter->TraceFilter。 其中:EchoFilter 的作用是判斷是否是回聲測試請求,是的話直接返回內容。回聲測試用於檢測服務是否可用,回聲測試按照正常請求流程執行,能夠測試整個呼叫是否通暢,可用於監控。ClassLoaderFilter 則只是在主功能上添加了功能,更改當前執行緒的 ClassLoader。

在 ServiceConfig 繼承 AbstractInterfaceConfig,中有 filter 屬性。以此為切入點,給每個 Mock 服務新增 filter,記錄每次 dubbo 服務請求資訊(介面、方法、入參、返回、響應時長)。

方案二、記錄請求資訊

將請求資訊儲存在記憶體中,一個介面的每個被 Mock 的方法儲存近 10次 記錄資訊。使用二級快取儲存,快取程式碼如下:

 

@Singleton

(lazy =

true

)

 

class

CacheUtil

{

 

private

static

final

Object

PRESENT =

new

Object

()

 

private

int

maxInterfaceSize =

10000

 

// 最大介面快取數量

 

private

int

maxRequestSize =

10

 

// 最大請求快取數量

 

private

Cache

<

String

,

Cache

<

RequestDO

,

Object

>> caches =

CacheBuilder

.newBuilder()

               .maximumSize(maxInterfaceSize)

               .expireAfterAccess(

7

,

TimeUnit

.DAYS)    

// 7天未被請求的介面,快取回收

               .build()

   }  

如上程式碼所示,二級快取中的一個 Object 是被浪費的記憶體空間,但是由於想不到其他更好的方案,所以暫時保留該設計。

1.3.3 遇到的坑

泛化呼叫時引數物件轉換

使用 ReferenceConfig 進行服務直接呼叫,繞過了對一個介面方法簽名的校驗,所以在進行泛化呼叫時,最大的問題就是 Object[] 內的引數型別了。每次當遇到資料型別問題時,本人只會用最笨的辦法,列舉解決。程式碼如下:

 

/** 引數存在基本資料型別時,預設使用基本資料型別

    * 基本型別包含:

    * 實數:double、float

    * 整數: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

       }

   }

如以上程式碼所示,是將傳入引數轉成對應的包裝型別。當介面的簽名如果為 int,那麼入參物件是 Integer 也是可以的。因為$invoke(StringmethodName,String[]paramsTypes,Object[]objects),是由 paramsTypes 檢查方法簽名,然後再將 objects 傳入具體服務中進行呼叫。

ReferenceConfig 初始化優先設定 initialize 為 true

使用泛化呼叫發起遠端 Dubbo 服務請求,在發起 invoke 前,有GenericServicegenericService=referenceConfig.get()操作。當 Dubbo 服務沒有起來,此時首次發起呼叫後,進行 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!"

);

       }

 

// 獲取消費者全域性配置

       checkDefault();

       appendProperties(

this

);

 

if

(getGeneric() ==

&& getConsumer() !=

) {

           setGeneric(getConsumer().getGeneric());

       }

       ...

   }

結果導致:由於第一次初始化的時候,先把 initialize 設定為 true,但是後面未獲取到有效的 genericService,導致後面即使 Dubbo 服務起來後,也會泛化呼叫失敗。

解決方案:泛化呼叫就是使用 genericService 執行 invoke 呼叫,所以每次請求都使用一個新的 ReferenceConfig,當初始化進行 get() 操作時報異常或返回為 null 時,不儲存;直到初始化進行 get() 操作時獲取到有效的 genericService 時,將該 genericService 儲存起來。實現程式碼如下:

 

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 單個伺服器,支援部署多個相同和不同的Service

根據需求,需要解決兩個問題:1.伺服器執行過程中,外部API的Jar包載入問題;2.註冊多個相同介面服務時,名稱相同的問題。

1.4.1 動態外部Jar包載入的設計方案

方案一、為外部 Jar 包生成單獨的 URLClassLoader,然後在泛化註冊時使用儲存的 ClassLoader,在回撥時進行切換 currentThread 的 ClassLoader,進行相同 API 介面不同版本的 Mock。

不可用原因:

JavassistProxyFactory 中finalWrapperwrapper=Wrapper.getWrapper(proxy.getClass().getName().indexOf('$')<0?proxy.getClass():type);wapper 獲取的時候,使用的 makeWrapper 中預設使用的是 ClassHelper.getClassLoader(c);導致一直會使用 AppClassLoader。API 資訊會儲存在一個 WapperMap 中,當消費者請求過來的時候,會優先取這個 Map 找對應的 API 資訊。

導致結果:

1.由於使用泛化註冊,所以 class 不在 AppClassLoader 中。設定了 currentThread 的 ClassLoader 不生效。

2.由於 dubbo 儲存 API 資訊只有一個 Map,所以導致釋出的服務的 API 也只能有一套。

解決方案:

使用自定義 ClassLoader 進行載入外部 Jar 包中的 API 資訊。

一臺 Mock 終端存一套 API 資訊,更新 API 時需要重啟伺服器。

方案二、在程式啟動時,使用自定義 TestPlatformClassLoader。還是給每個 Jar 包生成對應的 ApiClassLoader,由 TestPlatformClassLoader 統一管理。

不可用原因:

在 Mock 終端部署時,使用 -Djava.system.class.loader設定 ClassLoader 時,JVM 啟動引數不可用。因為,TestPlatformClassLoader 不存在於當前 JVM 中,而是在工程程式碼中。詳細引數如下: -Djava.system.class.loader= com.youzan.test.mocker.internal.classloader.TestPlatformClassLoader

解決方案:(由架構師汪興提供)

使用自定義 Runnable(),儲存程式啟動需要的 ClassLoader、啟動引數、mainClass 資訊。

在程式啟動時,新起一個 Thread,傳入自定義 Runnable(),然後將該執行緒啟動。

方案三、使用自定義容器啟動服務

應用啟動流程,如下圖所示(下圖來自有贊架構團隊)

 

 

Java 的類載入遵循雙親委派的設計模式,從 AppClassLoader 開始自底向上尋找,並自頂向下載入,所以在沒有自定義 ClassLoader 時,應用的啟動是通過 AppClassLoader 去載入 Main 啟動類去執行。

自定義 ClassLoader 後,系統 ClassLoader 將被設定成容器自定義的 ClassLoader,自定義 ClassLoader 重新去載入 Main 啟動類執行,此時後續所有的類載入都會先去自定義的 ClassLoader 裡查詢。

難點:應用預設系統類載入器是 AppClassLoader,在 New 物件時不會經過自定義的 ClassLoader。

巧妙之處:Main 函式啟動時,AppClassLoader 載入 Main 和容器,容器獲取到 Main class,用自定義 ClassLoader 重新載入Main,設定系統類載入器為自定義類載入器,此時 New 物件都會經過自定義的 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 時,即使使用 newClassPool()也不能完全隔離。因為生成 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(newSecureClassLoader(clz.classLoader))

PS:該方案目前沒有做過壓測,不知道會不會導致記憶體溢位。

二、方案實現

2.1 Mock 工廠整體設計架構

 

 

2.2 Mocker 容器設計圖

 

 

2.3 二方包管理時序圖

 

 

2.4 Mocker 容器服務註冊時序圖

 

 

三、支援場景

3.1 元素及名詞解釋

 

 

上圖所示為基本元素組成,相關名詞解釋如下:

消費者:呼叫方發起 DubboRequest

Base 服務:不帶 Service Chain 標識的正常服務

Mock 服務:通過 Mock 工廠生成的 dubbo 服務

ETCD:註冊中心,此處同時註冊著 Base 服務和 Mock 服務

預設服務透傳:對介面中不需要 Mock 的方法,直接泛化呼叫 Base 服務

自定義服務(CF):使用者自己起一個泛化 dubbo 服務(PS:不需要註冊到註冊中心,也不需要 Service Chain 標識)

3.2 支援場景簡述

場景1:不帶 Service Chain 請求(不使用 Mock 服務時)

消費者從註冊中心獲取到 Base 環境服務的 IP+PORT,直接請求 Base 環境的服務。

 

 

場景2、帶 Service Chain 請求、Mock 服務採用 JSON 返回實現

消費者從註冊中心獲取到兩個地址:1.Base 環境服務的 IP+PORT;2.帶 Service Chain 標記服務(Mock服務)的 IP+PORT。根據 Service Chain 呼叫路由,去請求 Mock 服務中的該方法,並返回 Mock 資料。

 

 

場景3、帶 Service Chain 請求、Mock 服務沒有該方法實現

消費者從註冊中心獲取到兩個地址:1.Base 環境服務的 IP+PORT;2.帶 Service Chain 標記服務(Mock 服務)的 IP+PORT。根據 Service Chain 呼叫路由,去請求 Mock 服務。由於 Mock 服務中該方法是預設服務透傳,所以由 Mock 服務直接泛化呼叫 Base 服務,並返回資料。

 

 

場景4、帶 Service Chain 請求頭、Mock 服務採用自定義服務(CR)實現

消費者從註冊中心獲取到兩個地址:1.Base 環境服務的 IP+PORT;2.帶 Service Chain 標記服務(Mock 服務)的 IP+PORT。根據 Service Chain 呼叫路由,去請求Mock服務。由於 Mock 服務中該方法是自定義服務(CF),所以由 Mock 服務呼叫使用者的 dubbo 服務,並返回資料。

 

 

場景5、帶 Service Chain 請求頭、Mock 服務沒有該方法實現、該方法又呼叫帶 Service Chain 的 InterfaceB 的方法

消費者呼叫 InterfaceA 的 Method3 時,從註冊中心獲取到兩個地址:1.Base 環境服務的 IP+PORT;2.帶 Service Chain 標記服務(Mock 服務)的 IP+PORT。根據 Service Chain 呼叫路由,去請求 InterfaceA 的 Mock 服務。由於 Mock 服務中該方法是預設服務透傳,所以由 Mock 服務直接泛化呼叫 InterfaceA 的 Base 服務的Method3。

但是,由於 InterfaceA 的 Method3 是呼叫 InterfaceB 的 Method2,從註冊中心獲取到兩個地址:1.Base 環境服務的 IP+PORT;2.帶 Service Chain 標記服務(Mock 服務)的 IP+PORT。由於 Service Chain 標識在整個請求鏈路中是一直被保留的,所以根據Service Chain呼叫路由,最終請求到 InterfaceB 的 Mock 服務,並返回資料。

 

 

場景6、帶 Service Chain 請求頭、Mock已經存在的 Service Chain 服務

由於不能同時存在兩個相同的 Service Chain 服務,所以需要降原先的 Service Chain 服務進行只訂閱、不註冊的操作。然後將Mock服務的透傳地址,配置為原 Service Chain 服務(即訂閱)。

消費者在進行請求時,只會從 ETCD 發現 Mock 服務,其他同場景2、3、4、5。

 

 

四、結束語

Mock平臺實踐過程中,遇到很多的難題.

歡迎工作一到五年的Java工程師朋友們加入Java程式設計師開發: 854393687
群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!