泛化调用是指在调用方没有服务方提供的 API 的情况下,对服务方进行调用,并且可以正常拿到调用结果。
泛化调用主要用于实现一个通用的远程服务 Mock 框架,通过实现 GenericService 接口处理所有服务请求,比如如下场景:
下面,我用一系列 Demo 介绍如何使用 Dubbo 实现泛化调用。
引入 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>
服务提供者为 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
rpc 模块项目结构:
服务接口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 对象(User
和Wrapper
):
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 模块项目结构:
ActivityBooth
为IActivityBooth
的实现类:
// 省略...
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();
}
}
服务调用方通过 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
实例在缓存中。如果有,它将重用这个实例,而不是创建一个新的。String
、Integer
、Double
、Boolean
、List
、Set
等类型均属于原生类型
案例见服务 Consumer小节对sayHi
方法的调用。
// 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 传递对象。
$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'}
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
)
因此,如果在一个线程中执行多次泛化调用,并且使用 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.PojoUtils
中realize0
方法的实现。一言以蔽之,realize0
方法根据 POJO 的实际类型(Class、Type),将泛化调用传入的参数(例如:Map 或 JSON 字符串)转化为实际的 DTO 对象。