我们的网关是基于SpringCloudGateway
实现,显然不可能在网关项目中引入所有业务项目的client.jar,更不可能在client.jar变化时重启网关,那要如何实现rpc调用呢?这里就要引入一个Dubbo
里的重要功能:泛化调用
所谓的泛化调用就是在调用方没有接口及模型类元的情况,通过已知条件直接构造出接口相关引用来实现rpc调用的一种手段。网关通过泛化调用将内部的Dubbo
接口转换成rest形式给前端使用
通过泛化调用可以在不依赖jar的情况下进行rpc调用,本篇内容主要讲述泛化调用和接口路由转换。以下代码片段全部来自于plume
接口路由转换
在说明泛化调用之前,我们先来设置一下接口路由转换,我们在这里定义一个uri:
/{system}/{clazz}/{method}
- system:指定的系统名
- clazz:系统里的类名,简写
- method:类里面的方法名
我们通过system.clazz.method
这种形式去查找此次调用的具体信息,当然实际还需要当次调用的参数长度
- 上述的uri被称为
pathName
,system.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
里的信息都是自数据库中查出来,然后放在缓存里的。后文有详细说明
总结
由于目前并未并未深入源码,在之后的组合调用中将为大家详细讲述整个前置流程。到这里我讲述了三件事情来说明这个网关方案的可行性:
- 给每个子项目定一个
systemName
,然后将项目中的每个类和方法按照固定规则转换成invokeName
- 每个类和方法及相关的
systemName
、invokeName
等都会被存储在数据库,网关在每次调用时都要获取这些信息
- 网关通过
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 = '方法表';
|