本文為霍格沃茲測試學院優秀學員學習筆記,進階學習文末加群。
更多技術文章分享及測試資料點此獲取
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×tamp=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、端口、注冊中心等信息都不用出現在測試的工程中,只需要用環境標簽做區分,在服務端進行請求轉發即可,也保證了一定安全性。
大體上的思路流程如下:
以上,期待與大家多交流學習。