如何打造一个Dubbo网关--泛化调用

我们的网关是基于SpringCloudGateway实现,显然不可能在网关项目中引入所有业务项目的client.jar,更不可能在client.jar变化时重启网关,那要如何实现rpc调用呢?这里就要引入一个Dubbo里的重要功能:泛化调用

所谓的泛化调用就是在调用方没有接口及模型类元的情况,通过已知条件直接构造出接口相关引用来实现rpc调用的一种手段。网关通过泛化调用将内部的Dubbo接口转换成rest形式给前端使用

通过泛化调用可以在不依赖jar的情况下进行rpc调用,本篇内容主要讲述泛化调用接口路由转换。以下代码片段全部来自于plume

接口路由转换

在说明泛化调用之前,我们先来设置一下接口路由转换,我们在这里定义一个uri:

/{system}/{clazz}/{method}

  • system:指定的系统名
  • clazz:系统里的类名,简写
  • method:类里面的方法名

我们通过system.clazz.method这种形式去查找此次调用的具体信息,当然实际还需要当次调用的参数长度

  • 上述的uri被称为pathNamesystem.clazz.method这种形式被称为invokeName
  • 类和方法都会存储在数据库中(表在文末),具体如何存储将会在平台里详细介绍
  • 网关里的统一使用post+json形式接口,这样做是为了编写足够简单,同时前后端沟通足够简单,消除这方面的歧义;当然采用rest风格也没关系,这不重要
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 默认服务后缀
*/
private final String serviceSuffix = "Service";

@PostMapping("/{system}/{clazz}/{method}")
public Mono<ApiResponse> apiPath(ServerHttpRequest request,
@PathVariable String system, @PathVariable String clazz, @PathVariable String method,
@RequestBody ApiRequest input) {
if (clazz.startsWith(servicePrefix)) {
clazz = StringUtil.firstLowerCase(system) + StringUtil.firstUpperCase(clazz.substring(1));
} else {
clazz = StringUtil.firstLowerCase(clazz);
}
if (!clazz.endsWith(serviceSuffix)) {
clazz += serviceSuffix;
}
return doPath(request, system, clazz, method, input);
}

这里使用serviceSuffix是为了让pathName更加简单和好看。doPath里拼装invokeName并将获取body、cookie等信息交给dubboInvoker处理,实际最终是由doResult方法进行处理

PS. 这里有一个强制要求:对外提供的服务的类必须以Service结尾,否则无法通过path调用

泛化调用

Dubbo的泛化调用相当简单,只需要构造出GenericService对象然后使用$invoke进行调用即可

泛化调用和正常调用最大的不同是:没有类信息,所以调用过程中只能使用json序列化,拿到的结果也是一个map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public ReferenceConfig<GenericService> referenceConfig(String clazzName) {
ReferenceConfig<GenericService> reference = referenceCache.getIfPresent(clazzName);
if (null == reference) {
reference = new ReferenceConfig<>();
reference.setInterface(clazzName);
reference.setApplication(applicationConfig);
reference.setRegistry(registryConfig);
reference.setConsumer(consumerConfig);
reference.setGeneric(true);
referenceCache.put(clazzName, reference);
}
return reference;
}

private Object doResult(RequestInfo request, String getToken, String invokeName,
Map<String, Object> inputParam, Map<String, Object> inputAttach,
Map<String, Object> memberInfo, InvokeDetailCache cache, String proof) {
//分割获取方法名
String methodName = StringUtil.splitLastByDot(invokeName);
......

//拼装输入参数
final List<Object> invokeParams = new ArrayList<>();
......

Object result = null;
GenericService genericService = null;
try {
genericService = gatewayCache.referenceConfig(cache.getClassName()).get();
if (null == genericService) {
gatewayCache.referenceClean(cache.getClassName());
genericService = gatewayCache.referenceConfig(cache.getClassName()).get();
}
log.debug("[GATEWAY] 将调用的方法参数为: {} = {} | 凭证: {} | 引用: {}", methodName, cache, proof, genericService);
result = genericService.$invoke(methodName, cache.getTypes(), invokeParams.toArray());
} catch (NoSuchMethodError | NullPointerException ex) {
log.error("[GATEWAY] 调用的方法缓存错误,清除缓存: {} = {} | 凭证: {} | 引用: {}", methodName, cache, proof, genericService);
gatewayCache.referenceClean(cache.getClassName());
}
if (null == result) {
throw new PassedException(PlatformExceptionEnum.RESULT_ERROR);
}
return result;
}
  • 需要先从referenceConfig()里获取一个相关clazzName的引用,然后获取GenericService进行调用。注意这里的clazzName是全名
  • ReferenceConfig这类对象是Dubbo里的重对象,必须缓存而不能使用时创建,当然在Provider销毁时也要注意网关里引用的销毁和移除
  • GenericService是实际发起调用的对象,因为Java存在重写,所以必须指定所有参数类型
  • InvokeDetailCache里的信息都是自数据库中查出来,然后放在缓存里的。后文有详细说明

总结

由于目前并未并未深入源码,在之后的组合调用中将为大家详细讲述整个前置流程。到这里我讲述了三件事情来说明这个网关方案的可行性:

  1. 给每个子项目定一个systemName,然后将项目中的每个类和方法按照固定规则转换成invokeName
  2. 每个类和方法及相关的systemNameinvokeName等都会被存储在数据库,网关在每次调用时都要获取这些信息
  3. 网关通过invokeName查询到InvokeDetailCache,拼装出GenericService进行泛化调用拿到最终结果并返回给前端

表信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
create table info_method
(
group_name varchar(64) not null comment '组名',
clazz_name varchar(128) not null comment '所属类名',
path_name varchar(64) not null comment '路径名',
invoke_name varchar(64) not null comment '调用名',
invoke_length int not null comment '参数长度',
system_id int not null comment '系统id',
is_whitelist boolean default false comment '是否在白名单中',
is_background boolean default false comment '是否在是后台接口',
cache_time int default 0 comment '是否要使用网关缓存(0不使用大于0则表示缓存的分钟数)',
is_track boolean default false comment '是否记录用户请求',
simple_name varchar(128) comment '简单方法名',
simple_parameter json comment '简单参数',
simple_comment varchar(256) comment '简单信息',
comment_info json comment '注释信息',
parameter_info json comment '输入信息',
return_info json comment '返回信息',
injection_info json comment '需要注入的信息',
permission_info json comment '权限信息',
method_data json comment '缓存返回值',
param_mock json comment '参数mock信息',
return_mock json comment '返回mock信息',
created_at timestamp default current_timestamp comment '添加时间',
updated_at timestamp default current_timestamp on update current_timestamp comment '更新时间',
deleted_at timestamp,
index idx_path_name(path_name),
index idx_invoke_name(invoke_name),
index idx_simple_name(simple_name),
index idx_group_clazz_name(group_name, clazz_name),
primary key (group_name, invoke_name, invoke_length)
) engine = innodb
default charset = utf8mb4 comment = '方法表';

如何打造一个Dubbo网关--泛化调用
https://back.pub/post/hh-code-dubbo-gateway-1/
作者
Dash
发布于
2018年10月27日
许可协议