一看就会!Dubbo 泛化调用简明教程——含不同类型入参处理

Dubbo泛化调用

  • 简介
  • 配置
    • pom.xml
    • application.yml
    • 服务 Provider
    • 服务 Consumer
  • 不同入参的处理
    • 原生类型入参
    • DTO 入参
      • Map 方式
      • JSON 方式
    • 集合入参
    • 泛型入参

简介

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

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

  1. 网关服务:如果要搭建一个网关服务,那么服务网关要作为所有 RPC 服务的调用端。但是网关本身不应该依赖于服务提供方的接口 API(否则每一个新的服务发布,都需要修改网关的代码并重新部署),所以需要泛化调用支持。
  2. 测试平台:如果要搭建一个可以测试 RPC 调用的平台,用户输入分组名、接口、方法名等信息,就可以测试对应的 RPC 服务。

下面,我用一系列 Demo 介绍如何使用 Dubbo 实现泛化调用。

配置

pom.xml

引入 Dubbo 依赖:

<dependency>
    <groupId>org.apache.dubbogroupId>
    <artifactId>dubboartifactId>
    <version>2.7.5version>
dependency>

使用 Zookeeper 作为配置中心(需要在本机安装好 zk):

<dependency>
    <groupId>org.apache.curatorgroupId>
    <artifactId>curator-recipesartifactId>
    <version>4.0.1version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeepergroupId>
            <artifactId>zookeeperartifactId>
        exclusion>
    exclusions>
dependency>

<dependency>
    <groupId>org.apache.zookeepergroupId>
    <artifactId>zookeeperartifactId>
    <version>3.4.10version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-log4j12artifactId>
        exclusion>
        <exclusion>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-apiartifactId>
        exclusion>
        <exclusion>
            <groupId>log4jgroupId>
            <artifactId>log4jartifactId>
        exclusion>
    exclusions>
dependency>

application.yml

服务提供者为 Spring Boot 项目,需要在 application.yml 中配置 Dubbo:

server:
  port: 8090
dubbo:
  application:
    version: 1.0.0
    name: api-gateway-test-provider
  registry:
    address: zookeeper://127.0.0.1:2181
  protocol:
    name: dubbo
    port: 20880
  scan:
    base-packages: cn.wzz.gateway.interfaces
  provider:
    timeout: 30000

服务 Provider

rpc 模块项目结构
一看就会!Dubbo 泛化调用简明教程——含不同类型入参处理_第1张图片


服务接口IActivityBooth

public interface IActivityBooth {
	// case1: 入参为字符串、整数等原生数据类型
    String sayHi(String msg);
	// case2: 入参为DTO 对象
    String describeUser(User user);
	// case3: 入参为 DTO 对象集合
    String describeUsers(List<User> user);
	// case4: 入参为泛型对象
    String describeWrapper(Wrapper<User> userWrapper);
}

DTO 对象UserWrapper):

Wrapper包装类,用于测试含泛型入参的泛化调用

public class User implements Serializable {
    private String uid;
    private Integer age;
    private String nickName;
    //getter/setter
}

public class Wrapper<T> implements Serializable {
    T val;
    
    @Override
    public String toString() {
        return val.toString();
    }
    // getter/setter
}

interfaces 模块项目结构:
一看就会!Dubbo 泛化调用简明教程——含不同类型入参处理_第2张图片


ActivityBoothIActivityBooth的实现类:

// 省略...
import org.apache.dubbo.config.annotation.Service;

@Service(version = "1.0.0")
@Controller
public class ActivityBooth implements IActivityBooth {
    @Override
    public String sayHi(String msg) {
        return String.format("[sayHi] msg: %s", msg);
    }

    @Override
    public String describeUser(User user) {
        return user.toString();
    }

    @Override
    public String describeUsers(List<User> user) {
        StringBuilder builder = new StringBuilder();
        for(User usr: user) {
            builder.append(usr.toString()).append(", ");
        }

        int lastIdx = builder.lastIndexOf(", ");
        return lastIdx == -1 ? builder.toString() : builder.substring(0, lastIdx);
    }

    @Override
    public String describeWrapper(Wrapper<User> userWrapper) {
        return userWrapper.toString();
    }
}

服务 Consumer

服务调用方通过 API 使用泛化调用,步骤如下:

  • 设置ReferenceConfig时,使用setGeneric("true")开启泛化调用;
  • 配置完ReferenceConfig后,使用referenceConfig.get()获取GenericService 类的实例;
  • 使用GenericService#$invoke方法,执行泛化调用;

完整配置如下:

ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("api-gateway-test-provider"); // 服务提供方名称
applicationConfig.setQosEnable(false); // 关闭qos服务

// 创建注册中心配置
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
registryConfig.setRegister(false); // 是否向注册中心注册服务, false为只订阅, 不注册


// 创建服务引用配置, reference封装了与注册中心以及提供者的连接, 是个很重的实例
ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
referenceConfig.setInterface("cn.wzz.gateway.rpc.IActivityBooth"); // rpc接口
referenceConfig.setVersion("1.0.0");
referenceConfig.setGeneric("true"); // 重点: 设置泛化调用

referenceConfig.setRegistry(registryConfig);
referenceConfig.setApplication(applicationConfig);

DubboBootstrap bootstrap = DubboBootstrap.getInstance();
bootstrap.application(applicationConfig)
        .registry(registryConfig)
        .reference(referenceConfig)
        .start();

ReferenceConfigCache cache = ReferenceConfigCache.getCache();
GenericService genericService = cache.get(referenceConfig);

case1:入参为字符串,调用sayHi方法

Object result = genericService.$invoke("sayHi", new String[]{"java.lang.String"}, new Object[]{"generic caller wzz"});
System.out.println("sayHi result: " + result);
// 输出: 
// sayHi result: [sayHi] msg: generic caller wzz

使用ReferenceConfigCache而不是直接通过ReferenceConfig.get()获取泛化调用GenericService实例,原因是为了管理和优化ReferenceConfig实例的使用:

  • ReferenceConfig实例是重量级的,包含了与注册中心以及服务提供者的连接信息。
  • ReferenceConfigCache是一个简单的缓存实现,它可以重用已经创建的ReferenceConfig实例。当你通过ReferenceConfigCache请求一个服务引用时,它首先检查是否已经有一个相同配置的ReferenceConfig实例在缓存中。如果有,它将重用这个实例,而不是创建一个新的。
  • 性能优化:减少了重复创建重量级对象的开销,保持稳定的连接状态和服务引用,而不是每次都重新建立连接。

不同入参的处理

原生类型入参

StringIntegerDoubleBooleanListSet等类型均属于原生类型
案例见服务 Consumer小节对sayHi方法的调用。

DTO 入参

// case2: DTO User对象作为入参
String describeUser(User user);

public class User implements Serializable {
    private String uid;
    private Integer age;
    private String nickName;
    // getter/setter
}

泛化调用方可以使用 Map 或 JSON 传递对象。

Map 方式

$invoke中指定方法名、方法入参类型(全限定名)数组、参数值。因为没有引入服务 Provider 接口依赖,所以使用 Map 描述 DTO 对象。

/* case2: DTO作为入参 */
HashMap<String, Object> userMap = new HashMap<>();
userMap.put("age", 12);
userMap.put("uid", "32792");
userMap.put("nickName", "wzz");
Object describeUserRes = genericService.$invoke("describeUser", new String[]{"cn.wzz.gateway.rpc.dto.User"}, new Object[]{userMap});

System.out.println("describeUser result: " + describeUserRes);
// 结果:
// describeUser result: User{uid='32792', age=12, nickName='wzz'}

JSON 方式

Dubbo 2.7.12 之后支持 JSON 泛化调用,需要修改 pom.xml(更新dubbo、curator版本;增加curator-x-discovery模块):

<dependency>
    <groupId>org.apache.dubbogroupId>
    <artifactId>dubboartifactId>
    <version>2.7.14version>
dependency>
<dependency>
    <groupId>org.apache.dubbogroupId>
    <artifactId>dubbo-spring-boot-starterartifactId>
    <version>2.7.14version>
dependency>

<dependency>
    <groupId>org.apache.curatorgroupId>
    <artifactId>curator-recipesartifactId>
    <version>5.1.0version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeepergroupId>
            <artifactId>zookeeperartifactId>
        exclusion>
    exclusions>
dependency>

<dependency>
    <groupId>org.apache.curatorgroupId>
    <artifactId>curator-x-discoveryartifactId>
    <version>5.1.0version>
dependency>

<dependency>
    <groupId>org.apache.zookeepergroupId>
    <artifactId>zookeeperartifactId>
    <version>3.6.1version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-log4j12artifactId>
        exclusion>
        <exclusion>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-apiartifactId>
        exclusion>
        <exclusion>
            <groupId>log4jgroupId>
            <artifactId>log4jartifactId>
        exclusion>
    exclusions>
dependency>

ReferenceConfig不变,RpcContext中设置generic=gson

ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
referenceConfig.setInterface("cn.wzz.gateway.rpc.IActivityBooth"); // rpc接口
referenceConfig.setGeneric("true"); // 重点: 设置泛化调用
// 设置 generic=gson
RpcContext.getContext().setAttachment("generic", "gson");

随后,使用 fastjson 将 User 对象转变为 json 字符串,并执行泛化调用:

User user = new User();
user.setAge(12);
user.setUid("32792");
user.setNickName("wzz");
String userJson = JSON.toJSONString(user);
Object describeUserJsonRes = genericService.$invoke("describeUser", new String[]{"cn.wzz.gateway.rpc.dto.User"}, new Object[]{userJson});

踩坑注意!!:当前线程使用GenericService执行完一次泛化调用后,都会 remove RpcContext(详见org.apache.dubbo.rpc.filter.ContextFilter#invoke
一看就会!Dubbo 泛化调用简明教程——含不同类型入参处理_第3张图片
因此,如果在一个线程中执行多次泛化调用,并且使用 JSON 字符串方式传递入参 DTO 对象,则需要在每次调用前,手动在RpcContext中设置generic=gson
正确的代码如下,在完成sayHi方法调用后,setAttachment("generic", "gson")重新设置generic

/* case1: 字符串作为入参 */
RpcContext.getContext().setAttachment("generic", "gson");
String sayHiJson = JSON.toJSONString("generic caller wzz");
Object result = genericService.$invoke("sayHi", new String[]{"java.lang.String"}, new Object[]{sayHiJson});
System.out.println("sayHi result: " + result);

// 注意: 调用完成后会清空generic的值, 需要重新设置!
Object genericVal = RpcContext.getContext().get("generic");
System.out.println("generic=" + genericVal);  // null

/* case2: DTO作为入参 */
RpcContext.getContext().setAttachment("generic", "gson"); // 重新设置RpcContext
User user = new User();
user.setAge(12);
user.setUid("32792");
user.setNickName("wzz");
String userJson = JSON.toJSONString(user);
Object describeUserJsonRes = genericService.$invoke("describeUser", new String[]{"cn.wzz.gateway.rpc.dto.User"}, new Object[]{userJson});

集合入参

方法入参为 List、Set 等集合:

String describeUsers(List<User> user);

使用ArrayList存储多个 User Map,然后将List对象作为参数值执行泛化调用:

/* case3: POJO集合作为入参 */
HashMap<String, Object> userMap = new HashMap<>();
userMap.put("age", 12);
userMap.put("uid", "32792");
userMap.put("nickName", "wzz");

HashMap<String, Object> userMap2 = new HashMap<>();
userMap2.put("age", 22);
userMap2.put("uid", "32972");
userMap2.put("nickName", "wy");

ArrayList<HashMap<String, Object>> userList = new ArrayList<>();
userList.add(userMap);
userList.add(userMap2);

Object describeUsersRes = genericService.$invoke("describeUsers", new String[]{"java.util.List"}, new Object[]{userList});

泛型入参

接口入参的类型包含泛型,例如Wrapper

String describeWrapper(Wrapper<User> userWrapper);

// Wrapper
public class Wrapper<T> implements Serializable {
    T val;
    
    @Override
    public String toString() {
        return val.toString();
    }
}

当使用 Map 表示 User 对象时,在 Map 中增加key="class"的条目用于指定泛型的具体类型

/* case4: 带泛型的POJO作为入参 */
Map<String, Object> userMap = new HashMap<>();
userMap.put("age", 12);
userMap.put("uid", "32792");
userMap.put("nickName", "wzz");
userMap.put("class", "cn.wzz.gateway.rpc.dto.User"); // PojoUtils解析Map类型,如果存在key=class时,会直接指定该Object类型

Map<String, Object> wrapperMap = new HashMap<>();
wrapperMap.put("val", userMap);
// 泛化调用
Object describeWrapperRes = genericService.$invoke("describeWrapper", new String[]{"cn.wzz.gateway.rpc.dto.Wrapper"}, new Object[]{wrapperMap});

具体 Dubbo 如何解析泛化调用传入的参数,需要查看 org.apache.dubbo.common.utils.PojoUtilsrealize0方法的实现。一言以蔽之,realize0方法根据 POJO 的实际类型(Class、Type),将泛化调用传入的参数(例如:Map 或 JSON 字符串)转化为实际的 DTO 对象
一看就会!Dubbo 泛化调用简明教程——含不同类型入参处理_第4张图片

  • 若泛化调用传递的参数值为Map类型,则从Map中获取key="class"的value。
    一看就会!Dubbo 泛化调用简明教程——含不同类型入参处理_第5张图片
  • 根据泛型的实际类型,反射创建实例dest,然后解析Map中的 key-value,并设置dest实例对应的字段。

你可能感兴趣的:(dubbo,java,spring,boot,java-zookeeper)