dubbo_dependency_scan.py
10.1 KB
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Dubbo 依赖对账:**每一个 `<dubbo:reference>` 的实现方,是否都会进单 Jar**。
为什么必须有这一步:合成单 Jar 后 Dubbo 注册中心没了,跨模块调用变成进程内 Spring DI。
如果有哪个 reference 的实现类所在模块**没有**被并入 reactor,那么老代码里所有
`<dubbo:reference>` 都会被翻译成一个 `@Resource` 字段,但**永远没有对应的 Bean** ——
编译能过、启动能过,一调用就是 `NoSuchBeanDefinitionException`,属于"上线才发现"的故障。
产出:把每个 reference 的「消费方模块 → 提供方模块」列出来,标出提供方不在并入清单里的。
用法:
python3 dubbo_dependency_scan.py <老仓库根目录> <输出目录>
"""
import os
import re
import sys
from collections import defaultdict
# P2 计划并入单 Jar 的模块(其余模块的实现**不在**这个清单里 → 必须随迁或另作处理)
MERGED_MODULES = {
'zteits-sys', 'zteits-park', 'zteits-order', 'zteits-pay',
'zteits-bill', 'zteits-charge',
# 方案 §4.6 的随迁例外
'zteits-business', 'zteits-data-collection',
'zteits-framework', 'zteits-api',
}
DUBBO_SERVICE = re.compile(
r'<dubbo:service\b[^>]*?interface\s*=\s*"([^"]+)"[^>]*?(?:ref\s*=\s*"([^"]+)")?',
re.IGNORECASE | re.DOTALL)
DUBBO_REFERENCE = re.compile(
r'<dubbo:reference\b[^>]*?interface\s*=\s*"([^"]+)"',
re.IGNORECASE | re.DOTALL)
JAVA_SERVICE = re.compile(r'@Service\s*\(\s*"([\w.$]+)"\s*\)')
# `class Xxx implements A, B<C>` —— 用于兜底识别"有实现类但没在 provider XML 里声明"的接口
JAVA_IMPLEMENTS = re.compile(r'\bclass\s+\w+[^{;]*?\bimplements\s+([^{]+)', re.DOTALL)
def fqn(raw):
"""
取属性里的接口全限定名。
⚠️ 老仓库的 provider XML 写成 `interface=" com.zteits...Xxx"`——**引号内带前导空格**
(dubbo-sys-provider.xml 第 95 行等)。不 strip 的话字典键就是带空格的字符串,
与 consumer 侧干净的 FQN 永远匹配不上,会把大批"其实有实现方"的接口误报成"找不到提供方"。
"""
return (raw or '').strip()
def module_of(rel):
parts = rel.replace(os.sep, '/').split('/')
for p in parts:
if p.startswith('zteits-'):
return p
return '(unknown)'
def main():
if len(sys.argv) != 3:
print(__doc__)
return 2
root, outdir = sys.argv[1], sys.argv[2]
os.makedirs(outdir, exist_ok=True)
providers = defaultdict(set) # interface FQN -> {模块}(来自 provider XML / @Service)
impls = defaultdict(set) # 接口简名 -> {模块}(来自 `implements`,兜底)
refs = [] # (消费方模块, interface FQN, 文件)
provider_xml_seen = 0
beanless = 0
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames
if d not in ('node_modules', 'target', '.git', 'dist')]
for fn in filenames:
if not fn.endswith(('.xml', '.java')):
continue
full = os.path.join(dirpath, fn)
rel = os.path.relpath(full, root)
if 'dubbo' not in rel and 'dubbo' not in fn:
if not fn.endswith('.java'):
continue
module = module_of(rel)
try:
text = open(full, encoding='utf-8', errors='ignore').read()
except OSError:
continue
low = rel.lower()
if fn.endswith('.xml') and 'dubbo' in low:
# ⚠️ 必须先去掉 XML 注释:老仓库大量 `<dubbo:reference>` 是被 `<!-- -->` 包起来的
# 历史残留(如 dubbo-order-consumer.xml 第 63、74 行整条注释掉),
# 不去注释会把它们当成"活跃引用",凭空多出一批"找不到提供方"的假告警。
text = re.sub(r'<!--.*?-->', ' ', text, flags=re.DOTALL)
if 'provider' in low:
provider_xml_seen += 1
for m in DUBBO_SERVICE.finditer(text):
providers[fqn(m.group(1))].add(module)
if not m.group(2):
beanless += 1
elif 'consumer' in low:
for m in DUBBO_REFERENCE.finditer(text):
refs.append((module, fqn(m.group(1)), rel))
elif fn.endswith('.java'):
for m in JAVA_SERVICE.finditer(text):
providers[fqn(m.group(1))].add(module)
# 实现类兜底:接口 FQN 取 import 里的全名,简名在这里拼不出来,
# 所以只按**简名**登记一份,引用侧比对时同时看全名与简名。
for m in JAVA_IMPLEMENTS.finditer(text):
for raw in m.group(1).split(','):
name = raw.strip().split('<')[0].strip()
if name and name[0].isupper():
impls[name].add(module)
# 消费方 -> 提供方 -> 接口
gaps = defaultdict(list) # 提供方模块 -> [(消费方, 接口)]
ok = defaultdict(int)
resolvable = [] # 有实现类、但没在 provider XML 声明 → 单体内按类型能解析
unknown = []
for consumer, iface, rel in refs:
pmods = providers.get(iface)
if pmods:
for pmod in pmods:
if pmod in MERGED_MODULES:
ok[pmod] += 1
else:
gaps[pmod].append((consumer, iface))
continue
# 没在 provider XML 里声明,看看有没有实现类
impl_mods = impls.get(iface.rsplit('.', 1)[-1])
if impl_mods:
resolvable.append((consumer, iface, sorted(impl_mods)))
else:
unknown.append((consumer, iface, rel))
# 对账:实际提供方模块集合
all_provider_mods = defaultdict(set)
for iface, mods in providers.items():
for m in mods:
all_provider_mods[m].add(iface)
print('reference 总数: %d(去重接口 %d)' % (len(refs), len({r[1] for r in refs})))
print('provider XML: %d 份;@Service 注解接口 %d 个'
% (provider_xml_seen, sum(1 for v in providers.values())))
print('缺 <bean> 的 <dubbo:service>(ref 直接写类名): %d' % beanless)
print('实现模块不在并入清单的 reference: %d' % sum(len(v) for v in gaps.values()))
print('未声明但存在实现类(单体内按类型可解析): %d' % len(resolvable))
print('找不到任何实现方的 reference: %d' % len(unknown))
md = os.path.join(outdir, 'dubbo-dependency-report.md')
with open(md, 'w', encoding='utf-8') as fh:
w = fh.write
w('# Dubbo 依赖对账(单 Jar 化前的必查项)\n\n')
w('> 生成:`tools/dubbo_dependency_scan.py`\n\n')
w('`<dubbo:reference>` 共 **%d** 处(已剔除 XML 注释里的历史残留),去重后 **%d** 个接口;\n'
% (len(refs), len({r[1] for r in refs})))
w('提供方在并入清单内 **%d** 处,**不在清单内 %d 处**,\n'
% (sum(ok.values()), sum(len(v) for v in gaps.values())))
w('**有实现类但没在 provider XML 声明 %d 处**,完全找不到实现方 %d 处。\n\n'
% (len(resolvable), len(unknown)))
w('## 1. ⚠️ 实现模块不在并入清单(必须处理,否则调用即 500)\n\n')
if not gaps:
w('无。\n')
else:
for pmod in sorted(gaps, key=lambda k: -len(gaps[k])):
w('\n### `%s` —— 被引用 %d 处\n\n' % (pmod, len(gaps[pmod])))
w('| 消费方 | 接口 |\n|---|---|\n')
for consumer, iface in sorted(set(gaps[pmod])):
w('| `%s` | `%s` |\n' % (consumer, iface))
w('\n## 2. 提供方模块分布(清单内的)\n\n')
w('| 提供方模块 | 被引用的接口数 |\n|---|---:|\n')
for pmod in sorted(ok, key=lambda k: -ok[k]):
w('| `%s` | %d |\n' % (pmod, ok[pmod]))
w('\n## 3. 有实现类但没在 provider XML 声明(单体内会自动修好,但要验证)\n\n')
w('这些接口**在老的 Dubbo 世界里根本没人暴露**——`<dubbo:reference>` 指向一个不存在的\n')
w('provider。老系统靠 `check="false"` 起得来,真正调用时才失败,属于**存量潜伏缺陷**。\n')
w('合成单体后 `<dubbo:reference>` 变成 `@Resource` 按类型注入,实现类只要在同一个\n')
w('ApplicationContext 里就会被解析到 —— **合并顺手修好了它**,但必须逐个确认\n')
w('"实现类确实被扫进 Spring 容器",否则就是把原来的运行期失败换了个位置。\n\n')
w('| 消费方 | 接口 | 实现类所在模块 |\n|---|---|---|\n')
for consumer, iface, mods in sorted(set((c, i, tuple(m)) for c, i, m in resolvable)):
w('| `%s` | `%s` | %s |\n'
% (consumer, iface, '、'.join('`%s`' % m for m in mods)))
w('\n## 4. 找不到实现方的 reference(多是跨仓契约,需人工确认)\n\n')
w('| 消费方 | 接口 | 位置 |\n|---|---|---|\n')
for consumer, iface, rel in sorted(unknown)[:200]:
w('| `%s` | `%s` | `%s` |\n' % (consumer, iface, rel))
if len(unknown) > 200:
w('\n… 另有 %d 处,见 CSV。\n' % (len(unknown) - 200))
print('写出 %s' % md)
csv_path = os.path.join(outdir, 'dubbo-references.csv')
with open(csv_path, 'w', encoding='utf-8') as fh:
fh.write('消费方模块,接口,提供方模块,状态\n')
for consumer, iface, _ in sorted(refs):
pmods = providers.get(iface)
if not pmods:
fh.write('%s,%s,,NO_PROVIDER\n' % (consumer, iface))
else:
for pmod in sorted(pmods):
state = 'OK' if pmod in MERGED_MODULES else 'NOT_MERGED'
fh.write('%s,%s,%s,%s\n' % (consumer, iface, pmod, state))
print('写出 %s' % csv_path)
return 0
if __name__ == '__main__':
sys.exit(main())