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()); } catch (ClassNotFoundException e) { throw new IllegalStateException(e.getMessage(), e); } checkInterfaceAndMethods(interfaceClass, methods); checkRef(); generic = Boolean.FALSE.toString();
从 Thread.currentThread().getContextClassLoader() (绝大部分情况就是SystemClassLoader)中加载服务接口类。
-
MockServiceConfig
为支持指定的ClassLoader来加载服务类,我的方案是创建MockServiceConfig类,它继承自ServiceConfig,行为与ServiceConfig无异,不同之处仅仅是加载类的时候使用指定的ClassLoader,如下:
try { if (this.classLoader != null) { interfaceClass = Class.forName(interfaceName, true, this.classLoader); } else { interfaceClass = Class.forName(interfaceName, true, Thread.currentThread().getContextClassLoader()); } } catch (ClassNotFoundException e) { throw new IllegalStateException(e.getMessage(), e); } checkInterfaceAndMethods(interfaceClass, methods); checkRef(); generic = Boolean.FALSE.toString();
4.5 易用性:为调用方提供注解
以上都是MockServer端的工作,server端准备好了,接下来就要给调用方提供方便易用的注解。
- 为Guice容器提供Mock注解
在我们的内部框架(DC ,AS),使用Google Guice 做IOC容器,一个业务service是通过ProxyBuilder类build出来的。如下:
@AutoBindSingleton
public class XXXServiceFactory implements Provider<XXXService> {
@Inject
private ProxyBuilder proxyBuilder;
public XXXService get() {
return proxyBuilder.buildProxy(XXXService.class);
}
}
现在我们需要做到的是,通过Mock注解,注入MockProxyBuilder,由它build出来的服务类,全是走mock通道的。像下面这样使用Mock注解:
@Inject
@Mock
private ProxyBuilder proxyBuilder;
public XXXService get() {
return proxyBuilder.buildProxy(XXXService.class);
}
被@Mock注解修饰的ProxyBuilder,实际上被注入的是ProxyBuilder的子类MockProxyBuilder:
public class MockAnnotationModule implements Module {
@Override
public void configure(Binder binder) {
binder.bind(ProxyBuilder.class).annotatedWith(Mock.class)
.toProvider(MockProxyBuilderProvider.class);
}
}
public class MockProxyBuilderProvider implements Provider<ProxyBuilder> {
@Override
public ProxyBuilder get() {
...
proxyBuilder = new MockProxyBuilder();
proxyBuilder.init(...);
return proxyBuilder;
}
}
至此,Mock注解的实现就完成了,补充一点,Guice的自定义注解需要被@BindingAnnotation修饰:
@BindingAnnotation
@Target({FIELD, PARAMETER, METHOD})
@Retention(RUNTIME)
public @interface Mock {}
- SpringBean EnableAutoConfiguration
对于Springboot web项目,要做的就是在框架层向Spring IOC容器注入一个MockProxyBuilder 的Been,如下:
@Configuration
public class MockProxyBuilderProviderInSpring {
@Bean(name = "mock")
@Scope(value = "singleton")
public ProxyBuilder proxyBuilder() {
...
}
}
那么还有一个问题:如何让Spring扫描到这个 MockProxyBuilderProviderInSpring类?我们不能要求每个项目调用方用scanpackage指定MockProxyBuilderProviderInSpring所在的package,这样代码侵入大。可以借助Spring的自动注入机制 EnableAutoConfiguration。在框架层增加一个文件:META-INF/spring.factories,里面添加一行
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.xxx.MockProxyBuilderProviderInSpring
4.6 易用性:WEBUI功能-protobuf类生成Json
开发者对JSON是比较熟悉的,所以在设计之初就考虑把protobuf请求和响应数据以JSON格式来配置。既然这样就要支持protobuf转JSON,如下所示:

Message类转Json其实分两步,第一步是先给GeneratedMessage或者GeneratedMessageLite填充字段值,第二步才是将Message转JSON。
用google protobuf官方生成器生成的类有两种:
com.google.protobuf.GeneratedMessage和com.google.protobuf.GeneratedMessageLite。
- 填充Message字段值
要填充字段值首先就要知道Message有哪些字段。GeneratedMessage好说,它有Reflection和Desriptor等API,可方便获取字段和给字段赋值。但是GeneratedMessageLite没有这样的API,因为Lite的存在就是用于那些节省传输流量,已明确知道Message有哪些字段的场景。那如何给GeneratedMessageLite填充字段值呢?你应该也想到了,就是用Java最原始的Reflection机制。如下是填充GeneratedMessageLite的示例代码:
public E generate(double p) throws Exception {
Builder builder = messageLiteInstance.newBuilderForType();
Field[] fields = builder.getClass().getDeclaredFields();
for (Field field : fields) {
if ("bitField0_".equals(field.getName()))
continue;
// setField 就是填充字段值的方法,里面也是用了Java 反射API来做
setField(builder, field, getRandomValue(builder, field, p));
}
return (E) builder.build();
}
- Message转JSON
GeneratedMessage转Json也有官方支持,google protobuf有个项目protobuf-java-format
就是做这个的。但是GeneratedMessageLite转Json就没有官方的支持了。我这里的方案也是使用Java 原始的Reflection API获取字段和值,再结合fastjson API 将值转为Map,示例代码如下:
public static String printToString(MessageLite message) {
try {
Map resultMap = _printToMap(message);
return JSONObject.toJSONString(resultMap);
} catch (Exception ex) {
logger.error(ex.getMessage(), ex);
return null;
}
}
private static Map _printToMap(MessageLite message) throws Exception {
MessageLite.Builder builder = message.newBuilderForType();
Map map = Maps.newHashMap();
Field[] fields = builder.getClass().getDeclaredFields();
for (Field field : fields) {
String fieldName = field.getName();
if ("bitField0_".equals(fieldName))
continue;
Class<?> fieldClass = field.getType();
if (java.util.List.class.isAssignableFrom(fieldClass)) {
ParameterizedType listType = (ParameterizedType) field.getGenericType();
Class<?> genericType = (Class<?>) listType.getActualTypeArguments()[0];
if (MessageLite.class.isAssignableFrom(genericType)) {
Method getterListMethod = message.getClass()
.getDeclaredMethod(MethodUtils.getterListMethodName(fieldName), null);
List<MessageLite> list =
(List<MessageLite>) getterListMethod.invoke(message, null);
if (list != null) {
JSONArray jsonArray = new JSONArray();
map.put(MethodUtils.normalFieldName(fieldName), jsonArray);
for (MessageLite _message : list) {
Map subMap = _printToMap(_message);
jsonArray.add(subMap);
}
}
}
} else {
Method getterMethod = message.getClass()
.getDeclaredMethod(MethodUtils.getterMethodName(fieldName), null);
Object value = getterMethod.invoke(message, null);
if (value instanceof MessageLite) {
Map subMap = _printToMap((MessageLite) value);
map.put(MethodUtils.normalFieldName(fieldName), subMap);
} else {
map.put(MethodUtils.normalFieldName(fieldName), value);
}
}
}
return map;
}
5. 参考资料
-
dubbo官方文档 http://dubbo.apache.org/zh-cn/docs/user/quick-start.html
-
一个高质量的JAVA API Example站点 https://www.programcreek.com/java-api-examples/index.php
本文介绍了Dubbo RPC MockServer的设计与实现,包括需求、设计目标、设计思路和具体实现方法。通过扩展Dubbo的SPI,实现Mock调用与真实调用的隔离,利用ClassLoader隔离实现API版本切换,以及提供注解和WEBUI提高易用性。

608

被折叠的 条评论
为什么被折叠?



