#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Dubbo 依赖对账:**每一个 `` 的实现方,是否都会进单 Jar**。 为什么必须有这一步:合成单 Jar 后 Dubbo 注册中心没了,跨模块调用变成进程内 Spring DI。 如果有哪个 reference 的实现类所在模块**没有**被并入 reactor,那么老代码里所有 `` 都会被翻译成一个 `@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']*?interface\s*=\s*"([^"]+)"[^>]*?(?:ref\s*=\s*"([^"]+)")?', re.IGNORECASE | re.DOTALL) DUBBO_REFERENCE = re.compile( r']*?interface\s*=\s*"([^"]+)"', re.IGNORECASE | re.DOTALL) JAVA_SERVICE = re.compile(r'@Service\s*\(\s*"([\w.$]+)"\s*\)') # `class Xxx implements A, B` —— 用于兜底识别"有实现类但没在 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-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('缺 的 (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('`` 共 **%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 世界里根本没人暴露**——`` 指向一个不存在的\n') w('provider。老系统靠 `check="false"` 起得来,真正调用时才失败,属于**存量潜伏缺陷**。\n') w('合成单体后 `` 变成 `@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())