泛化调用(客户端泛化)

不需要服务端 API 的 RPC 调用

功能描述

泛化调用是指在没有服务端提供的 API(SDK)的情况下调用服务端,并且可以正常获取调用结果。

使用场景

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

  1. 网关服务:如果你想构建一个网关服务,那么网关服务应该是所有 RPC 服务的调用端。但是,网关本身不应该依赖于服务提供者的接口 API(这会导致每次发布新服务时都需要修改和重新部署网关的代码),因此需要支持泛化调用。

  2. 测试平台:如果你想构建一个可以测试 RPC 调用的平台,用户可以通过输入组名、接口、方法名等信息来测试相应的 RPC 服务。然后,出于同样的原因(即每次发布新服务时都需要修改和重新部署网关的代码),平台本身不应该依赖于服务提供者的接口 API。因此需要支持泛化调用。

如何使用

演示可以在 dubbo 项目中的示例代码 中看到

API 部分以该演示为例,说明如何使用它。

服务定义

服务接口

public interface HelloService {

    String sayHello(String name);

    CompletableFuture<String> sayHelloAsync(String name);

    CompletableFuture<Person> sayHelloAsyncComplex(String name);

    CompletableFuture<GenericType<Person>> sayHelloAsyncGenericComplex(String name);
}

服务实现类

public class HelloServiceImpl implements HelloService {

    @Override
    public String sayHello(String name) {
        return "sayHello: " + name;
    }

    @Override
    public CompletableFuture<String> sayHelloAsync(String name) {
        CompletableFuture<String> future = new CompletableFuture<>();
        new Thread(() -> {
            try {
                Thread. sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            future.complete("sayHelloAsync: " + name);
        }).start();

        return future;
    }

    @Override
    public CompletableFuture<Person> sayHelloAsyncComplex(String name) {
        Person person = new Person(1, "sayHelloAsyncComplex: " + name);
        CompletableFuture<Person> future = new CompletableFuture<>();
        new Thread(() -> {
            try {
                Thread. sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            future. complete(person);
        }).start();

        return future;
    }

    @Override
    public CompletableFuture<GenericType<Person>> sayHelloAsyncGenericComplex(String name) {
        Person person = new Person(1, "sayHelloAsyncGenericComplex: " + name);
        GenericType<Person> genericType = new GenericType<>(person);
        CompletableFuture<GenericType<Person>> future = new CompletableFuture<>();
        new Thread(() -> {
            try {
                Thread. sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            future. complete(genericType);
        }).start();

        return future;
    }
}

通过 API 使用泛化调用

服务发起者

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

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

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

private static String zookeeperAddress = "zookeeper://" + System.getProperty("zookeeper.address", "127.0.0.1") + ":2181";

    public static void main(String[] args) throws Exception {
        new Embedded ZooKeeper(2181, false).start();

        //Create ApplicationConfig
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("generic-impl-provider");
        //Create registry configuration
        RegistryConfig registryConfig = new RegistryConfig();
        registryConfig.setAddress(zookeeperAddress);
        
        //Create a new service implementation class, pay attention to use GenericService to receive
        GenericService helloService = new GenericImplOfHelloService();

        //Create service related configuration
        ServiceConfig<GenericService> service = new ServiceConfig<>();
        service.setApplication(applicationConfig);
        service.setRegistry(registryConfig);
        service.setInterface("org.apache.dubbo.samples.generic.call.api.HelloService");
        service.setRef(helloService);
        // Key point: set to generalization call
        // Note: it is no longer recommended to use the setGeneric function whose parameter is a Boolean value
        //should use referenceConfig.setGeneric("true") instead
        service.setGeneric("true");
        service. export();

        System.out.println("dubbo service started");
        
        new CountDownLatch(1). await();
    }
}

泛化调用者

步骤

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

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

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

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

    //Define generalized call service class
    private static GenericService genericService;
    public static void main(String[] args) throws Exception {
        //Create ApplicationConfig
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("generic-call-consumer");
        //Create registry configuration
        RegistryConfig registryConfig = new RegistryConfig();
        registryConfig.setAddress("zookeeper://127.0.0.1:2181");
        //Create service reference configuration
        ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
        //Set the interface
        referenceConfig.setInterface("org.apache.dubbo.samples.generic.call.api.HelloService");
        applicationConfig.setRegistry(registryConfig);
        referenceConfig.setApplication(applicationConfig);
        // Key point: set to generalization call
        // Note: it is no longer recommended to use the setGeneric function whose parameter is a Boolean value
        //should use referenceConfig.setGeneric("true") instead
        referenceConfig.setGeneric(true);
        //Set asynchronous, not necessary, it depends on the business.
        referenceConfig.setAsync(true);
        //Set the timeout
        referenceConfig.setTimeout(7000);
        
        //Get the service, because it is a generalized call, so it must be of the GenericService type
        genericService = referenceConfig. get();
        
        //Using the $invoke method of the GenericService class object can be used instead of the original method
        //The first parameter is the name of the method to call
        //The second parameter is the parameter type array of the method to be called, which is a String array, and the full class name of the parameter is stored in it.
        //The third parameter is the parameter array of the method to be called, which is an Object array, and the required parameters are stored in it.
        Object result = genericService. $invoke("sayHello", new String[]{"java. lang. String"}, new Object[]{"world"});
        //Use CountDownLatch, if you use synchronous calls, you don't need to do this.
        CountDownLatch latch = new CountDownLatch(1);
        //Get the result
        CompletableFuture<String> future = RpcContext.getContext().getCompletableFuture();
        future. whenComplete((value, t) -> {
            System.err.println("invokeSayHello(whenComplete): " + value);
            latch. countDown();
        });
        // print the result
        System.err.println("invokeSayHello(return): " + result);
        latch. await();
    }

使用 Spring 进行泛化调用

在 Spring 中使用服务暴露和服务发现有很多方法,例如 xml 和注解。这里以 xml 为例。步骤

  1. 生产者端无需更改

  2. 在消费者端,将 generic=true 的属性添加到原始的 dubbo:reference 标签中。

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

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


    private static GenericService genericService;

    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/generic-impl-consumer.xml");
        context. start();
        //The name of the bean corresponding to the service is determined by the id of the xml tag
        genericService = context. getBean("helloService");
        //Get the result
        Object result = genericService. $invoke("sayHello", new String[]{"java. lang. String"}, new Object[]{"world"});
    }

Protobuf 对象泛化调用

普通的泛化调用只能在生成的 service 参数是 POJO 时使用,而 GoogleProtobuf 对象是基于 Builder 生成的非正常 POJO,可以通过 protobuf-json 进行泛化调用。

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

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

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

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

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

GenericService barService = (GenericService) applicationContext. getBean("barService");
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 对象进行泛化调用

ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
// Weakly typed interface name
reference.setInterface(GenericService.class.getName());
reference.setInterface("com.xxx.XxxService");
// Declare as Protobuf-json
reference.setGeneric(Constants.GENERIC_SERIALIZATION_PROTOBUF);

GenericService genericService = reference. get();
Map<String, Object> person = new HashMap<String, Object>();
person. put("fixed64", "0");
person. put("int64", "0");
// Referring to Google's official protobuf 3 syntax, only one POJO object is transmitted in each method of the service
// The generalized call of protobuf only allows passing a json object of type String to represent the request parameter
String requestString = new Gson().toJson(person);
// The return object is the json string of the GoolgeProtobuf response object.
Object result = genericService. $invoke("sayHello", new String[] {
    "com.xxx.XxxService.GooglePbBasic$CDubboGooglePBRequestType"},
    new Object[] {requestString});

GoogleProtobuf 对象的处理

GoogleProtobuf 对象是通过 Protocol 契约生成的。有关相关知识,请参考 ProtocolBuffers 文档。如果存在以下 Protobuf 契约

syntax = "proto3";
package com.xxx.XxxService.GooglePbBasic.basic;
message CDubboGooglePBRequestType {
    double double = 1;
    float float = 2;
    int32 int32 = 3;
    bool bool = 13;
    string string = 14;
    bytes bytesType = 15;
}

message CDubboGooglePBResponseType {
    string msg = 1;
}

service CDubboGooglePBService {
    rpc sayHello (CDubboGooglePBRequestType) returns (CDubboGooglePBResponseType);
}

那么相应的请求将根据以下方法构建

Map<String, Object> person = new HashMap<>();
person. put("double", "1.000");
person. put("float", "1.00");
person. put("int32","1");
person. put("bool","false");
//String objects need to be base64 encoded
person. put("string","someBaseString");
person. put("bytesType","150");

GoogleProtobuf 服务元数据分析

Google Protobuf 对象缺乏标准的 JSON 格式,导致生成的 service 元数据信息不正确。请添加以下依赖项,这些依赖项依赖于元数据解析。

<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-metadata-definition-protobuf</artifactId>
    <version>${dubbo.version}</version>
</dependency>

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

注意事项

  1. 如果参数是基本类型或 Date、List、Map 等,则无需转换,直接调用即可。

  2. 如果参数是另一个 POJO,则使用 Map 代替。

例如

public class Student {
    String name;
    int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this. age = age;
    }
}

应该转换为

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

上次修改时间:2023 年 1 月 2 日:增强 en 文档 (#1798) (95a9f4f6c1c)