1、什么是Dubbo
最开始是应用于淘宝网,由阿里巴巴开源的一款优秀的高性能服务框架,由Java开发,后来贡献给了Apache组织
下面以官网的一个说明来了解一下架构的演变过程,从而了解dubbo的诞生原因:
- 单一应用架构
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。 - 垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,提升效率的方法之一是将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。 - 分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。 - 流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。
2、Dubbo架构简介
节点 | 角色说明 | 实例对应 |
Provider | 暴露服务的服务提供方 | 开发的代码 |
Consumer | 调用远程服务的服务消费方 | 调用方代码、Telnet、Jmeter |
Registry | 服务注册与发现的注册中心 | zookeeper |
Monitor | 统计服务的调用次数和调用时间的监控中心 | dubbo-monitor |
Container | 服务运行容器 | dubbo内置容器、Jetty |
Dubbo比较有特点的地方就是这个注册中心,平常我们测试较多的HTTP接口,直接请求接口,调用后端服务即可;而Dubbo是要先走注册中心获取服务的位置,下面来举个现实生活中的例子来说明
- 现实举例
好比大家平常约个同事朋友一起出去吃饭,吃什么呢?今天我们就吃个川菜吧,听说赠李白(没收广告费~)不错,于是就决定去赠李白吃饭。
然后我们需要去找这家饭店在哪,这个时候我们打开了我们的小蓝或小黄APP去查这家店的信息,找到了之后知道了具体的地址,在哪家商场,哪层的几号;
接着我们就根据地址出发,至于是走路,打车还是骑车,就随意了
最后,找到了店铺,开始消费,开始长胖~
而这里APP就相当于注册中心(Registry
),我们这群吃货就是消费者(Consumer
),商家,也就是赠李白属于生产者(Provider
)。商家把自己的信息注册在APP上,消费者根据APP查询到商家的信息,再根据信息找到商家进行消费。
2.1、zookeeper简介
之前经常有小伙伴问我zk是啥?干啥的?怎么干的?下面我们来简单了解一哈
ZK,全称就是zookeeper
,wikipedia上的解释是这样的: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.
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官方又给我们提供了另外一个方法,就是泛化调用,来看看官方的解释:
4.5.1 泛化调用的使用
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);
}
}
运行后我们来看看结果,咦~也成功访问了:
4.5.1 泛化调用的原理
我们通过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实现以满足各种接口测试的一个组合
4.6.1 python-hessian库
Dubbo
是支持hessian+http
协议调用的,hessian
是一种二进制序列化的方式。
了解到可以通过这种方式实现,具体没有尝试过,还需要开发在项目中将序列化的方式改为
hessian
,并且需要知道URL,有兴趣的小伙伴可以去了解一下
4.6.2 telnetlib库
telnetlib
是Python3自带的一个库,可以调用telnet
命令,其实也就相当于上面说到的使用telnet
方式访问dubbo
的方法
4.6.3 开发dubbo测试服务
我们可以使用Java来开发一个dubbo测试的web服务,实现上就可以使用dubbo的泛化调用,然后我们再用HTTP访问的形式去访问这个服务,将我们的测试参数信息传过去,剩下的就交给Java去处理就好了。
这样经过封装设计后,可以实现Python端的使用者在访问dubbo时就像在测试HTTP
接口一样(例如Python的request
库);另外服务的IP、端口、注册中心等信息都不用出现在测试的工程中,只需要用环境标签做区分,在服务端进行请求转发即可,也保证了一定安全性。
大体上的思路流程如下: