泛化呼叫(客戶端泛化)

無需伺服器端 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. 消費端在原有 dubbo:reference 標籤上加上 generic=true 的屬性即可

   <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 物件泛化呼叫

一般的泛化呼叫只能在生成的服務參數為 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 格式化,導致服務元數據信息有誤,請添加如下依賴 metadata 解析的依賴。

<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 日:增強英文文檔 (#1798) (95a9f4f6c1c)