Dubbo 接口测试原理及多种方法实践总结
1、什么是 DubboDubbo 最开始是应用于淘宝网由阿里巴巴开源的一款优秀的高性能服务框架由 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和UserServiceOrderServicepackage com.qinzhen.testmall.service; import com.qinzhen.testmall.bean.UserAddress; import java.util.List; public interface OrderService { /** * 初始化订单 * param userID */ public ListUserAddress initOrder(String userID); }UserServicepackage com.qinzhen.testmall.service; import com.qinzhen.testmall.bean.UserAddress; import java.util.List; /** * 用户服务 */ public interface UserService { /** * 按照userId返回所有的收获地址 * param userId * return */ public ListUserAddress getUserAddressList(String userId); /** * 返回所有的收获地址 * param * return */ public ListUserAddress 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 ListUserAddress 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 ListUserAddress 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 ListUserAddress initOrder(String userId) { //1.查询用户的收货地址 System.out.println(用户ID为 userId); ListUserAddress userAddressList userService.getUserAddressList(userId); return userAddressList; } }consumer MainApplicationpackage 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); // 获取远程服务代理 ListUserAddress userAddresses orderService.initOrder(3);// 执行远程方法 System.out.println(userAddresses); System.out.println(调用完成。。。); System.in.read(); } }consumer.xml:?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:dubbohttp://code.alibabatech.com/schema/dubbo xmlns:contexthttp://www.springframework.org/schema/context xsi:schemaLocationhttp://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-packagecom.qinzhen.testmall.service.impl/context:component-scan dubbo:application nameorder-service-comsumer/dubbo:application dubbo:registry addresszookeeper://127.0.0.1:2181/dubbo:registry !--声明需要远程调用远程服务的接口生成远程服务代理-- dubbo:reference interfacecom.qinzhen.testmall.service.UserService iduserService/dubbo:reference /beans实例演示首先确保provider已启动:运行consumer,可以看到成功调用到dubbo方法获取地址列表信息4.2 telnetinvoke我们使用 telnet 命令可以直接访问对应的服务但是前提是你需要知道服务对应的ip端口。如下配置文件我们可以知道服务暴露在本地的20880端口dubbo.application.nameboot-user-service-provider dubbo.registry.address127.0.0.1:2181 dubbo.registry.protocolzookeeper dubbo.protocol.namedubbo dubbo.protocol.port20880使用 telnet 命令进行访问如下出现 Dubbo 字样时说明连接成功% telnet localhost 20880 Trying ::1... Connected to localhost. Escape character is ^]. dubboDubbo 内建的 telnet 命令的说明和用法如下lsls: 显示服务列表ls -l: 显示服务详细信息列表ls XxxService: 显示服务的方法列表ls -l XxxService: 显示服务的方法详细信息列表dubbols com.qinzhen.testmall.service.UserService dubbols -l com.qinzhen.testmall.service.UserService - dubbo://192.168.2.xxx:20880/com.qinzhen.testmall.service.UserService?anyhosttrueapplicationboot-user-service-providerbind.ip192.168.2.xxxbind.port20880dubbo2.6.2genericfalseinterfacecom.qinzhen.testmall.service.UserServicemethodsgetUserAddressListpid55472qos.enablefalsesideprovidertimestamp1615088321885 dubbodubbols com.qinzhen.testmall.service.UserService getUserAddressList getUserAddressList dubbodubbols -l com.qinzhen.testmall.service.UserService java.util.List getUserAddressList(java.lang.String) java.util.List getUserAddressList()invokeinvoke 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属性指定需要转换类当参数为MapInteger,Tkey的类型为Integer时建议指定类型。例如invoke com.xxx.xxxApiService({3:0.123, class:java.util.HashMap})然后我们使用invoke命令对dubbo方法getUserAddressList()进行调用如下dubboinvoke 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}] dubboinvoke getUserAddressList(1) [{consignee:qz,id:1,isDefault:Y,phoneNum:12345678,userAddress:杭州市西湖区xx公司,userId:1}] elapsed: 14 ms.4.3 JMeter对于 JMeter 测试 Dubbo 接口的方法可参考往期文章4.4 Dubbo-admin对于 Dubbo-admin 的安装调试可参考文章《dubbo-adminzookeeper 的环境搭建实操与 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(){ // 引用远程服务 // 该实例很重量里面封装了所有与注册中心及服务提供方连接请缓存 ReferenceConfigGenericService reference new ReferenceConfigGenericService(); // 弱类型接口名 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); // 泛化类型为空的话就使用generictrue的方式 if (StringUtils.isEmpty(generic) || ProtocolUtils.isDefaultGenericSerialization(generic)) { args PojoUtils.realize(args, params, method.getGenericParameterTypes()); // 判断是否为genericnativejava方式 } 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 { 。。。 } } // 判断是否为genericbean方式 } 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是支持hessianhttp协议调用的hessian是一种二进制序列化的方式。了解到可以通过这种方式实现具体没有尝试过还需要开发在项目中将序列化的方式改为hessian并且需要知道URL有兴趣的小伙伴可以去了解一下。2. telnetlib库telnetlib是Python3自带的一个库可以调用telnet命令其实也就相当于上面说到的使用telnet方式访问dubbo的方法3. 开发dubbo测试服务我们可以使用 Java 来开发一个 Dubbo 测试的 Web 服务实现上就可以使用 Dubbo 的泛化调用然后我们再用 HTTP 访问的形式去访问这个服务将我们的测试参数信息传过去剩下的就交给 Java 去处理就好了。这样经过封装设计后可以实现 Python 端的使用者在访问 Dubbo 时就像在测试HTTP接口一样(例如 Python 的request库)另外服务的 IP、端口、注册中心等信息都不用出现在测试的工程中只需要用环境标签做区分在服务端进行请求转发即可也保证了一定安全性。大体上的思路流程如下最后下方这份完整的软件测试 视频教程已经整理上传完成需要的朋友们可以自行领取【保证100%免费】软件测试面试文档我们学习必然是为了找到高薪的工作下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料并且有字节大佬给出了权威的解答刷完这一套面试资料相信大家都能找到满意的工作。