如何打造一个Dubbo网关--平台

文档篇中我们成功生成了接口信息数据,并将其保存在了运行时的jar中;在更早的泛化调用中我们知道所有信息都是通过invokeName从数据库里查出来的

我们把连接在其中桥梁称为平台。平台负责接收业务系统上报的接口信息数据,对比其版本并将有变更的写入数据库。为什么要把网关和平台拆分呢:

  1. 网关就应该只作为一个应答器,只承担流量入口、流量转发、限流等和流量相关的职责
  2. 平台负责其它附加的功能,这样拆分后职责更加清晰、明确,出问题也能更快定位
  3. 网关追求高性能和稳定,可能会部署很多份;平台最多有一个standby即可,甚至因为不对外提供服务可以随意启停,方便开发

本篇内容主要讲述文档上报文档展示,文档存储部分虽然代码繁琐但是没有特别需要注意的地方就跳过了,由于代码量较大只会挑部分说明。以下代码片段全部来自于plume

文档上报

文档上报根据插件生成的版本信息,会分为两步

读取jar的index

这里又会有两种情况,读到一个index文件;读到多个index文件

平台会提供两个都叫clazzInfo的rpc来处理这两种情况,流程如下:读取文件、拿到结果、进行新的rpc调用

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
/**
* 发送index信息给平台
*
* @param gateway 平台提供的rpc服务
* @param parsePath classpath:/PARSE-INF/${systemId}
* @param groupInfo 资源初始化后的结果,可以忽略
* @param isNewgroup 是否是新的项目或分组
* @param isSingle 根据parsePath里的version数量判断,如果只有一个时true
*/
public static void classResult(GatewayService gateway, String parsePath,
GroupEntry groupInfo, Boolean isNewgroup, Boolean isSingle) {
// 只有一个或多个index处理情况类似
if (isSingle) {
// 单个index数据
final byte[] index = ResourceGet.resourceFile2Byte(parsePath + "index");
final String classInfo = new String(index);
log.debug("[COMMON] 获取单个索引数据: {}", classInfo);
// clazzInfo会返回有变更的类,methodResult里边会判断并调用新的rpc
methodResult(gateway, parsePath, groupInfo, gateway.clazzInfo(groupInfo, isNewgroup, classInfo));
} else {
// 多个index数据
final byte[][] index = ResourceGet.resourceMulti2Byte(parsePath + "index");
// 拼装成一个String数组发送
final String[] classInfos = new String[index.length];
for (int i = 0; i < index.length; i++) {
classInfos[i] = new String(index[i]);
}
log.debug("[COMMON] 获取多个索引数据: {}", Arrays.toString(classInfos));
// clazzInfo会返回有变更的类,methodResult里边会判断并调用新的rpc,这里是重载方法
methodResult(gateway, parsePath, groupInfo, gateway.clazzInfo(groupInfo, isNewgroup, classInfos));
}
}

发送有更新的类信息

这里就是根据上一步的结果去读取类,并将信息发送给平台

平台会提供一个叫methodInfo的rpc来上报,注意其返回结果和clazzInfo一致,都有一个需要的类全名列表,只有为空才代表初始化完成

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
/**
* 根据clazzInfo结果上报需要的类信息
*
* @param gateway 平台提供的rpc服务
* @param parsePath classpath:/PARSE-INF/${systemId}
* @param groupInfo 资源初始化后的结果,可以忽略
* @param methodResult 调用clazzInfo的结果或者调用methodInfo的结果,这里可能有递归
*/
public static void methodResult(GatewayService gateway, String parsePath,
GroupEntry groupInfo, GatewayMethodResult methodResult) {
// 只有上次上次调用成功才继续
if (methodResult.isSuccess()) {
// 获取平台返回的需要的类全名
final List<String> methodList = methodResult.getClassList();
final int methodSize = methodList.size();
// 为空不需要初始化
if (methodSize <= 0) {
log.info("[GATEWAY] 没有需要初始化的方法");
return;
}

// 读取并拼装类信息
final String[] methodInfos = new String[methodSize];
for (int i = 0; i < methodSize; i++) {
methodInfos[i] = new String(ResourceGet.resourceFile2Byte(parsePath + methodList.get(i)));
}

// methodInfo仍然会返回有变更的类,只有类变更列表为空才表示初始化完成
final GatewayMethodResult newResult = gateway.methodInfo(groupInfo, methodResult.getClassVersion(), methodInfos);
if (newResult.isSuccess()) {
if (null == newResult.getClassList() || newResult.getClassList().isEmpty()) {
log.info("[GATEWAY] 初始化网关成功");
} else {
methodResult(gateway, parsePath, groupInfo, newResult);
}
} else {
log.warn("[GATEWAY] 初始化网关失败, 方法列表: {}", methodList);
}
}
}

文档展示

在业务系统上报解析数据时,还需要其运行时的group,平台在存储时会根据group将类和方法存储在两个两个表里。文档展示流程如下

  1. 根据输入的group去类表里查找相关项目的所有接口信息(接口会在解析时标记出来)
  2. 根据输入的group去方法表里查找某个类相关的所有方法,这样就能获取方法所有相关的类:所在的类、所在类的父类、入参和返回参数的类
  3. 将上一步拿到的所有相关类,并加上输入group和默认group=stable,都去类表里读出来。一个group不会包含所有项目,不存在的情况下按我们的规则会走stable,所以把输入和stable两个分组的所有相关类都查出来,如果输入分组有则使用,否则使用stable里的信息
  4. 将方法相关的、会在文档接口中给前端的所有数据都放入缓存(方法表的一个字段),如果有则从缓存读取
  5. 当方法变更时,将整条数据replace掉,从而清理缓存重新生成文档数据
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// 首先查询数据库里方法是否存在
final InfoMethodDetail methodDetail = infoComponent.getMethodDetail(handle, checkGroup, invokeName, invokeLength)
.orElseThrow(() -> new PassedException(PlatformExceptionEnum.NOT_FOUND));
log.info("[GATEWAY] 查询的方法 {} ({}) | [{}] | [{}]", invokeName, invokeLength, system, checkGroup);

// 判断是否使用缓存,及缓存是否存在
if ((null == useCache || useCache) && null != methodDetail.getMethodData()) {
final String methodData = methodDetail.getMethodData();
Map<String, Object> map = Jackson.json2Map(methodData);
if (CollectionUtil.isEmpty(map)) {
log.warn("[GATEWAY] 查询的方法 {} ({}) | [{}] | [{}], 解析缓存数据失败", invokeName, invokeLength, system, checkGroup);
infoComponent.clearMethodCache(handle, group, system, invokeName);
return Collections.emptyMap();
}

// 这里是生成mock数据
boolean update = false;
String paramMock = methodDetail.getParamMock();
if (null == paramMock) {
update = true;
Object paramInfo = map.get(PARAMETER_INFO);
if (null == paramInfo) {
paramMock = "[]";
} else {
paramMock = Jackson.toJson(mock.paramMock(Jackson.objCopyList(paramInfo, FieldDetailInfo.class)));
}
}
String returnMock = methodDetail.getReturnMock();
if (null == returnMock) {
update = true;
Object returnInfo = map.get(RETURN_INFO);
if (null == returnInfo) {
returnMock = "{}";
} else {
returnMock = Jackson.toJson(mock.returnMock(Jackson.objCopyBean(returnInfo, FieldDetailInfo.class)));
}
}
map.remove("methodData");

// 如果是第一次生成mock数据,mock数据也保存下
if (update) {
infoComponent.updateMethodCache(handle, checkGroup, invokeName, invokeLength,
methodData, paramMock, returnMock);
}
// 覆盖掉map里的数据
map.put("paramMock", paramMock);
map.put("returnMock", returnMock);
return map;
} else {
Map<String, Object> map = Jackson.bean2Map(methodDetail);
assert map != null;
// 注释信息
if (null != methodDetail.getCommentInfo()) {
map.put("commentInfo", Jackson.fromJson(methodDetail.getCommentInfo()));
} else {
map.put("commentInfo", Collections.emptyMap());
}
// 注入信息
if (null != methodDetail.getInjectionInfo()) {
map.put("injectionInfo", Jackson.fromJson(methodDetail.getInjectionInfo()));
} else {
map.put("injectionInfo", Collections.emptyMap());
}

// 开始处理参数,参数可能会有多个
final String thisClazzName = methodDetail.getClazzName();
Set<String> clazzNames = Sets.newHashSet(thisClazzName);
final String parameterJson = methodDetail.getParameterInfo();
List<FieldDetailInfo> parameterInfo = null;
if (StringUtil.notBlank(parameterJson)) {
parameterInfo = Jackson.json2List(parameterJson, FieldDetailInfo.class);
// 获取所有的类全名,准备再查一次
clazzNames.addAll(parameterInfo.stream().map(FieldDetailInfo::getFullTypeName).collect(Collectors.toSet()));
} else {
map.put("parameterInfo", Collections.emptyList());
}

// 开始处理返回值,返回值只有一个
final String returnJson = methodDetail.getReturnInfo();
FieldDetailInfo returnInfo = null;
if (StringUtil.notBlank(returnJson)) {
returnInfo = Jackson.fromJson(returnJson, FieldDetailInfo.class);
// 同样获取所有的类全名,准备再查一次
clazzNames.add(returnInfo.getFullTypeName());
} else {
map.put("returnInfo", Collections.emptyMap());
}

if (!clazzNames.isEmpty()) {
// 去除java内部类,如int、String、List等
clazzNames.removeAll(InnerType.innerClass.values());
if (!clazzNames.isEmpty()) {
// getField回去类表查询相关的类
final Map<String, Optional<InfoClazzField>> refClazzField = getField(checkGroup, clazzNames);
final Set<String> refClazzNames = Sets.newHashSet();
// 这里再查询一次,也就是相关类的相关类
checkClazzName(group, refClazzNames, refClazzField, parameterInfo, returnInfo);
if (!refClazzNames.isEmpty()) {
refClazzField.putAll(getField(checkGroup, refClazzNames));
}

if (null != parameterInfo) {
// 抹去注入的参数,Injection标注的参数对前端是无意义的,也不需要传入,由网关处理
final Map<String, String> injectionNames = Maps.newHashMap();
if (null != methodDetail.getInjectionInfo()) {
final Map<String, InjectionInfo> injectionInfo = Jackson.json2Map(methodDetail.getInjectionInfo(), InjectionInfo.class);
injectionInfo.forEach((key, info) -> {
if (null != info && null != info.getInjectType()) {
injectionNames.put(key, StringUtil.splitLastByDot(info.getInjectType()));
}
});
}
// clazzType里可能出现递归,主要作用是补全类信息
if (injectionNames.isEmpty()) {
parameterInfo.forEach(parameterOne -> clazzType(thisClazzName, parameterOne, refClazzField));
} else {
parameterInfo = parameterInfo.stream().filter(one -> {
if (null != one.getEnquire() && one.getEnquire()) {
return false;
}
final String fieldName = one.getFieldName();
final String injectionType = injectionNames.get(fieldName);
// 字段名有注入信息,并且当是 MEMBER_ID 时类型是 Long
final boolean enquire = injectionNames.containsKey(fieldName)
&& (!injectionType.equalsIgnoreCase(InjectEnum.MEMBER_ID.toString())
|| InnerType.isLongType(one.getFullTypeName()));
one.setEnquire(enquire);
return !enquire;
}).collect(Collectors.toList());
parameterInfo.forEach(parameterOne -> clazzType(thisClazzName, parameterOne, refClazzField));
}
map.put("parameterInfo", parameterInfo);
}
if (null != returnInfo) {
clazzType(thisClazzName, returnInfo, refClazzField);
map.put("returnInfo", returnInfo);
}
} else {
if (null != parameterInfo) {
map.put("parameterInfo", parameterInfo);
}
if (null != returnInfo) {
map.put("returnInfo", returnInfo);
}
}
}

// 同样处理mock
String paramMock = Jackson.toJson(mock.paramMock(parameterInfo));
String returnMock = Jackson.toJson(mock.returnMock(returnInfo));
map.remove("methodData");

final int result = infoComponent.updateMethodCache(handle, checkGroup, invokeName, invokeLength,
Jackson.toJson(map), paramMock, returnMock);
log.info("[GATEWAY] 方法缓存更新结果: {}", result);

map.put("paramMock", paramMock);
map.put("returnMock", returnMock);
return map;
}

某些注解比如Injection标注的参数对前端是无意义的,也不需要前端传入,由网关在实际调用时补上,所以在文档上也不需要展示出来,这里面还有网关调用时参数长度的问题,这里就不赘述的,可以看源码

clazzType里可能出现递归,每个类都可能有字段,甚至如果使用了泛型,实际使用的类也会字段;而这些类又有可能引入了其它自定义的类,这些类的字段在文档里都需要展示。clazzType就是用来补全这些字段的,目前不允许实体里出现循环引用


如何打造一个Dubbo网关--平台
https://back.pub/post/hh-code-dubbo-gateway-6/
作者
Dash
发布于
2018年12月8日
许可协议