Dubbo源码解析——mock

Dubbo源码解析——mock

使用

Dubbo提供了mock和stub两种方式进行服务的优雅的动态降级,临时屏蔽一些非关键服务,并定义返回策略。官方文档上给出了我们使用方式:

1
2
3
RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null"));

其中:

  • 使用mock=force:return+null表示消费方对该服务的方法调用都直接返回null值,不发起远程调用。
  • 还可以改为mock=fail:return+null表示消费方对该服务的方法调用在失败后,再返回null值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。
  • 其中Ip地址配置为0.0.0.0表示对所有IP地址生效,如果只想覆盖某个IP的数据,请填入具体IP。
  • dynamic=false表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心。
  • application=foo表示只对指定应用生效,可不填,表示对所有应用生效。
  • category=configurators表示该数据为动态配置类型。
  • override://表示数据采用覆盖方式,支持overrideabsent(缺省:如果原先存在该属性的配置,则以原先配置的属性值优先;如果原先没有配置该属性,则添加新的配置属性)。

以上的一些具体信息,可以看下dubbo的官方手册的配置规则服务降级两个小节。

代码分析

来看下dubbo的实现,这里会稍微复习一下Dubbo的wrapper class机制,顺便小提一下Dubbo的SPI。

我们先看看我们的mock在哪里生效,我们看下MockClusterInvoker这个类,至于为什么是在这个类中生效的,我们一会说,先看下这个类的invoker方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Result result = null;
// 获取mock的值,用我们上面的例子来看,这里是force:return+null
String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim();
if (value.length() == 0 || value.equalsIgnoreCase("false")) {
// 不进行mock操作
result = this.invoker.invoke(invocation);
} else if (value.startsWith("force")) {
// 强制mock,不进行远程调用
result = doMockInvoke(invocation, null);
} else {
// 失败时返回mock的结果
try {
result = this.invoker.invoke(invocation);
} catch (RpcException e) {
if (e.isBiz()) {
throw e;
} else {
result = doMockInvoke(invocation, e);
}
}
}
return result;

MockClusterInvoker跟所有的ClusterInvoker一样,会维护一个Directory,关于Directory这里不细说,可以翻看我的历史文章。

这里先从URL里获取键mock的值,然后进行区分是进行强制mock(不发起远程调用,直接返回)还是失败mock(调用失败则返回mock的值)。

我们看下,如果使用force mock的情况,fail mock非常类似就不讲了,看下force mocke时,调用的doMockInvoke方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Result result = null;
Invoker<T> minvoker;
// 查找mock invoker,我们这种情况下是没有的
List<Invoker<T>> mockInvokers = selectMockInvoker(invocation);
if (mockInvokers == null || mockInvokers.isEmpty()) {
// 创建一个mock invoker
minvoker = (Invoker<T>) new MockInvoker(directory.getUrl());
} else {
minvoker = mockInvokers.get(0);
}
try {
// 执行mock invoker 的invoke
result = minvoker.invoke(invocation);
} catch (RpcException me) {
if (me.isBiz()) {
result = new RpcResult(me.getCause());
} else {
throw new RpcException(me.getCode(), getMockExceptionMessage(e, me), me.getCause());
}
} catch (Throwable me) {
throw new RpcException(getMockExceptionMessage(e, me), me.getCause());
}
return result;

我们用覆盖配置的方式,selectMockInvoker方法是找不到mock invoker的,只有使用这种方式时,才可以直接找到自定义的MockInvoker

1
<dubbo:reference id="demoService" mock="org.apache.dubbo.demo.DemoServiceMock" check="false" interface="org.apache.dubbo.demo.DemoService"/>

注意mock里的值必须以XXXMock为名,不能乱起。我们看下覆盖配置的时候会发生什么:

1
2
3
4
if (mockInvokers == null || mockInvokers.isEmpty()) {
// 创建一个mock invoker
minvoker = (Invoker<T>) new MockInvoker(directory.getUrl());
}

直接用url创建一个MockInvoker,需要注意的是,这里的Url是已经覆盖过的Url(因为我们向注册中心注册了一个override的url,为什么这里的URL变化了,可以看我以前Directory的文章),我们写入了一个override://的覆盖配置,生效之后,我们的Url应该是类似:

1
...&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&mock=force:return+null&...

注意这里的mock已经被我们的配置给覆盖掉了。

继续看到MockInvoker#invoke

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 方法上的mock,这里应该是空的
String mock = getUrl().getParameter(invocation.getMethodName() + "." + Constants.MOCK_KEY);
if (invocation instanceof RpcInvocation) {
((RpcInvocation) invocation).setInvoker(this);
}
if (StringUtils.isBlank(mock)) {
// 使用url上的mock,这里是force:return+null
mock = getUrl().getParameter(Constants.MOCK_KEY);
}
if (StringUtils.isBlank(mock)) {
throw new RpcException(new IllegalAccessException("mock can not be null. url :" + url));
}
// 解析mock,解析出来是 return null
mock = normallizeMock(URL.decode(mock));
if (Constants.RETURN_PREFIX.trim().equalsIgnoreCase(mock.trim())) {
RpcResult result = new RpcResult();
result.setValue(null);
return result;
} else if (mock.startsWith(Constants.RETURN_PREFIX)) {
mock = mock.substring(Constants.RETURN_PREFIX.length()).trim();
mock = mock.replace('`', '"');
try {
// 尝试构建一个return xxx后面的这个xxx的实例,我们的例子中,就是构建一个null
Type[] returnTypes = RpcUtils.getReturnTypes(invocation);
Object value = parseMockValue(mock, returnTypes);
// 把null作为value 放到result中返回
return new RpcResult(value);
} catch (Exception ew) {
throw new RpcException(...);
}
} else if (mock.startsWith(Constants.THROW_PREFIX)) {
// 如果mock不是返回而是throw异常
mock = mock.substring(Constants.THROW_PREFIX.length()).trim();
mock = mock.replace('`', '"');
if (StringUtils.isBlank(mock)) {
throw new RpcException("...");
} else {
// 如果是一个用户自定义异常
Throwable t = getThrowable(mock);
throw new RpcException(RpcException.BIZ_EXCEPTION, t);
}
} else {
try {
// 这种情况是我们之前说的,自定义一个XXXMock类的情况,这里会去加载这个类,然后进行mock
Invoker<T> invoker = getInvoker(mock);
return invoker.invoke(invocation);
} catch (Throwable t) {
throw new RpcException(...);
}
}

这里其实就几种情况:

  1. 如果只有一个return,一般是mock返回值为void的情况,直接返回一个Result。
  2. 如果是开头是return,比如return null,那么就尝试构建一个null并且返回。
  3. 如果是mock抛出异常,则尝试抛出指定的异常。
  4. 如果都不是,那么就是用户自定义了mock的类,就像之前说的在配置文件中声明mock="XXXMock",这种情况就尝试去加载XXXMock这个类,然后执行对应的方法进行mock。

MockClusterInvoker什么时候把ClusterInvoker包装的

我们可以debug一下整个过程,其实MockClusterInvoker是在具体的ClusterInvoker执行invoke之前就生效了的(要不然也没法做到不force mock,因为force mock是不发起远程请求的)。

我们可以理解为MockClusterInvoker里维护了一个ClusterInvoker

可能看过我以前的文章的同学已经明白是怎么生效的了,这里重新拿出来大体说一下。

我们在使用ExtensionLoader加载扩展点的时候,方法:

1
2
3
4
5
6
7
8
private boolean isWrapperClass(Class<?> clazz) {
try {
clazz.getConstructor(type);
return true;
} catch (NoSuchMethodException e) {
return false;
}
}

判断是否是一个WrapperClass,依据就是这个Class是否有一个以当前类型为入参的构造方法,比如MockClusterWrapper就是一个WrapperClass,它会包装Cluster的实现,因为他有个以Cluster为入参的构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MockClusterWrapper implements Cluster {

private Cluster cluster;
// 说明是Cluster的包装类
public MockClusterWrapper(Cluster cluster) {
this.cluster = cluster;
}

@Override
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new MockClusterInvoker<T>(directory,
this.cluster.join(directory));
}
}

这样我们执行join时实际产生的是MockClusterInvoker。而具体在什么时候进行包装的,这个是在ExtensionLoader#createExtension里:

1
2
3
4
5
6
7
8
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
// 对于Cluster来说,其中一个包装类就是MockClusterInvoker
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for (Class<?> wrapperClass : wrapperClasses) {
// 生产一个MockClusterInvoker实例,并且注入需要的属性
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}

这样我们就把ClusterInvoker包装好了,再通过其中的Directory动态控制URL,然后实现我们的动态mock以及降级。

后记

  1. 最近被选为Dubbo committer了,顺便加入了Apache基金会。我自己本人知道Dubbo,学习Dubbo,贡献代码,再到成为Committer,经历了大概两年多,我会写一个我的学习历程和新路历程,希望帮助更多的人加入到Dubbo里,也为希望学习Dubbo的同学指一条我自己走出来的路。
  2. 其实Dubbo还有一种方式实现优雅降级,不过不太常用,就是本地存根——Stub,这个比较复杂且场景比较少,可能会有空再去研究一下了。也就是说,本篇文章不是系列文章,而是单讲mock