前言
大家好,今天开始给大家分享 — Dubbo 专题之 Dubbo Stub
和Mock
。在前一个章节中我们介绍了 Dubbo 事件通知,以及我们也例举了常见的使用场景并且进行了源码解析来分析其实现原理,同时知道 Dubbo 中的事件通知可以在某个服务调用之前、调用之后、异常发生时触发回调事件,我们可以通过回调事件做一些额外的工作。我们在 Dubbo 服务开发过程中可能遇到我们调用的服务方并没有编写完成,那我们是不是需要等待服务提供方开发完成我们才能开始测试呢?那么在本章节我们会通过介绍 Dubbo Stub
和 Mock
来解决这个问题。那么什么是 Stub
和 Mock
?下面就让我们快速开始吧!
1. 本地 Stub/Mock 简介
在 Dubbo 中提供的 Stub
也可称为本地存根具有类似代理模式的功能,即把我们调用的真正对象重新包装然后把包装对象提供给调用方,那么在调用真正的对象之前和之后我们可以做相应处理逻辑。同理 Mock
也可称为本地伪装和本地存根具有类似原理只是 Mock
针对发生RpcException
异常或者我们强制使用 Mock
方式才去调用 Mock
的实现。从下面我们可以看出来Stub
和 Mock
与代理对象关系:
从图中我们可以看出的 Stub
和 Mock
都是实现同一个服务的接口,它们都是通过代理对象来调用远程暴露的服务,而 Mock
紧紧是在调用失败时会触发。
Tips:
Mock
是Stub
的一个子集,便于服务提供方在客户端执行容错逻辑,因经常需要在出现RpcException
(比如网络失败,超时等)时进行容错,而在出现业务异常(比如登录用户名密码错误)时不需要容错,如果用Stub
,可能就需要捕获并依赖RpcException
类,而用Mock
就可以不依赖RpcException
,因为它的约定就是只有出现RpcException
时才执行。
2. 使用方式
2.1 Stub
配置方式
<dubbo:service interface="com.foo.BarService" stub="true" />
或者
<dubbo:service interface="com.foo.BarService" stub="com.foo.BarServiceStub" />
Stub
的实现类:
/**
* @author <a href="http://youngitman.tech">青年IT男</a>
* @version v1.0.0
* @className BookFacadeStub
* @description
* @JunitTest: {@link }
* @date 2020-11-15 23:43
**/
public class BookFacadeStub implements BookFacade {
//真正远程服务对象
private BookFacade bookFacade;
//必须提供BookFacade签名的构造函数
public BookFacadeStub(BookFacade bookFacade){
this.bookFacade = bookFacade;
}
@Override
public List<Book> queryAll() {
try {
//做一些前置处理
return bookFacade.queryAll();
} catch (Exception e) {
// 发生异常做一些处理
return Lists.newArrayList();
}finally {
//做一些后置处理
}
}
}
2.2 Mock
配置方式
<dubbo:reference interface="com.foo.BarService" mock="true" />
或者
<dubbo:reference interface="com.foo.BarService" mock="com.foo.BarServiceMock" />
Mock
的实现类:
public class BookFacadeMock implements BookFacade {
/**
*
* 这里我们可以把服务端的方法执行时间加大 使之超时就可以触发Mock的调用
*
* @author liyong
* @date 12:18 AM 2020/11/16
* @param
* @exception
* @return List<Book>
**/
@Override
public List<Book> queryAll() {
// 你可以伪造容错数据,此方法只在出现RpcException时被执行
Book book = new Book();
book.setDesc("default");
book.setName("default");
return Lists.newArrayList(book);
}
}
3. 使用场景
在前面的 Stub
/Mock
简介中我们可以知道他们都是基于对调用代理对象的包装,也就是调用代理对象前面我们可以做一些自定的操作。下面我们简单的介绍一些工作中的使用场景:
-
前置校验器:在我们工作中经常在调用操作前都会做一些校验,如果满足校验条件才能执行后面的逻辑。我们可以利用
Stub
的方式在调用代理对象前做一些校验工作。 -
日志打印:我们可以利用
Stub
在调用对象前面做一些通用的日志打印出来。 -
性能监控:利用
Stub
调用代理对象前后操作的时间来计算服务调用的时间。 -
服务降级:我们可以利用
Mock
针对出现RpcException
异常时触发Mock
调用机制来做一些服务降级处理工作,例如:当我们调用远程服务时远程服务不可用,这时候我们希望返回一个兜底的数据就可以使用Mock
的方式。
5. 示例演示
下面以获取图书列表服务为例进行示例演示。项目结构如下:
以下是消费者的配置文件dubbo-consumer-xml.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<dubbo:application name="demo-consumer" logger="log4j"/>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<!--开启Mock/Stub模式-->
<dubbo:reference id="bookFacade"
interface="com.muke.dubbocourse.mockstub.api.BookFacade" mock="true"></dubbo:reference>
</beans>
上面的 XML 配置中我们使用mock="true"
表示开启 Mock
模式。下面是服务提供者的 XML 配置dubbo-provider-xml.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<dubbo:application name="demo-provider" metadata-type="remote"/>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<bean id="bookFacade" class="com.muke.dubbocourse.mockstub.provider.BookFacadeImpl"/>
<!--暴露本地服务为Dubbo服务 ,当cluster="failover"指定集群容错模式-->
<dubbo:service interface="com.muke.dubbocourse.mockstub.api.BookFacade" ref="bookFacade" stub="true"/>
</beans>
上面的服务提供者配置stub="true"
表示开启本地存根模式。下面我们分别配置 Stub
和 Mock
的实现类:
public class BookFacadeMock implements BookFacade {
/**
*
* 这里我们可以把服务端的方法执行时间加大 使之超时就可以触发Mock的调用
*
* @author liyong
* @date 12:18 AM 2020/11/16
* @param
* @exception
* @return List<Book>
**/
@Override
public List<Book> queryAll() {
// 你可以伪造容错数据,此方法只在出现RpcException时被执行
Book book = new Book();
book.setDesc("default");
book.setName("default");
return Lists.newArrayList(book);
}
}
这里的 BookFacadeMock
同样实现 BookFacade
接口,当调用 BookFacade
远程服务发生RpcException
异常时就会调用 BookFacadeMock
。
public class BookFacadeStub implements BookFacade {
private BookFacade bookFacade;
public BookFacadeStub(BookFacade bookFacade){
this.bookFacade = bookFacade;
}
@Override
public List<Book> queryAll() {
try {
//做一些前置处理
System.out.println("调用方法queryAll前置处理");
return bookFacade.queryAll();
} catch (Exception e) {
// 发生异常做一些处理
return Lists.newArrayList();
}finally {
//做一些后置处理
System.out.println("调用方法queryAll后置处理");
}
}
}
上面的BookFacadeStub
实现了接口 BookFacade
,同时提供参数类型为BookFacade
的构造函数。当我们对接口配置了stub="true"
我们获取的对象就是BookFacadeStub
的实例。
5. 实现原理
下面我们主要分析Stub
的实现其Mock
的实现也是类似。 前面的简介中我们介绍了关于Stub
和Mock
都是消费端对代理对象的包装,所有我们可以大胆猜测对Stub
和Mock
的包装过程就在ReferenceConfig
类中。首先我们看到org.apache.dubbo.config.ReferenceConfig#init
方法核心代码如下:
/**
*
* 远程代理对象引用
*
* @author liyong
* @date 5:56 PM 2020/8/26
* @param
* @exception
* @return void
**/
public synchronized void init() {
//...
//本地存根check
checkStubAndLocal(interfaceClass);
//mock检测
ConfigValidationUtils.checkMock(interfaceClass, this);
//...
//创建远程代理对象
ref = createProxy(map);
//...
}
我们这里主要讨论Stub
看org.apache.dubbo.config. AbstractInterfaceConfig#checkStubAndLocal
核心内容如下:
/**
* 本地存根检测
*/
public void checkStubAndLocal(Class<?> interfaceClass) {
if (ConfigUtils.isNotEmpty(local)) {
//加载以接口名称+Local的Class
Class<?> localClass = ConfigUtils.isDefault(local) ?
ReflectUtils.forName(interfaceClass.getName() + "Local") : ReflectUtils.forName(local);
//校验该Class是否能够加载
verify(interfaceClass, localClass);
}
if (ConfigUtils.isNotEmpty(stub)) {
//加载以接口名称+Local的Class
Class<?> localClass = ConfigUtils.isDefault(stub) ?
ReflectUtils.forName(interfaceClass.getName() + "Stub") : ReflectUtils.forName(stub);
//校验该Class是否能够加载
verify(interfaceClass, localClass);
}
}
上面的代码加载XxxLocal
或XxxStub
的 Class
,校验方法org.apache.dubbo.config. AbstractInterfaceConfig#verify
核心内容如下:
private void verify(Class<?> interfaceClass, Class<?> localClass) {
//判断该localClass是否为interfaceClass的子类型
if (!interfaceClass.isAssignableFrom(localClass)) {
throw new IllegalStateException("The local implementation class " + localClass.getName() +
" not implement interface " + interfaceClass.getName());
}
try {
//判断localClass构造函数是否以interfaceClass类型的参数签名
ReflectUtils.findConstructor(localClass, interfaceClass);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("No such constructor \"public " + localClass.getSimpleName() +
"(" + interfaceClass.getName() + ")\" in local implementation class " + localClass.getName());
}
}
上面的方法主要是校验localClass
(例如:XxxLocal
或XxxStub
)是否实现interfaceClass
接口并且以interfaceClass
参数签名的构造函数。接下来创建代理对象调用方法org.apache.dubbo.config.ReferenceConfig #createProxy
最终会调用到org.apache.dubbo.rpc.proxy.wrapper .StubProxyFactoryWrapper #getProxy(org.apache.dubbo.rpc.Invoker<T>)
方法:
public <T> T getProxy(Invoker<T> invoker) throws RpcException {
T proxy = proxyFactory.getProxy(invoker);
if (GenericService.class != invoker.getInterface()) {
URL url = invoker.getUrl();
//获取本地存根配置
String stub = url.getParameter(STUB_KEY, url.getParameter(LOCAL_KEY));
if (ConfigUtils.isNotEmpty(stub)) {
Class<?> serviceType = invoker.getInterface();
if (ConfigUtils.isDefault(stub)) {
//尝试获取stub如果未查找到默认使用XxxLocal
if (url.hasParameter(STUB_KEY)) {
stub = serviceType.getName() + "Stub";
} else {
stub = serviceType.getName() + "Local";
}
}
try {
//加载XxxStub或XxxLocal
Class<?> stubClass = ReflectUtils.forName(stub);
if (!serviceType.isAssignableFrom(stubClass)) {
throw new IllegalStateException("The stub implementation class " + stubClass.getName() + " not implement interface " + serviceType.getName());
}
try {
//获取参数签名为接口类型的构造函数
Constructor<?> constructor = ReflectUtils.findConstructor(stubClass, serviceType);
//包装远程代理对象,在调用前先执行本地逻辑(*Stub、*Local)
proxy = (T) constructor.newInstance(new Object[]{proxy});
//暴露包装后的对象
URLBuilder urlBuilder = URLBuilder.from(url);
if (url.getParameter(STUB_EVENT_KEY, DEFAULT_STUB_EVENT)) {
//..
try {
export(proxy, (Class) invoker.getInterface(), urlBuilder.build());
} catch (Exception e) {
LOGGER.error("export a stub service error.", e);
}
}
} catch (NoSuchMethodException e) {
//..
}
} catch (Throwable t) {
//..
}
}
}
return proxy;
}
从上面的代码中可以看出远程调用代理对象传递给XxxStub
或XxxLocal
形式的实例构造函数,并且把XxxStub
或XxxLocal
返回给ReferenceConfig.ref
,这样我们使用 Dubbo 服务是获取的就是XxxStub
或XxxLocal
的实例对象。
6. 小结
在本小节中我们主要学习了 Dubbo 中 Stub
和 Mock
以及两种模式的使用方式。同时也分析了 Stub
和 Mock
实现的原理,其本质上是通过包装对引用代理对象的调用,其中 Mock
是 Stub
实现的一个子集针对调用抛出RpcException
异常时被调用。
本节课程的重点如下:
-
理解 Dubbo
Stub
和Mock
-
了解了两种模式使用方式
-
了解
Stub
和Mock
实现原理 -
了解
Stub
和Mock
使用场景
作者
个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。