基于動態(tài)代理 Mock dubbo 服務的實現(xiàn)方案

原文鏈接: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)思路如下圖所示)。
image

方案二、使用 Javassist,生成需要mock接口的Proxy實現(xiàn),并注冊到 ETCD 上(主要實現(xiàn)思路如下圖所示)。
image

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=*

在服務發(fā)現(xiàn)的時候,RegistryDirectory 中有個 map,保存了所有 Service 的注冊信息。也就是說, method=* 和正常 method=doNothing,say,age 被保存在了一起。
image

客戶端請求服務的時候,優(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 暴露服務的過程介紹

image

上圖(來自 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 服務消費的過程介紹

image

在消費端: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(),然后將該線程啟動。
方案三、使用自定義容器啟動服務

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

image

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 工廠整體設計架構

image

2.2 Mocker 容器設計圖

image

2.3 二方包管理時序圖

image

2.4 Mocker 容器服務注冊時序圖

image

三、支持場景

3.1 元素及名詞解釋

image

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

  • 消費者:調(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)境的服務。
image

場景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ù)。
image

場景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ù)。
image

場景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ù)。
image

場景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ù)。
image

場景6、帶 Service Chain 請求頭、Mock已經(jīng)存在的 Service Chain 服務

由于不能同時存在兩個相同的 Service Chain 服務,所以需要降原先的 Service Chain 服務進行只訂閱、不注冊的操作。然后將Mock服務的透傳地址,配置為原 Service Chain 服務(即訂閱)。 消費者在進行請求時,只會從 ETCD 發(fā)現(xiàn) Mock 服務,其他同場景2、3、4、5。
image
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容