#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ P2 第三步:把 Dubbo XML 装配改成进程内 Spring DI。 ## 为什么是机械替换 老工程的服务实现**本来就带 Spring 注解**(`@Service("com.zteits.bill.biz.vipcard .VipCardCustInfoServiceImpl")` 这种 FQN 当 bean 名的写法),装配靠 `@ComponentScan(basePackages={"com.zteits.park", ...})` + Dubbo 的 `` 暴露。因此去 Dubbo 要做的只有两件事: 1. **provider 侧**:少数只写在 `` 里、类上没有注解的实现,补注解, 让它们仍被 component-scan 注册。(348/390 的 provider 已经有注解了。) 2. **consumer 侧**:老代码用 `private XxxService xxxService;` 裸字段, Spring 是靠 Dubbo `` 生成 bean 后按**变量名**注入的 (`@Autowired` 缺失是因为老版本 `` 配了自动装配, 或干脆就是靠 Dubbo 的 ref 装配)。现在要在字段上补 `@Autowired`。 ## 为什么用 @Autowired 而不是 @Resource 实测 390 个 provider 接口**没有一个存在多实现**(见脚本末尾的校验), 所以按类型注入无歧义;而按名字注入要求 bean 名与字段名严格一致, 老代码里字段名与 XML 的 `ref`/`id` 并不总是同名(例如 `@Component("com.zteits .sys.biz.SysNoticeServiceImpl")` 的 bean 名是 FQN,字段名却是 `sysNoticeService`)。 按类型注入能同时覆盖这两种情况。 ## 幂等 已有 `@Autowired`/`@Resource` 的字段跳过;已有 Spring 原型注解的类跳过。 可用 `--dry-run` 只出报告不改文件。 """ import argparse import os import re import sys import collections CLOUD = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) LEGACY = os.path.join(os.path.dirname(CLOUD), "zteits-clouds") # 只处理已迁入的模块(portal 未搬,政务 P6 再说) MODS = [ "parking-module-park", "parking-module-order", "parking-module-pay", "parking-module-bill", "parking-module-charge", "parking-module-business", "parking-module-system", "parking-framework/parking-legacy-common", ] API_SERVICE_PREFIX = "com.zteits.clouds.api.service." SPRING_STEREOTYPES = ("Service", "Component", "Repository", "Configuration", "RestController", "Controller") COMMENT = re.compile(r"", re.S) TAG_BEAN = re.compile(r"]*?)/?>", re.S) TAG_DUBBO_SERVICE = re.compile(r"]*?)/?>", re.S) TAG_DUBBO_REF = re.compile(r"]*?)/?>", re.S) IMPORT = re.compile(r"^import\s+(?:static\s+)?([\w.$]+)\s*;", re.M) def attrs(s): # ⚠️ 必须 strip:老 XML 里存在 ref=" com.zteits.park.biz...ServiceImpl" 这种 # 属性值前带空格的写法(zteits-park provider 第 704 行),不 strip 会去找 # 一个首字符是空格的类名。 return { k: v.strip() for k, v in re.findall(r"([\w:.-]+)\s*=\s*\"([^\"]*)\"", s) } def _glob(pat): import glob as g return g.glob(pat) def read(path): return open(path, encoding="utf-8", errors="surrogateescape").read() # --------------------------------------------------------------------------- # 源码索引 # --------------------------------------------------------------------------- def build_index(): """FQN -> 文件路径""" idx = {} for mod in MODS: base = os.path.join(CLOUD, mod, "src/main/java") for root, _, files in os.walk(base): for f in files: if f.endswith(".java"): fqn = os.path.relpath(os.path.join(root, f), base)[:-5].replace(os.sep, ".") idx[fqn] = os.path.join(root, f) return idx IDX = build_index() # 简单类名索引:兜底解析「XML 里 ref 写错但类名对得上」的情况 # (实测:zteits-sys provider 里 ref 写成 # com.zteits.clouds.api.service.sys.WxcpMsgSendConfigService.WxcpMsgSendConfigServiceImpl, # 真实实现是 com.zteits.sys.biz.WxcpMsgSendConfigServiceImpl) SIMPLE_INDEX = collections.defaultdict(list) for _fqn in IDX: SIMPLE_INDEX[_fqn.rsplit(".", 1)[1]].append(_fqn) def resolve_impl(impl): """把 XML 里的 ref/class 值解析成 (真实 FQN, 源文件路径),失败返回 None。""" p = IDX.get(impl) if p: return impl, p cands = SIMPLE_INDEX.get(impl.rsplit(".", 1)[1], []) if len(cands) == 1: return cands[0], IDX[cands[0]] return None IMPORT_LINE = re.compile(r"^import\s+(?:static\s+)?[\w.$]+\s*;[ \t]*$", re.M) def add_import(txt, fqn): """插入 import,位置跟在最后一个已存在 import 之后(保持字典序)。 已存在则原样返回。""" line = f"import {fqn};" if re.search(r"^\s*" + re.escape(line) + r"\s*$", txt, re.M): return txt last = None for m in IMPORT_LINE.finditer(txt): last = m if last is None: return txt # 按字典序插入到合适位置:找第一个比 fqn 大的 import target = None for m in IMPORT_LINE.finditer(txt): if m.group(0).strip() > f"import {fqn};": target = m break anchor = target or last insert_at = anchor.start() if target else anchor.end() if target: return txt[:insert_at] + line + "\n" + txt[insert_at:] return txt[:insert_at] + "\n" + line + txt[insert_at:] # --------------------------------------------------------------------------- # provider 侧 # --------------------------------------------------------------------------- def scan_providers(): """返回 (impl_fqn -> {interfaces}), (孤儿 bean: beanid -> class)""" impl_ifaces = collections.defaultdict(set) all_beans = {} # beanId -> class FQN orphan_beans = {} # beanId -> class FQN(没有被任何 dubbo:service 引用) for p in _glob(f"{LEGACY}/zteits-*/src/main/resources/dubbo/*provider*.xml"): txt = COMMENT.sub("", read(p)) beans = {} for m in TAG_BEAN.finditer(txt): a = attrs(m.group(1)) if a.get("id") and a.get("class"): beans[a["id"]] = a["class"] all_beans.update(beans) refs = set() for m in TAG_DUBBO_SERVICE.finditer(txt): a = attrs(m.group(1)) i, r = a.get("interface"), a.get("ref") if not i or not r: continue refs.add(r) impl_ifaces[beans.get(r, r)].add(i) for bid, cls in beans.items(): if bid not in refs: orphan_beans[bid] = cls return impl_ifaces, orphan_beans # --------------------------------------------------------------------------- # 类声明定位(含前置注解) # --------------------------------------------------------------------------- def class_decl(txt, simple): return re.search( r"^(?P(?:@[\w.]+(?:\([^)]*\))?\s*)*)" r"public\s+(?Pfinal\s+|abstract\s+)?class\s+" + re.escape(simple) + r"\b", txt, re.M, ) def has_stereotype(anns): return any(re.search(r"@" + s + r"\b", anns) for s in SPRING_STEREOTYPES) # --------------------------------------------------------------------------- # consumer 侧:字段注入 # --------------------------------------------------------------------------- class_field = re.compile( r"^(?P[ \t]*)" r"(?P(?:@[\w.]+(?:\([^)]*\))?[ \t]*\n[ \t]*)*)" r"(?P(?:private|protected|public)[ \t]+)" r"(?!static|final)" r"(?P[\w.$]+)[ \t]+(?P\w+)[ \t]*;", re.M, ) INJECTED = ("@Autowired", "@Resource", "@Inject") def wire_fields(path, api_ifaces): """给类型属于 api_ifaces 的裸字段补 @Autowired。返回改动列表。""" txt = read(path) if API_SERVICE_PREFIX not in txt: return [] # 快速跳过:全仓 7000+ 文件,绝大多数不含老契约 import imap = {} for m in IMPORT.finditer(txt): fqn = m.group(1) if fqn.startswith(API_SERVICE_PREFIX) or fqn.startswith("com.zteits.clouds.api."): imap[fqn.rsplit(".", 1)[1]] = fqn hits = [] def repl(m): t = m.group("type") fqn = imap.get(t) or (t if t.startswith("com.zteits.clouds.api.") else None) if not fqn or fqn not in api_ifaces: return m.group(0) if any(a in m.group("anns") for a in INJECTED): return m.group(0) hits.append((m.group("name"), fqn)) indent = m.group("indent") anns = m.group("anns") return f"{indent}@Autowired\n{indent}{m.group('mods')}{t} {m.group('name')};" new = class_field.sub(repl, txt) if new != txt: open(path, "w", encoding="utf-8", errors="surrogateescape").write(new) return hits # --------------------------------------------------------------------------- def main(): ap = argparse.ArgumentParser() ap.add_argument("--dry-run", action="store_true") args = ap.parse_args() impl_ifaces, orphan_beans = scan_providers() print(f"provider impl 类:{len(impl_ifaces)}") # 前提校验:同一接口不得有多个实现,否则 @Autowired 按类型注入会歧义 owners = collections.defaultdict(set) for impl, ifaces in impl_ifaces.items(): for i in ifaces: owners[i].add(impl) multi = {i: v for i, v in owners.items() if len(v) > 1} if multi: print(f"\n!! 以下接口存在多实现,@Autowired 会歧义,需人工处理:") for i, v in sorted(multi.items()): print(f" {i}\n {sorted(v)}") else: print("✔ 前提校验通过:没有任何接口存在多实现,按类型注入安全") # ---- A. provider 侧补注解 + import ---- # TODO 列表元素:(真实FQN, 路径, 注解类型, 注解值) todo, no_src = [], [] for impl, ifaces in sorted(impl_ifaces.items()): r = resolve_impl(impl) if not r: no_src.append((impl, sorted(ifaces)[0])) continue real, p = r txt = read(p) m = class_decl(txt, real.rsplit(".", 1)[1]) if not m: no_src.append((impl, f"<找不到类声明 {real}>")) continue todo.append((real, p, "Service", real, impl)) # 孤儿 bean(provider XML 里声明了但没被 dubbo:service 引用) for bid, cls in sorted(orphan_beans.items()): r = resolve_impl(cls) if not r: no_src.append((cls, f'<孤儿 bean id="{bid}">')) continue real, p = r todo.append((real, p, "Component", real, cls)) need, done = [], 0 for real, p, kind, value, raw in todo: txt = read(p) m = class_decl(txt, real.rsplit(".", 1)[1]) if not m: continue anns = m.group("anns") # ① 已经有本脚本加的那种注解 → 只可能缺 import(上一次跑漏了) # ② 已经有别的 Spring 原型注解(老代码自己的 @Component/@Service)→ 不动它 # ③ 什么注解都没有 → 补注解 + import if re.search(r"@" + kind + r"\b", anns): ann_missing, imp_missing = False, ( f"import org.springframework.stereotype.{kind};" not in txt ) elif has_stereotype(anns): continue else: ann_missing, imp_missing = True, True if ann_missing or imp_missing: need.append((real, p, kind, value, raw, ann_missing, imp_missing)) print(f"\n【provider】需补 Spring 原型注解/import 的实现类:{len(need)}") for real, p, kind, value, raw, am, im_ in need: flags = [] if am: flags.append(f'@ {kind}("{value}")') if im_: flags.append(f"import {kind}") extra = "" if real == raw else f" (XML 里写的是 {raw})" print( f' + {" / ".join(flags)} {os.path.relpath(p, CLOUD)}{extra}' ) if no_src: print(f"\n【provider】新工程里解析不出源码(未随迁 / 死配置):{len(no_src)}") for impl, iface in no_src: print(f" ? {impl}\n iface={iface}") if not args.dry_run: for real, p, kind, value, raw, am, im_ in need: txt = read(p) if im_: txt = add_import(txt, f"org.springframework.stereotype.{kind}") done += 1 if am: m = class_decl(txt, real.rsplit(".", 1)[1]) if m and not re.search(r"@" + kind + r"\b", m.group("anns")): txt = ( txt[: m.start()] + f'@{kind}("{value}")\n' + txt[m.start():] ) done += 1 open(p, "w", encoding="utf-8", errors="surrogateescape").write(txt) print(f"\n✅ provider 侧写入 {done} 处") # ---- B. consumer 侧补 @Autowired ---- consumer_ifaces = set() for p in _glob(f"{LEGACY}/zteits-*/src/main/resources/dubbo/*consumer*.xml"): txt = COMMENT.sub("", read(p)) for m in TAG_DUBBO_REF.finditer(txt): a = attrs(m.group(1)) if a.get("interface"): consumer_ifaces.add(a["interface"]) # 以「provider 提供的接口 ∪ consumer 引用的接口」为准 provided = {i for s in impl_ifaces.values() for i in s} targets = { t for t in (provided | consumer_ifaces) if t.startswith(API_SERVICE_PREFIX) } print(f"\n【consumer】需要注入的契约接口:{len(targets)}") total_fields, total_files = 0, 0 by_mod = collections.Counter() for mod in MODS: base = os.path.join(CLOUD, mod, "src/main/java") for root, _, files in os.walk(base): for f in files: if not f.endswith(".java"): continue p = os.path.join(root, f) hits = wire_fields(p, targets) if hits: total_fields += len(hits) total_files += 1 by_mod[mod] += len(hits) print(f" {os.path.relpath(p, CLOUD)} <- {len(hits)}: {', '.join(n for n, _ in hits)}") print(f"\n✅ consumer 侧共 {total_fields} 个字段 / {total_files} 个文件") for k, v in by_mod.most_common(): print(f" {k}: {v}") if __name__ == "__main__": main()