1. MockServer需求
- 可界面配置RPC接口的请求和响应数据
- 调用方代码只需很少的改动就能使用RPC Mock
- 可平滑的切换API版本
- 预发和生产环境忽略Mock
2. 设计目标
- 代码极少侵入:调用方代码只需很少的改动(例如增加一个注解),就能使用Mock
- 调用隔离:Mock调用和真实调用隔离
- 环境隔离:预发和生产环境下忽略掉Mock
- 易用性:提供webUI配置
- API版本切换:支持依赖的API版本切换
3. 设计思路
- 代码极少侵入 -> 提供自定义注解声明要mock的服务
- 调用隔离 -> 方案对比
- 方案一 :在原有协议DubboProtocol上修改,即真实调用和Mock调用走同样的协议,占用同样的网络通道。
- 方案二 :扩展一个新的网络传输层(Transporter),Mock调用走这个传输通道,并且在底层就把Mock数据返回,无需经过上层的Invoker和Proxy。
- 方案三 :扩展协议(MockProtocol),扩展服务方(MockServiceConfig),扩展调用方(MockRefrenceConfig),扩展Invoker(MockInvoker),Proxy(MockProxyFactory),扩展注册协议(MockRegistry)。
- 环境隔离 -> 注解的生效和失效机制(Guice注解的binding,spring bean的EnableAutoConfiguration)
- API版本切换 -> classloader隔离机制
4. 实现
4.1 无米之炊?–当MockServer遇上ProtoBuf
protobuf的请求响应类都是根据事先定义好的.proto
描述文件生成的,而MockServer没有这些描述文件,也就没有那些生成的请求类响应类。收到的请求数据也就不知道怎么解码,如何解决?
- 用户上传.proto文件? – 缺点:
.proto
文件不开放,调用方要去问被调用方拿到这个文件,不方便,实施难。 - 用户填写Maven Artifact信息(GroupId,ArtifactId,Version)? – 优点:Maven Artifact是开放的,获取容易。
有了Maven Artifact之后,就可动态下载Maven jar,把里面的类加载到MockServer的上下文中。
- Eclipse aether 项目 (动态下载maven包)
- org.reflections.Reflections项目 (类扫描)
4.2 dubbo现有的方案能满足MockServer的需求吗?
dubbo(2.6.4版本)有一个支持MockServer的方案:泛化调用。
下面是dubbo文档中泛化调用的示例,可以看到需要显式调用**$invoke
** 方法,对代码有较大的侵入。所以不考虑泛化的方案。
GenericService barService = (GenericService) applicationContext.getBean("barService");
Object result = barService.$invoke("sayHello", new String[] { "java.lang.String" }, new Object[] { "World" });
4.3 利用Dubbo丰富的SPI扩展来实现Mock
下图来自dubbo官方文档,是暴露服务的时序图
从中可以看到export一个服务时,经过的关键类:
- ServiceConfig :服务配置类
- ProxyFactory :代理工厂类
- Invoker :这里是服务端的Invoker,由ProxyFactory提供
- Protocol :RPC 协议,封装远程调用细节
- Registry : 注册中心
这些关键类也是我们要扩展的类。扩展好后,通过ServiceConfig来指定对应的Protocol,Proxy,Registry。如下是ServiceConfig的配置代码
ServiceConfig<Object> serviceConfig = new MockServiceConfig<>(classLoader);
serviceConfig.setInterface(interfaceClass);
serviceConfig.setRef(ref);
serviceConfig.setVersion(version);
serviceConfig.setGroup(group);
serviceConfig.setProtocol(exportMockProtocolConfig());
serviceConfig.setRegistry(exportMockRegistryConfig());
serviceConfig.setApplication(exportApplicationConfig());
serviceConfig.setProxy(getConfigValue("dubbo.service.proxy"));
serviceConfig.setFilter(getConfigValue("dubbo.service.filter"));
return serviceConfig;
4.4 用ClassLoader隔离来实现API版本切换
-
ClassLoader隔离才能做到API版本切换
假设依赖的API包版本要升级,其中某些类有变化。MockServer需要在不重启的情况下重新加载这些类(也就是Class热替换)。如果一个Class被一个ClassLoader加载过,那么这个Class就不能再被同一个ClassLoader再次加载(指的是重新加载字节码、解析、验证)。要实现热替换,需要用一个新的classLoader去加载这个class。在我们这里,更新一个版本时MockServer会先下载一个新的maven jar包,再用一个新的URLClassLoader来加载jar包里的类。(这说明每个Maven Artifact (Maven 坐标)都有一个独立ClassLoader与之对应)。
-
原生Dubbo不支持使用指定的ClassLoader去加载类
由上分析,当API版本切换时,使用新的ClassLoader去加载类。但是dubbo在暴露服务时只会使用SystemClassLoader(
sun.misc.Launcher$AppClassLoader
)来加载类,不允许指定。如下为ServiceConfig类的doExport方法的代码段:try { interfaceClass = Class.forName(interfaceName, true, Thread.currentThread() .getContextClassLoader