Dubbo RPC MockServer设计与实现

本文介绍了Dubbo RPC MockServer的设计与实现,包括需求、设计目标、设计思路和具体实现方法。通过扩展Dubbo的SPI,实现Mock调用与真实调用的隔离,利用ClassLoader隔离实现API版本切换,以及提供注解和WEBUI提高易用性。

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一个服务时,经过的关键类:

  1. ServiceConfig :服务配置类
  2. ProxyFactory :代理工厂类
  3. Invoker :这里是服务端的Invoker,由ProxyFactory提供
  4. Protocol :RPC 协议,封装远程调用细节
  5. 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.GeneratedMessagecom.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. 参考资料

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值