本文來自網易雲社區
作者:呂彥峰
在工作中我們經常遇到關於接口測試的問題,無論是對於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工具包,開啟遠程調用的新生活吧。
本文來自網易雲社區,經作者呂彥峰授權發布
相關文章:
【推薦】 MySQL Group Replication數據安全性保障