1. 程式人生 > >一行程式碼搞定Dubbo介面呼叫

一行程式碼搞定Dubbo介面呼叫

本文來自網易雲社群

作者:呂彥峰

在工作中我們經常遇到關於介面測試的問題,無論是對於QA同學還是開發同學都會有遠端介面呼叫的需求。針對這種問題我研發了一個工具包,專門用於遠端Dubbo呼叫,下面就讓我們一起來學習一下。

主要解決的問題

  • 針對QA同學來講,如果對應的開發只是在某個任務中提供了介面,自己要怎麼測試?如何保證該介面在測試環境和預釋出環境都能測試通過?如果測試邊界值?

  • 針對開發同學來講,其他的業務方反饋說自己的介面在stabel_master上沒有返回資料或者少了欄位?stable_pre環境下資料返回不正確?線上資料不正確?開發要如何驗證自己的資料是否有問題呢?

remote-debug-util介紹

1.將工具jar包引入到本地pom檔案中,目前穩定版本問1.1.0

    <dependency>
        <groupId>com.netease.kaola</groupId>
        <artifactId>remote-debug-util</artifactId>
        <version>1.1.0</version>
    </dependency>

2.通過工具類邊寫具體的遠端呼叫邏輯 

AppGoodsServiceFacade appGoodsServiceFacade = InvokerFactory.getInstance("hst_test7", AppGoodsServiceFacade.class, EnvEnum.TEST_ENV);

通過以上呼叫,我們就拿到了hst_test7環境下的AppGoodsServiceFacade介面,具體要對介面進行怎麼樣的測試,就需要具體的開發自己確定了。
需要說明的一點是這樣子的:以上介面雖然指定了group和interface,但是沒有質指定version,version預設取的1.0版本。如果又其他版本的介面可以這麼呼叫:

AppGoodsServiceFacade appGoodsServiceFacade = InvokerFactory.getInstance("hst_test7", AppGoodsServiceFacade.class,"2.0", EnvEnum.TEST_ENV);

通過以上的呼叫我們就指定了version為2.0。 因為考拉的環境基本分為3個,TEST環境,PRE環境和ONLINE環境,所以通過最後的引數表示該環境為測試環境。如果介面呼叫的為PRE環境的話需要指定最後的環境引數為EnvEnum.PRE_ENV,Online環境的話指定為EnvEnum.ONLNIE_ENV。
但是上面的方式在工作環境中會遇到如下問題:

  • 預釋出環境和線上環境因為伺服器註冊服務時候是將自己的機房IP註冊到zk上的,我們及時拿到了具體服務的URL資訊,也無法掉的通。

  • 如果同一個伺服器上同時暴露了兩個服務,但是兩個服務的介面內容不一定,我們怎麼樣掉到我們指定的那一個介面。

針對第一個問題的解決方式:
因為機房IP我們無法ping的通所以介面掉不通,因為我們可以才作用Dubbo的url直連模式進行呼叫,這樣呼叫會相對於上面的情況稍微複雜一點:

    RemoteInvoker<GoodsFrontInventoryService> remoteInvoker = new RemoteInvoker("10.171.165.2:20880", "online","1.0", GoodsFrontInventoryService.class);
    GoodsFrontInventoryService goodsFrontInventoryService = remoteInvoker.getInvoker();

通過這兩行程式碼我們就拿到線上環境的GoodsFrontInventoryService介面,具體需要如何進行測試,對應的開發自己解決。
引數說明:

    /**
     * @param address 遠端服務的地址(host:port)
     * @param group 遠端服務的分組資訊 例如:stable_pre,online,hst_test1
     * @param version 介面的版本,預設為1.0
     * @param invokerClass 需要測試的介面
     */
    public RemoteInvoker(String address, String group, String version, Class invokerClass);

針對address的說明:adderss的host部分需要是本地可以ping通的ip地址,埠號可以不指定,如果不指定的話會預設填充為20880,如果具體的服務埠不是20880的話,會預設從20880重試至20890。這樣的話我們可以基本不同考慮服務埠的問題,除非埠號比較特殊的話需要自己在address中指定一下。

我在自己的使用過程中發現一個問題:IP地址和埠號比較多,不同環境都有不用的地址記憶起來非常繁瑣,因此希望可以只寫一次,以後都可以方便的呼叫,因此開發了alias配置模式,方式如下:

NewGoodsFacade newGoodsFacade = InvokerFactory.getInstance("goods.front.base-service2", NewGoodsFacade.class);

通過以上的呼叫方式就可以獲得goods-front工程的base-service2環境下的NewGoodsFacade介面。 

需要說明解析規則:這種方式使用的前提工作是自己配置好字首與對應地址的匹配規則,配置方式如下:
在自己的resource或者resource/config或者reource/config/locol目錄下建立remote-debug.properties配置檔案,檔案的內容大致如下:

    goods.front=com.netease.kaola.goods.constant.GoodsFrontConstant
    goods.compose=com.netease.kaola.goods.constant.GoodsComposeConstant

通過以上的規則可以確定:由goods.front字首可以找到GoodsFrontConstant這個配置檔案,該檔案的內容如下:

    public static final String GOODS_FRONT_ONLINE = "10.171.168.28:20880";    public static final String GOODS_FRONT_BETA = "10.164.104.66:20880";    public static final String GOODS_FRONT_BASE_SERVICE2 = "10.165.124.192:20880";    public static final String GOODS_FRONT_STABLE_DEV = "10.165.125.200:20880";    public static final String GOODS_FRONT_STABLE_PRE = "10.171.164.238:20881";    public static final String GOODS_FRONT_PRE5 = "10.171.160.28:20882";

解析規則:
例如:goods.front.base-service2 將所有的小寫變為大寫,.(英文點號)和-(中劃線)都替換為_(下劃線),因此goods.front.base-service2可以替換為GOODS_FRONT_BASE_SERVICE2。然後就可以找到對應的服務地址。
解析規則比較固定,目前不支援自定義解析規則,基本只要在首次使用的時候引入配置檔案,然後每次需要新增環境的時候把對應環境的地址資訊加上就好,埠如果配置錯了也沒有問題,工具會進行一定的容錯(在首次出錯之後再次從20880重試到20890,直到有可用的介面返回為止)。

基於自己的經驗對於工具類的使用作出以下建議:

1.測試環境下還是使用最簡便的方式,直接配置group和interface呼叫
2.常用工程下的介面測試建議配置alias模式,以方便自己以後對於其他介面的測試

3.不常用的介面直接配置地址和版本資訊直接呼叫

有時候為了不想在自己的工程內新增多餘的垃圾程式碼(因為遠端介面呼叫的程式碼實際上既不屬於業務程式碼,也不屬於單元測試,在工程中存在意義有點奇怪),因此也可以將工具類clone到本地,然後直接在工具類本地工程中邊寫測試程式碼。
工具類對於所有的模式都是支援,而且對於alias模式有更方便的支援,那就是可以直接在constant目錄下配置指定的地址檔案,不需要再次建立remote-debug.properties工具類。

實現方式解讀

  • 對於remote-debug工具類的定位就是一個純粹的工具類,不需要啟動Spring來載入dubbo的配置資訊,就跟呼叫自己本地寫的簡單方法一樣,基本上點選run之後就立馬會有結果響應。

  • 除了dubbo和zk之外沒有任何依賴,做到足夠小,足夠精。

工具實現的核心來時Dubbo中ReferenceConfig提供的直連模式,基本的執行原理如下:
如果是註冊中心模式的情況下:

  • 通過提供的group和version,interface資訊構造consumer端的直連URL。

  • 根據提供的環境資訊連線到對應的zk叢集

  • 根據URL資訊從Registry中找到與其匹配的提供者URL列表

  • 遍歷所有的URL資訊直到拿到可用的provider為止。

如果是alias或者基本配置模式:

  • 解析alias資訊,找到對應的地址(如果需要)

  • 根據配置資訊構造基本的URL

  • 通過Echo來測試介面的可用性,並負責重試。

  • 如果有異常的話將異常轉換為可讀的異常並返回/返回代理結果。

下面對於核心的處理邏輯進行介紹:

    /**
     * 獲得可用的RemoteInvoker物件
     * @param referenceConfig 這裡已經將對應的配置資訊轉換為ReferenceConfig物件
     * @return
     */
    private T getAvaiableRemoteInvoker(ReferenceConfig<T> referenceConfig) {
        T result = null;
        EchoService echoService = null;        try {
            result = referenceConfig.get();            //迴響測試
            echoService = (EchoService)result;
            echoService.$echo("OK");
        } catch (Exception e) {            //this.invoker主要為自定義的配置類
            String host = this.invokerConfig.getAddress().split(":")[0];            for (int i = defaultPort; i < endPort; i++) {
                invokerConfig.setAddress(host + ":" + i);
                referenceConfig = initRefConfig(invokerConfig, false);                try {
                    result = referenceConfig.get();
                    echoService = (EchoService)result;
                    echoService.$echo("OK");                    break;
                } catch (Exception e1){                    continue;
                }
            }
        }        if (result == null) {            throw new RuntimeException("Get remote invoker error, please check your network(host,port,VPN) by ping "
                    + invokerConfig.getHost() + "or telnet host ip");
        }        return result;
    }
    /**
     * 根據key讀取配置的host:port 規則:goods.front.stable_master -> GOODS_FRONT_STABLE_MASTER
     * @param key
     * @return
     */
    public static String getAddress(String key) {
        checkKeyNotNull(key);
        String[] keyGroup = key.split("\\.");
        StringBuilder sb = new StringBuilder();
        sb.append(".");        //讀取資料直到最後一個點號為止
        for (int i = 0; i < keyGroup.length - 1; i++) {
            sb.append(keyGroup[i].substring(0, 1).toUpperCase());
            sb.append(keyGroup[i].substring(1));
        }
        sb.append("Constant");
        String className = constantDir + sb.toString();        //先從工程constant目錄下讀取資料,如果讀不到就從remote-debug.properties指定的配置檔案中讀資料
        try {
            String result = loadAddress(key, className);            return result;
        } catch (Exception e) {            return getAddressByDefinedProp(key);
        }
    }
    /**
     * 根據配置資訊拿到最紅的介面代理
     * @param group 
     * @param version
     * @param interfaceClass
     * @param env 環境
     * @return
     */
    public static <T> T getInstance(String group, String version, Class interfaceClass, EnvEnum env) {
        URL url = URLUtil.valueOf(group, version, interfaceClass);
        String registryAddress = null;        if (env == null || env.getType() == EnvEnum.TEST_ENV.getType()) {
            registryAddress = ZkAddressEnum.TEST_ZK_ADDRESS.getAddress();
        } else if (env.getType() == EnvEnum.PRE_ENV.getType()) {
            registryAddress = ZkAddressEnum.PRE_ZK_ADDRESS.getAddress();
        } else {
            registryAddress = ZkAddressEnum.ONLINE_ZK_ADDRESS.getAddress();
        }        if (registryAddress == null) {            throw new RuntimeException("can not find registry address");
        }
        URL registryUrl = URL.valueOf(registryAddress);
        Registry registry = registryFactory.getRegistry(registryUrl);        if (registry == null) {            throw new RuntimeException("can not find registry, registryUrl is " + url.toFullString());
        }
        List<URL> providerUrls = registry.lookup(url);        //可用的provider可能有多個,因此會逐漸嘗試直到有可用的為止
        if (providerUrls != null && providerUrls.size() > 0) {
            T result = getInvoker(providerUrls);            if (result == null) {                throw new RuntimeException("can not find class " + interfaceClass.getName() + "in registry");
            }            return result;
        }        throw new RuntimeException("can not find matched url in registry");
    }

對於原始碼也只是設計了一些重要的流程,因為篇幅有限所有不能把所有的內容都講解清楚。

基本上通過remote-debug的呼叫,文章開頭提出的兩個問題都可以得到完美的解決,作為商品前臺的開發,我經常需要向其他業務方證明我或者他人的介面是沒有問題的,基本上我都是通過工具類呼叫介面,然後返回資料給他們看。尤其是當我遇到線上問題的時候,你會發現這種方式檢視介面返回資料是有多麼優雅~~~~

如果,你曾被telnet拼引數時候的蛋疼氣到過;
如果,你曾經在SOA上呼叫某個引數複雜的介面並且心情錯亂過;
如果,你是一個以簡為美的技術哥哥或者QA姐姐;
請速速轉移到remote-debug工具包,開啟遠端呼叫的新生活吧。

本文來自網易雲社群,經作者呂彥峰授權釋出