泛化调用(客户端泛化)

不需要服务端 API 的 RPC 调用

特性说明

泛化调用是指在调用方没有服务方提供的 API(SDK)的情况下,对服务方进行调用,并且可以正常拿到调用结果。

使用场景

泛化调用主要用于实现一个通用的远程服务 Mock 框架,可通过实现 GenericService 接口处理所有服务请求。比如如下场景:

  1. 网关服务:如果要搭建一个网关服务,那么服务网关要作为所有 RPC 服务的调用端。但是网关本身不应该依赖于服务提供方的接口 API(这样会导致每有一个新的服务发布,就需要修改网关的代码以及重新部署),所以需要泛化调用的支持。

  2. 测试平台:如果要搭建一个可以测试 RPC 调用的平台,用户输入分组名、接口、方法名等信息,就可以测试对应的 RPC 服务。那么由于同样的原因(即会导致每有一个新的服务发布,就需要修改网关的代码以及重新部署),所以平台本身不应该依赖于服务提供方的接口 API。所以需要泛化调用的支持。

使用方式

demo 可见 dubbo 项目中的示例代码

API 部分以此 demo 为例讲解使用方式。

服务定义

服务接口

  1. public interface HelloService {
  2. String sayHello(String name);
  3. CompletableFuture<String> sayHelloAsync(String name);
  4. CompletableFuture<Person> sayHelloAsyncComplex(String name);
  5. CompletableFuture<GenericType<Person>> sayHelloAsyncGenericComplex(String name);
  6. }

服务实现类

  1. public class HelloServiceImpl implements HelloService {
  2. @Override
  3. public String sayHello(String name) {
  4. return "sayHello: " + name;
  5. }
  6. @Override
  7. public CompletableFuture<String> sayHelloAsync(String name) {
  8. CompletableFuture<String> future = new CompletableFuture<>();
  9. new Thread(() -> {
  10. try {
  11. Thread.sleep(5000);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. future.complete("sayHelloAsync: " + name);
  16. }).start();
  17. return future;
  18. }
  19. @Override
  20. public CompletableFuture<Person> sayHelloAsyncComplex(String name) {
  21. Person person = new Person(1, "sayHelloAsyncComplex: " + name);
  22. CompletableFuture<Person> future = new CompletableFuture<>();
  23. new Thread(() -> {
  24. try {
  25. Thread.sleep(5000);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. future.complete(person);
  30. }).start();
  31. return future;
  32. }
  33. @Override
  34. public CompletableFuture<GenericType<Person>> sayHelloAsyncGenericComplex(String name) {
  35. Person person = new Person(1, "sayHelloAsyncGenericComplex: " + name);
  36. GenericType<Person> genericType = new GenericType<>(person);
  37. CompletableFuture<GenericType<Person>> future = new CompletableFuture<>();
  38. new Thread(() -> {
  39. try {
  40. Thread.sleep(5000);
  41. } catch (InterruptedException e) {
  42. e.printStackTrace();
  43. }
  44. future.complete(genericType);
  45. }).start();
  46. return future;
  47. }
  48. }

通过API使用泛化调用

服务启动方

  1. 在设置 ServiceConfig 时,使用setGeneric("true")来开启泛化调用

  2. 在设置 ServiceConfig 时,使用 setRef 指定实现类时,要设置一个 GenericService 的对象。而不是真正的服务实现类对象

  3. 其他设置与正常 Api 服务启动一致即可

  1. private static String zookeeperAddress = "zookeeper://" + System.getProperty("zookeeper.address", "127.0.0.1") + ":2181";
  2. public static void main(String[] args) throws Exception {
  3. new EmbeddedZooKeeper(2181, false).start();
  4. //创建ApplicationConfig
  5. ApplicationConfig applicationConfig = new ApplicationConfig();
  6. applicationConfig.setName("generic-impl-provider");
  7. //创建注册中心配置
  8. RegistryConfig registryConfig = new RegistryConfig();
  9. registryConfig.setAddress(zookeeperAddress);
  10. //新建服务实现类,注意要使用GenericService接收
  11. GenericService helloService = new GenericImplOfHelloService();
  12. //创建服务相关配置
  13. ServiceConfig<GenericService> service = new ServiceConfig<>();
  14. service.setApplication(applicationConfig);
  15. service.setRegistry(registryConfig);
  16. service.setInterface("org.apache.dubbo.samples.generic.call.api.HelloService");
  17. service.setRef(helloService);
  18. //重点:设置为泛化调用
  19. //注:不再推荐使用参数为布尔值的setGeneric函数
  20. //应该使用referenceConfig.setGeneric("true")代替
  21. service.setGeneric("true");
  22. service.export();
  23. System.out.println("dubbo service started");
  24. new CountDownLatch(1).await();
  25. }
  26. }

泛化调用方

步骤:

  1. 在设置 ReferenceConfig 时,使用 setGeneric("true") 来开启泛化调用

  2. 配置完 ReferenceConfig 后,使用 referenceConfig.get() 获取到 GenericService 类的实例

  3. 使用其 $invoke 方法获取结果

  4. 其他设置与正常 Api 服务启动一致即可

  1. //定义泛化调用服务类
  2. private static GenericService genericService;
  3. public static void main(String[] args) throws Exception {
  4. //创建ApplicationConfig
  5. ApplicationConfig applicationConfig = new ApplicationConfig();
  6. applicationConfig.setName("generic-call-consumer");
  7. //创建注册中心配置
  8. RegistryConfig registryConfig = new RegistryConfig();
  9. registryConfig.setAddress("zookeeper://127.0.0.1:2181");
  10. //创建服务引用配置
  11. ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
  12. //设置接口
  13. referenceConfig.setInterface("org.apache.dubbo.samples.generic.call.api.HelloService");
  14. applicationConfig.setRegistry(registryConfig);
  15. referenceConfig.setApplication(applicationConfig);
  16. //重点:设置为泛化调用
  17. //注:不再推荐使用参数为布尔值的setGeneric函数
  18. //应该使用referenceConfig.setGeneric("true")代替
  19. referenceConfig.setGeneric(true);
  20. //设置异步,不必须,根据业务而定。
  21. referenceConfig.setAsync(true);
  22. //设置超时时间
  23. referenceConfig.setTimeout(7000);
  24. //获取服务,由于是泛化调用,所以获取的一定是GenericService类型
  25. genericService = referenceConfig.get();
  26. //使用GenericService类对象的$invoke方法可以代替原方法使用
  27. //第一个参数是需要调用的方法名
  28. //第二个参数是需要调用的方法的参数类型数组,为String数组,里面存入参数的全类名。
  29. //第三个参数是需要调用的方法的参数数组,为Object数组,里面存入需要的参数。
  30. Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"world"});
  31. //使用CountDownLatch,如果使用同步调用则不需要这么做。
  32. CountDownLatch latch = new CountDownLatch(1);
  33. //获取结果
  34. CompletableFuture<String> future = RpcContext.getContext().getCompletableFuture();
  35. future.whenComplete((value, t) -> {
  36. System.err.println("invokeSayHello(whenComplete): " + value);
  37. latch.countDown();
  38. });
  39. //打印结果
  40. System.err.println("invokeSayHello(return): " + result);
  41. latch.await();
  42. }

通过 Spring 使用泛化调用

Spring 中服务暴露与服务发现有多种使用方式,如 xml,注解。这里以 xml 为例。 步骤:

  1. 生产者端无需改动

  2. 消费者端原有的 dubbo:reference 标签加上 generic=true 的属性。

  1. <dubbo:reference id="helloService" generic = "true" interface="org.apache.dubbo.samples.generic.call.api.HelloService"/>
  1. 获取到 Bean 容器,通过 Bean 容器拿到 GenericService 实例。

  2. 调用 $invoke 方法获取结果

  1. private static GenericService genericService;
  2. public static void main(String[] args) throws Exception {
  3. ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/generic-impl-consumer.xml");
  4. context.start();
  5. //服务对应bean的名字由xml标签的id决定
  6. genericService = context.getBean("helloService");
  7. //获得结果
  8. Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"world"});
  9. }

Protobuf 对象泛化调用

一般泛化调用只能用于生成的服务参数为 POJO 的情况,而 GoogleProtobuf 的对象是基于 Builder 生成的非正常 POJO,可以通过 protobuf-json 泛化调用。

GoogleProtobuf 序列化相关的 Demo 可以参考 protobuf-demo

通过 Spring 对 Google Protobuf 对象泛化调用

在 Spring 中配置声明 generic = “protobuf-json”

  1. <dubbo:reference id="barService" interface="com.foo.BarService" generic="protobuf-json" />

在 Java 代码获取 barService 并开始泛化调用:

  1. GenericService barService = (GenericService) applicationContext.getBean("barService");
  2. Object result = barService.$invoke("sayHello",new String[]{"org.apache.dubbo.protobuf.GooglePbBasic$CDubboGooglePBRequestType"}, new Object[]{"{\"double\":0.0,\"float\":0.0,\"bytesType\":\"Base64String\",\"int32\":0}"});

通过 API 方式对 Google Protobuf 对象泛化调用

  1. ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
  2. // 弱类型接口名
  3. reference.setInterface(GenericService.class.getName());
  4. reference.setInterface("com.xxx.XxxService");
  5. // 声明为Protobuf-json
  6. reference.setGeneric(Constants.GENERIC_SERIALIZATION_PROTOBUF);
  7. GenericService genericService = reference.get();
  8. Map<String, Object> person = new HashMap<String, Object>();
  9. person.put("fixed64", "0");
  10. person.put("int64", "0");
  11. // 参考google官方的protobuf 3 的语法,服务的每个方法中只传输一个POJO对象
  12. // protobuf的泛化调用只允许传递一个类型为String的json对象来代表请求参数
  13. String requestString = new Gson().toJson(person);
  14. // 返回对象是GoolgeProtobuf响应对象的json字符串。
  15. Object result = genericService.$invoke("sayHello", new String[] {
  16. "com.xxx.XxxService.GooglePbBasic$CDubboGooglePBRequestType"},
  17. new Object[] {requestString});

GoogleProtobuf 对象的处理

GoogleProtobuf 对象是由 Protocol 契约生成,相关知识请参考 ProtocolBuffers 文档。假如有如下 Protobuf 契约

  1. syntax = "proto3";
  2. package com.xxx.XxxService.GooglePbBasic.basic;
  3. message CDubboGooglePBRequestType {
  4. double double = 1;
  5. float float = 2;
  6. int32 int32 = 3;
  7. bool bool = 13;
  8. string string = 14;
  9. bytes bytesType = 15;
  10. }
  11. message CDubboGooglePBResponseType {
  12. string msg = 1;
  13. }
  14. service CDubboGooglePBService {
  15. rpc sayHello (CDubboGooglePBRequestType) returns (CDubboGooglePBResponseType);
  16. }

则对应请求按照如下方法构造

  1. Map<String, Object> person = new HashMap<>();
  2. person.put("double", "1.000");
  3. person.put("float", "1.00");
  4. person.put("int32","1" );
  5. person.put("bool","false" );
  6. //String 的对象需要经过base64编码
  7. person.put("string","someBaseString");
  8. person.put("bytesType","150");

GoogleProtobuf 服务元数据解析

Google Protobuf 对象缺少标准的 JSON 格式,生成的服务元数据信息存在错误。请添加如下依赖元数据解析的依赖。

  1. <dependency>
  2. <groupId>org.apache.dubbo</groupId>
  3. <artifactId>dubbo-metadata-definition-protobuf</artifactId>
  4. <version>${dubbo.version}</version>
  5. </dependency>

从服务元数据中也可以比较容易构建泛化调用对象。

注意事项

  1. 如果参数为基本类型或者 Date,List,Map 等,则不需要转换,直接调用。

  2. 如果参数为其他 POJO,则使用 Map 代替。

如:

  1. public class Student {
  2. String name;
  3. int age;
  4. public String getName() {
  5. return name;
  6. }
  7. public void setName(String name) {
  8. this.name = name;
  9. }
  10. public int getAge() {
  11. return age;
  12. }
  13. public void setAge(int age) {
  14. this.age = age;
  15. }
  16. }

在调用时应该转换为:

  1. Map<String, Object> student = new HashMap<String, Object>();
  2. student.put("name", "xxx");
  3. student.put("age", "xxx");
  1. 对于其他序列化格式,需要特殊配置

最后修改 December 16, 2022: Fix check (#1736) (97972c1)