干貨 | Dubbo 接口測試原理及多種方法實踐總結


在這里插入圖片描述

本文為霍格沃茲測試學院優秀學員學習筆記,進階學習文末加群。

更多技術文章分享及測試資料點此獲取

1、什么是 Dubbo?

Dubbo 最開始是應用於淘寶網,由阿里巴巴開源的一款優秀的高性能服務框架,由 Java 開發,后來貢獻給了 Apache 開源基金會組織。

下面以官網的一個說明來了解一下架構的演變過程,從而了解 Dubbo 的誕生原因:

單一應用架構

當網站流量很小時,只需一個應用,將所有功能都部署在一起,以減少部署節點和成本。此時,用於簡化增刪改查工作量的數據訪問框架(ORM)是關鍵。

垂直應用架構

當訪問量逐漸增大,單一應用增加機器帶來的加速度越來越小,提升效率的方法之一是將應用拆成互不相干的幾個應用,以提升效率。此時,用於加速前端頁面開發的 Web 框架(MVC)是關鍵。

分布式服務架構

當垂直應用越來越多,應用之間交互不可避免,將核心業務抽取出來,作為獨立的服務,逐漸形成穩定的服務中心,使前端應用能更快速的響應多變的市場需求。此時,用於提高業務復用及整合的分布式服務框架(RPC)是關鍵。

流動計算架構

當服務越來越多,容量的評估,小服務資源的浪費等問題逐漸顯現,此時需增加一個調度中心基於訪問壓力實時管理集群容量,提高集群利用率。此時,用於提高機器利用率的資源調度和治理中心(SOA)是關鍵。

2、Dubbo 架構簡介


Dubbo 比較有特點的地方就是這個注冊中心,平常我們測試較多的 HTTP 接口,直接請求接口,調用后端服務即可;而 Dubbo是要先走注冊中心獲取服務的位置,下面來舉個現實生活中的例子來說明。

現實舉例

好比大家平常約朋友一起出去吃飯,聽說川菜館“贈李白”不錯,然后需要找這家飯店在哪(用小藍或小黃App),知道了具體的地址才出發,至於是走路,打車還是騎車,就隨意了。

這里 App 就相當於注冊中心(Registry),我們這群吃貨就是消費者(Consumer),商家屬於生產者(Provider)。商家把自己的信息注冊在 App 上,消費者根據 App 查詢到商家的信息,再根據信息找到商家進行消費。

2.1、Zookeeper 簡介

之前經常有小伙伴問我 zk 干啥的?怎么用?下面就來簡單了解一哈:

ZK,全稱就是zookeeper,是 Apache 軟件基金會的一個軟件項目,它為大型分布式計算提供開源的分布式配置服務、同步服務和命名注冊。

下面的圖示也可以清晰的說明zk的部署和工作的一些方式(具體的技術細節需要的話可以針對zk專門搜索學習):

Leader:集群工作的核心,事務請求的唯一調度和處理者,保證事務處理的順序性。對於有寫操作的請求,需統一轉發給Leader處理。Leader需決定編號執行操作。

Follower:處理客戶端非事務請求,轉發事務請求轉發給Leader,參與Leader選舉。

Observer觀察者:進行非事務請求的獨立處理,對於事務請求,則轉發給Leader服務器進行處理.不參與投票。

3、什么是 Dubbo 接口?

所謂的 Dubbo 接口,其實就是一個個 Dubbo 服務中的方法,而測試 Dubbo 接口就相當於我們測試人員充當消費者或者創造消費者去"消費"這個方法。

具體的方式有很多,代碼、工具、命令皆可,在接下來的內容中來一一演示。

4、Dubbo 接口測試(創造消費者)

以下我將以本地的一個簡單的 Dubbo 服務 demo 為例,演示 Dubbo 測試的各種方法。

interface只有兩個,分別是OrderService和UserService

OrderService:

package com.qinzhen.testmall.service;

import com.qinzhen.testmall.bean.UserAddress;
import java.util.List;

public interface OrderService {

    /**
     * 初始化訂單
     * @param userID
     */
    public List<UserAddress> initOrder(String userID);
}

UserService:

package com.qinzhen.testmall.service;

import com.qinzhen.testmall.bean.UserAddress;
import java.util.List;

/**
 * 用戶服務
 */
public interface UserService {


    /**
     * 按照userId返回所有的收獲地址
     * @param userId
     * @return
     */
    public List<UserAddress> getUserAddressList(String userId);


    /**
     * 返回所有的收獲地址
     * @param 
     * @return
     */
    public List<UserAddress> getUserAddressList();
}

JavaBean 對象 UserAddress 如下:

package com.qinzhen.testmall.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;

@AllArgsConstructor
@Data
public class UserAddress implements Serializable {

    private Integer id;
    private String userAddress; //用戶地址
    private String userId; //用戶ID
    private String consignee; //收貨人
    private String phoneNum; //電話號碼
    private String isDefault; //是否為默認地址 Y-是  N-否

    public UserAddress(){

    }
}

創建一個provider來實現UserService的Interface:

實現方法中,根據 id 返回對應的用戶地址信息即可:

package com.qinzhen.testmall.bootuserserviceprovider.service.impl;

import com.alibaba.dubbo.config.annotation.Service;
import com.qinzhen.testmall.bean.UserAddress;
import com.qinzhen.testmall.service.UserService;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

@Component
@Service  //暴露服務
public class UserServiceImpl implements UserService {

    private UserAddress userAddress1 = new UserAddress(1, "杭州市西湖區XX公司", "1", "qz", "12345678", "Y");
    private UserAddress userAddress2 = new UserAddress(2, "杭州市西湖區花園", "2", "qz", "12345678", "N");

    @Override
    public List<UserAddress> getUserAddressList(String userId) {
        if (userId.equals("1")){
            return Collections.singletonList(userAddress1);
        }
        else if (userId.equals("2")){
            return Collections.singletonList(userAddress2);
        }
        else {
            return Arrays.asList(userAddress1, userAddress2);
        }
    }

    @Override
    public List<UserAddress> getUserAddressList(){
        return Arrays.asList(userAddress1, userAddress2);
    }
}

4.1 Java consumer 代碼

下面我們編寫consumer代碼,讓服務消費者去注冊中心訂閱服務提供者的服務地址,以RPC方式,獲取遠程服務代理,從而執行遠程方法,代碼也很簡單,如下:

代碼結構

實現場景就是實現OrderService中的initOrder()方法,初始化訂單,初始化中直接調用userService的getUserAddressLis(java.lang.String)方法,具體代碼如下:

package com.qinzhen.testmall.service.impl;

import com.qinzhen.testmall.bean.UserAddress;
import com.qinzhen.testmall.service.OrderService;
import com.qinzhen.testmall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

/**
 * 1、講服務提供者注冊到注冊中心(暴露服務)
 *          1)導入dubbo依賴:操作zookeeper的客戶端(curator)
 * 2、讓服務消費者去注冊中心訂閱服務提供者的服務地址
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    UserService userService;

    public List<UserAddress> initOrder(String userId) {
        //1.查詢用戶的收貨地址
        System.out.println("用戶ID為:" + userId);
        List<UserAddress> userAddressList = userService.getUserAddressList(userId);
        return userAddressList;
    }
}

consumer MainApplication

package com.qinzhen.testmall;

import com.qinzhen.testmall.bean.UserAddress;
import com.qinzhen.testmall.service.OrderService;
import com.qinzhen.testmall.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.List;


/**
 * 1、將服務提供者注冊到注冊中心(暴露服務)
 *          1)導入dubbo依賴:操作zookeeper的客戶端(curator)
 * 2、讓服務消費者去注冊中心訂閱服務提供者的服務地址
 */
@Service
public class MainApplication {
    public static void main(String[] args) throws IOException {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"consumer.xml"});
        context.start();
        OrderService orderService = context.getBean(OrderService.class); // 獲取遠程服務代理
        List<UserAddress> userAddresses = orderService.initOrder("3");// 執行遠程方法
        System.out.println(userAddresses);
        System.out.println("調用完成。。。");
        System.in.read();
    }
}

consumer.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.qinzhen.testmall.service.impl"></context:component-scan>

    <dubbo:application name="order-service-comsumer"></dubbo:application>

    <dubbo:registry address="zookeeper://127.0.0.1:2181"></dubbo:registry>

    <!--聲明需要遠程調用遠程服務的接口,生成遠程服務代理-->
    <dubbo:reference interface="com.qinzhen.testmall.service.UserService" id="userService"></dubbo:reference>

</beans>

實例演示
首先確保provider已啟動:

運行consumer,可以看到成功調用到dubbo方法,獲取地址列表信息:

4.2 telnet+invoke

我們使用 telnet 命令可以直接訪問對應的服務,但是前提是你需要知道服務對應的ip+端口。

如下配置文件,我們可以知道服務暴露在本地的20880端口

dubbo.application.name=boot-user-service-provider
dubbo.registry.address=127.0.0.1:2181
dubbo.registry.protocol=zookeeper
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880

使用 telnet 命令進行訪問,如下出現 Dubbo 字樣時說明連接成功:

% telnet localhost 20880
Trying ::1...
Connected to localhost.
Escape character is '^]'.

dubbo>

Dubbo 內建的 telnet 命令的說明和用法如下

  • ls
    • ls: 顯示服務列表
    • ls -l: 顯示服務詳細信息列表
    • ls XxxService: 顯示服務的方法列表
    • ls -l XxxService: 顯示服務的方法詳細信息列表
dubbo>ls
com.qinzhen.testmall.service.UserService

dubbo>ls -l
com.qinzhen.testmall.service.UserService -> dubbo://192.168.2.xxx:20880/com.qinzhen.testmall.service.UserService?anyhost=true&application=boot-user-service-provider&bind.ip=192.168.2.xxx&bind.port=20880&dubbo=2.6.2&generic=false&interface=com.qinzhen.testmall.service.UserService&methods=getUserAddressList&pid=55472&qos.enable=false&side=provider&timestamp=1615088321885

dubbo>dubbo>ls com.qinzhen.testmall.service.UserService
getUserAddressList
getUserAddressList

dubbo>dubbo>ls -l com.qinzhen.testmall.service.UserService
java.util.List getUserAddressList(java.lang.String)
java.util.List getUserAddressList()
  • invoke
    • invoke XxxService.xxxMethod(1234, "abcd", {"prop" : "value"}): 調用服務的方法
    • invoke com.xxx.XxxService.XxxService.xxxMethod(1234, "abcd", {"prop" : "value"}): 調用全路徑服務的方法
    • invoke xxxMethod(1234, "abcd", {"prop" : "value"}): 調用服務的方法(自動查找包含此方法的服務)
    • invoke xxxMethod({"name":"zhangsan","age":12,"class":"org.apache.dubbo.qos.legacy.service.Person"}) :當有參數重載,或者類型轉換失敗的時候,可以通過增加class屬性指定需要轉換類
    • 當參數為Map<Integer,T>,key的類型為Integer時,建議指定類型。例如invoke com.xxx.xxxApiService({"3":0.123, "class":"java.util.HashMap"})

然后我們使用invoke 命令對dubbo方法getUserAddressList()進行調用,如下:

dubbo>invoke getUserAddressList()
[{"consignee":"qz","id":1,"isDefault":"Y","phoneNum":"12345678","userAddress":"杭州市西湖區xx公司","userId":"1"},{"consignee":"qz","id":2,"isDefault":"N","phoneNum":"12345678","userAddress":"杭州市西湖區xx花園","userId":"2"}]

dubbo>invoke getUserAddressList("1")
[{"consignee":"qz","id":1,"isDefault":"Y","phoneNum":"12345678","userAddress":"杭州市西湖區xx公司","userId":"1"}]
elapsed: 14 ms.

學習鏈接
其他 Telnet 命令相關操作,需要可參考 Dubbo 官網:

4.3 JMeter

對於 JMeter 測試 Dubbo 接口的方法,可參考往期文章:
《基於 Jmeter 完成 Dubbo 接口的測試》

4.4 Dubbo-admin

對於 Dubbo-admin 的安裝調試,可參考文章:
《dubbo-admin+zookeeper 的環境搭建實操與 Could not extract archive 報錯踩坑》

4.5 泛化調用

測試 Dubbo 服務的時候,我們需要服務端的同學給我們提供 API,沒有這個 API 我們是測不了的,而為了解決這個問題,Dubbo 官方又給我們提供了另外一個方法,就是泛化調用,來看看官方的解釋:

泛化調用的使用

Dubbo 給我們提供了一個接口GenericService,這個接口只有一個方法,就是$invoke,它接受三個參數,分別為方法名、方法參數類型數組和參數值數組;
下面我們直接上代碼演示:

import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ReferenceConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.rpc.service.GenericService;
import org.junit.jupiter.api.Test;

public class TestDemo {

    @Test
    void testDubboGenericService(){
        // 引用遠程服務
        // 該實例很重量,里面封裝了所有與注冊中心及服務提供方連接,請緩存
        ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
        // 弱類型接口名
        reference.setApplication(new ApplicationConfig("order-service-consumer"));
        reference.setInterface("com.qinzhen.testmall.service.UserService");
        reference.setRegistry(new RegistryConfig("zookeeper://127.0.0.1:2181"));

        // 聲明為泛化接口
        reference.setGeneric(true);

        // 用org.apache.dubbo.rpc.service.GenericService可以替代所有接口引用
        GenericService genericService = reference.get();

        Object result = genericService.$invoke("getUserAddressList", new String[] {"java.lang.String"}, new Object[] {"2"});
        System.out.println(result);

    }
}

運行后我們來看看結果,咦~也成功訪問了:

泛化調用的原理
我們通過 debug 跟入 Dubbo 的源碼中,可以得到如下的調用鏈:

服務消費端:
在這里插入圖片描述

服務提供端:

從上面的調用鏈可以知道完成一次泛化調用,Dubbo 框架經歷了很多過濾器,我們分別選取兩端鏈路中的最后一步的 Filter 來簡單了解一下泛化調用做了哪些事.

簡化后的調用關系就如下:

先來看consumer端的GenericImplFilter,大概看下核心的處理步驟:

// 判斷是否為泛化調用
if (invocation.getMethodName().equals(Constants.$INVOKE)
                && invocation.getArguments() != null
                && invocation.getArguments().length == 3
                && ProtocolUtils.isGeneric(generic)) {
   // 獲取泛化調用參數
            Object[] args = (Object[]) invocation.getArguments()[2];
            // 判斷是否為nativejava方式
            if (ProtocolUtils.isJavaGenericSerialization(generic)) {

                for (Object arg : args) {
                    if (!(byte[].class == arg.getClass())) {
                        error(byte[].class.getName(), arg.getClass().getName());
                    }
                }
            // 判斷是否為bean方式
            } else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
                for (Object arg : args) {
                    if (!(arg instanceof JavaBeanDescriptor)) {
                        error(JavaBeanDescriptor.class.getName(), arg.getClass().getName());
                    }
                }
            }
   // 設置為泛化調用方式
            ((RpcInvocation) invocation).setAttachment(
                    Constants.GENERIC_KEY, invoker.getUrl().getParameter(Constants.GENERIC_KEY));
        }
        // 發起遠程調用
        return invoker.invoke(invocation);

再來看provider端的GenericFilter,大概的核心處理步驟如下:

package com.alibaba.dubbo.rpc.filter;

import ...

/**
 * GenericInvokerFilter.
 */
@Activate(group = Constants.PROVIDER, order = -20000)
public class GenericFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
     // 判斷是否為泛化請求
        if (inv.getMethodName().equals(Constants.$INVOKE)
                && inv.getArguments() != null
                && inv.getArguments().length == 3
                && !ProtocolUtils.isGeneric(invoker.getUrl().getParameter(Constants.GENERIC_KEY))) {
            // 獲取參數名稱、參數類型、參數值
            String name = ((String) inv.getArguments()[0]).trim();
            String[] types = (String[]) inv.getArguments()[1];
            Object[] args = (Object[]) inv.getArguments()[2];
            try {
             // 使用反射獲取調用方法
                Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
                Class<?>[] params = method.getParameterTypes();
                if (args == null) {
                    args = new Object[params.length];
                }
                // 獲取泛化引用方式使用的泛化類型
                String generic = inv.getAttachment(Constants.GENERIC_KEY);
                // 泛化類型為空的話就使用generic=true的方式
                if (StringUtils.isEmpty(generic)
                        || ProtocolUtils.isDefaultGenericSerialization(generic)) {
                    args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
                // 判斷是否為generic=nativejava方式
                } else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
                    for (int i = 0; i < args.length; i++) {
                        if (byte[].class == args[i].getClass()) {
                            try {
                                UnsafeByteArrayInputStream is = new UnsafeByteArrayInputStream((byte[]) args[i]);
                                args[i] = ExtensionLoader.getExtensionLoader(Serialization.class)
                                        .getExtension(Constants.GENERIC_SERIALIZATION_NATIVE_JAVA)
                                        .deserialize(null, is).readObject();
                            } catch (Exception e) {
                                。。。
                            }
                        } else {
                            。。。
                        }
                    }
                  // 判斷是否為generic=bean方式
                } else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
                    for (int i = 0; i < args.length; i++) {
                        if (args[i] instanceof JavaBeanDescriptor) {
                            args[i] = JavaBeanSerializeUtil.deserialize((JavaBeanDescriptor) args[i]);
                        } else {
                            。。。
                        }
                    }
                }
                // 傳遞請求,執行服務
                Result result = invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));
                。。。
}

上面的代碼很多,着重來提取一小段看一下:

Method method = ReflectUtils.findMethodByMethodSignature(invoker.getInterface(), name, types);
Class<?>[] params = method.getParameterTypes();

從上面的代碼中我們便可以得知原來泛化調用中使用了Java的反射技術來獲取對應的方法信息完成調用的

4.6 用 Python 來測試 Dubbo

我們知道 Dubbo 是個 Java 項目,測試 Dubbo 就是模擬消費者去調用 Dubbo 的 Java 方法,那顯而易見,用 Python 是肯定沒法去直接調用Java的,但是在日常的工作中,很多小伙伴可能是 Pyhton技術棧的,或者因為一些測試條件限制亦或歷史原因,必須要將 Dubbo 測試用 Python 實現以滿足各種接口測試的一個組合。

1. python-hessian庫

Dubbo是支持hessian+http協議調用的,hessian是一種二進制序列化的方式。
了解到可以通過這種方式實現,具體沒有嘗試過,還需要開發在項目中將序列化的方式改為hessian,並且需要知道URL,有興趣的小伙伴可以去了解一下。

2. telnetlib庫

telnetlib是Python3自帶的一個庫,可以調用telnet命令,其實也就相當於上面說到的使用telnet方式訪問dubbo的方法

3. 開發dubbo測試服務

我們可以使用 Java 來開發一個 Dubbo 測試的 Web 服務,實現上就可以使用 Dubbo 的泛化調用,然后我們再用 HTTP 訪問的形式去訪問這個服務,將我們的測試參數信息傳過去,剩下的就交給 Java 去處理就好了。

這樣經過封裝設計后,可以實現 Python 端的使用者在訪問 Dubbo 時就像在測試HTTP接口一樣(例如 Python 的request庫);另外服務的 IP、端口、注冊中心等信息都不用出現在測試的工程中,只需要用環境標簽做區分,在服務端進行請求轉發即可,也保證了一定安全性。

大體上的思路流程如下:

以上,期待與大家多交流學習。

更多技術文章分享及測試資料點此獲取


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM