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

序言

背景概述

公司目前 Java 項目提供服務都是基於 Dubbo 框架的,並且 Dubbo 框架已經成爲大部分國內互聯網公司選擇的一個基礎組件。java

在平常項目協做過程當中,其實會碰到服務不穩定、不知足需求場景等狀況,不少開發都會經過在本地使用 Mocktio 等單測工具做爲自測輔助。那麼,在聯調、測試等協做過程當中怎麼處理?spring

其實,Dubbo 開發者估計也是遇到了這樣的問題,因此提供了一個提供泛化服務註冊的入口。可是在服務發現的時候有個弊端,就說經過服務發現去請求這個 Mock 服務的話,在註冊中心必須只有一個服務有效,不然消費者會請求到其餘非Mock服務上去。json

爲了解決這個問題,Dubbo 開發者又提供了泛化調用的入口。既支持經過註冊中心發現服務,又支持經過 IP+PORT 去直接調用服務,這樣就能保證消費者調用的是 Mock 出來的服務了。設計模式

以上泛化服務註冊和泛化服務調用結合起來,看似已是一個閉環,能夠解決 Dubbo 服務的 Mock 問題。可是,結合平常工做使用時,會出現一些麻煩的問題:api

  • 服務提供方使用公用的註冊中心,消費方沒法準確調用
  • 消費者不可能更改代碼,去直連 Mock 服務
  • 使用私有註冊中心能解決以上問題,可是 Mock 最小緯度爲 Method,一個 Service 中被 Mock 的 Method 會正常處理,沒有被 Mock 的 Method 會異常,致使服務方須要 Mock Service 的所有方法

在解決以上麻煩的前提下,爲了能快速註冊一個須要的 Dubbo 服務,提升項目協做過程當中的工做效率,開展了 Mock 工廠的設計與實現。緩存

功能概述

  • Mock Dubbo 服務
  • 單個服務器,支持部署多個相同和不一樣的 Service
  • 動態上、下線服務
  • 非 Mock 的 Method 透傳到基礎服務

1、方案探索

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 服務實現設計方案

方案1、基於 GenericService 生成須要 Mock 接口的泛化實現,並註冊到 ETCD 上(主要實現思路以下圖所示)。 app

image

方案2、使用 Javassist,生成須要mock接口的Proxy實現,並註冊到 ETCD 上(主要實現思路以下圖所示)。

image

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 被保存在了一塊兒。

image
客戶端請求服務的時候,優先匹配到正常的服務的 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 暴露服務的過程介紹

image

上圖(來自 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[])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 將自定義的實現類注入,並完成註冊,實現以下:

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 生成實現類設計方案

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

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

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

功能介紹:根據入參 String or Json,生成代理對象。method 對應的 mock 數據由 methodMockMap 指定,由 methodName 獲取惟一 method 定義,因此被 mock 接口不能有重載方法(只支持多個不一樣方法 mock)。消費者請求到 Mock 服務的對應 mock method 時,Mock 服務將保存的數據轉成對應的返回類型,並返回。

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

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

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

功能介紹:根據入參的實現類,生成代理對象。由 methodName 獲取惟一 method 定義,因此被 mock 接口不能有重載方法(只支持一個實現類 mock 多個方法)。消費者請求到 Mock 服務的對應 mock method 時,Mock 服務調用該實現類的對應方法,並返回。

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

功能介紹:根據入參 ServiceReference,生成代理對象。method 對應的自定義 ServiceReference 由 methodMockMap 指定,由 methodName 獲取惟一method定義,因此被 mock 接口不能有重載方法(只支持多個不一樣方法 mock)。消費者請求到 Mock 服務的對應 mock method 時,Mock 服務會主動請求自定義的 Dubbo 服務。

1.2.3 設計方案選擇

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

方案3、方案四被排除的主要緣由: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 服務消費的過程介紹

image

在消費端: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 != 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
}
複製代碼

如上代碼所示,具體業務 DTO 類型,泛化調用結果非僅結果數據,還包含 DTO 的 class 信息,須要特殊處理結果,取出須要的結果進行返回。

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

方案1、捕獲請求信息

服務提供方和服務消費方調用過程攔截,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 服務請求信息(接口、方法、入參、返回、響應時長)。

方案2、記錄請求信息

將請求信息保存在內存中,一個接口的每一個被 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(String methodName, String[] paramsTypes, Object[] objects),是由 paramsTypes 檢查方法簽名,而後再將 objects 傳入具體服務中進行調用。

ReferenceConfig 初始化優先設置 initialize 爲 true

使用泛化調用發起遠程 Dubbo 服務請求,在發起 invoke 前,有 GenericService genericService = referenceConfig.get() 操做。當 Dubbo 服務沒有起來,此時首次發起調用後,進行 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 服務起來後,也會泛化調用失敗。

解決方案:泛化調用就是使用 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 != 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

根據需求,須要解決兩個問題:1.服務器運行過程當中,外部API的Jar包加載問題;2.註冊多個相同接口服務時,名稱相同的問題。

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

方案1、爲外部 Jar 包生成單獨的 URLClassLoader,而後在泛化註冊時使用保存的 ClassLoader,在回調時進行切換 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 中,當消費者請求過來的時候,會優先取這個 Map 找對應的 API 信息。

致使結果:

  • 1.因爲使用泛化註冊,因此 class 不在 AppClassLoader 中。設置了 currentThread 的 ClassLoader 不生效。
  • 2.因爲 dubbo 保存 API 信息只有一個 Map,因此致使發佈的服務的 API 也只能有一套。

解決方案:

  • 使用自定義 ClassLoader 進行加載外部 Jar 包中的 API 信息。
  • 一臺 Mock 終端存一套 API 信息,更新 API 時須要重啓服務器。
方案2、在程序啓動時,使用自定義 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(),而後將該線程啓動。
方案3、使用自定義容器啓動服務

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

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 設計方案選擇

以上三個方案,實際上是實踐過程當中的一個迭代。最終結果:

  • 方案1、保留爲外部Jar包生成單獨的 URLClassLoader。
  • 方案2、保留自定義 TestPlatformClassLoader,使用 TestPlatformClassLoader 保存每一個 Jar 包中 API 與其 ClassLoader 的對應關係。
  • 方案3、採用自定義容器啓動,新起一個線程,並設置其 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:該方案目前沒有作過壓測,不知道會不會致使內存溢出。

2、方案實現

2.1 Mock 工廠總體設計架構

image

2.2 Mocker 容器設計圖

image

2.3 二方包管理時序圖

image

2.4 Mocker 容器服務註冊時序圖

image

3、支持場景

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 環境的服務。

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

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

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

消費者從註冊中心獲取到兩個地址:1.Base 環境服務的 IP+PORT;2.帶 Service Chain 標記服務(Mock 服務)的 IP+PORT。根據 Service Chain 調用路由,去請求 Mock 服務。因爲 Mock 服務中該方法是默認服務透傳,因此由 Mock 服務直接泛化調用 Base 服務,並返回數據。

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

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

場景五、帶 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 服務,並返回數據。

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

因爲不能同時存在兩個相同的 Service Chain 服務,因此須要降原先的 Service Chain 服務進行只訂閱、不註冊的操做。而後將Mock服務的透傳地址,配置爲原 Service Chain 服務(即訂閱)。 消費者在進行請求時,只會從 ETCD 發現 Mock 服務,其餘同場景二、三、四、5。

4、結束語

Mock平臺實踐過程當中,遇到不少的難題,此處須要特別感謝架構組何煒龍、汪興的友情支持。後續還有不少須要完善的,但願你們能多提寶貴意見(郵箱:zhongyingying@youzan.com)。

相關文章
相關標籤/搜索