#!/usr/bin/env python3 # -*- coding: utf-8 -*- r""" 一次性纠偏:把 `p2_strip_dubbo.py` 第一版换错的 CollectionUtils 改回来。 ## 错在哪 第一版把 `com.alibaba.dubbo.common.utils.CollectionUtils` 映射成了 `org.springframework.util.CollectionUtils`。Dubbo 那份是从 commons-collections 抄的,`isEmpty` 和 `isNotEmpty` 都有;Spring 那份**只有 `isEmpty`**—— 从 3.1 到 7.0 的每个版本都没有 `isNotEmpty`(用 javap 逐个核对过 ~/.m2 里的 38 个 spring-core,全为 0)。存量代码里 `isNotEmpty` 有 938 处调用, 于是 legacy-common 一编译就报 8 个文件的「找不到符号 isNotEmpty」。 `p2_strip_dubbo.py` 里的映射已改为 `org.apache.commons.collections4.CollectionUtils`(与 Dubbo 那份同源)。 但那个脚本是「找不到就不改」的幂等设计,已经换过的文件它不会再动, 所以需要本脚本单独纠偏一次。 ## 为什么不直接全局替换 不能全仓替换 `org.springframework.util.CollectionUtils`:存量代码里本来就有 若干文件是**真的**在用 Spring 那份(只用 `isEmpty`),那些是对的、不该动。 所以这里**以老仓库为准**枚举:凡是老文件 import 了 dubbo 那份的, 就把它在**新工程里的对应文件**改过来——老文件是唯一权威的事实来源。 ## 映射与路径 与 `p2_import_legacy.py` 的 SOURCES 一致:老模块 → 新模块,包内相对路径不变。 用法:python3 -I tools/p2_fix_dubbo_collectionutils.py [--dry-run] """ import argparse import os import re CLOUD = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) LEGACY = os.path.join(os.path.dirname(CLOUD), "zteits-clouds") DUBBO_IMPORT = "import com.alibaba.dubbo.common.utils.CollectionUtils;" WRONG_IMPORT = "import org.springframework.util.CollectionUtils;" RIGHT_IMPORT = "import org.apache.commons.collections4.CollectionUtils;" # 第二种写法:存量代码里有一批是**全限定**调用 Spring 那份的 isNotEmpty # (写 FQN 是为了跟同文件里另一个 CollectionUtils 区分开)。只换 import 治不了它。 FQN_WRONG = "org.springframework.util.CollectionUtils.isNotEmpty" FQN_RIGHT = "org.apache.commons.collections4.CollectionUtils.isNotEmpty" # 兜底扫描时跳过的目录:构建产物和 tools/recover 下的恢复副本 SKIP_DIR_NAMES = {"target", "out"} # 「裸的」CollectionUtils.isNotEmpty( 调用:前面既不是 `.`(排除全限定写法)也不是标识符字符 BARE_ISNOTEMPTY = re.compile(r"(?无法转换为 Collection」): * Spring 的 `CollectionUtils.isEmpty` 有 **Map 重载**; * commons-collections4 的 **没有**(它的 Map 版在 `MapUtils` 里)。 于是「同一个文件里既用 Spring 的 isEmpty(Map)、又用 isNotEmpty(List)」这种混用, 一翻 import 就把本来好好的 Map 判空打成编译错误——而 Map 调用远多于 isNotEmpty 调用 (那个文件是 29 : 1),翻 import 是「为一个病人把 29 个健康人一起动了手术」。 正确做法是**保留 Spring 的 import,只把这一个调用点改成 commons-collections4 的全限定名**。 这正是老代码作者当年手工用的手法:老文件里那处调用的原文就是 `com.alibaba.dubbo.common.utils.CollectionUtils.isNotEmpty(...)` —— 写成全限定, 就是为了跟文件顶上那个 Spring import 区分开。 """ call_fixed, fqn_fixed = [], [] for r, dirs, files in os.walk(root): dirs[:] = [d for d in dirs if d not in SKIP_DIR_NAMES] for f in files: if not f.endswith(".java"): continue p = os.path.join(r, f) with open(p, encoding="utf-8", errors="surrogateescape") as fh: src = fh.read() orig = src if FQN_WRONG in src: src = src.replace(FQN_WRONG, FQN_RIGHT) fqn_fixed.append(p) # 只处理「文件顶上是 Spring 那条 import」的:这才是真正解析错的场景。 # 已经 import commons-collections4 的文件(老代码本来就用的那一批)不动—— # 它们的裸调用本来就是对的,改成全限定名只会徒增几百处无意义 diff。 if WRONG_IMPORT in src: # 只命中「裸的」调用:前面不能是 `.`(排除已是全限定的一类,如 # org.apache.commons.collections4.CollectionUtils.isNotEmpty)也不能是标识符字符 src, n = BARE_ISNOTEMPTY.subn(FQN_RIGHT + "(", src) if n: call_fixed.append(p) if src != orig and not dry: with open(p, "w", encoding="utf-8", errors="surrogateescape") as fh: fh.write(src) return fqn_fixed, call_fixed def main(): ap = argparse.ArgumentParser() ap.add_argument("--dry-run", action="store_true") args = ap.parse_args() fixed, already, missing_new, missing_import, stale = [], [], [], [], [] for legacy_mod, new_mod in MODULE_MAP.items(): base = os.path.join(LEGACY, legacy_mod, SRC) if not os.path.isdir(base): continue for root, _, files in os.walk(base): for f in files: if not f.endswith(".java"): continue old_path = os.path.join(root, f) with open(old_path, encoding="utf-8", errors="surrogateescape") as fh: if DUBBO_IMPORT not in fh.read(): continue rel = os.path.relpath(old_path, base) new_path = os.path.join(CLOUD, new_mod, SRC, rel) if not os.path.isfile(new_path): missing_new.append((legacy_mod, rel)) continue with open(new_path, encoding="utf-8", errors="surrogateescape") as fh: src = fh.read() if RIGHT_IMPORT in src: already.append(rel) continue if WRONG_IMPORT not in src: # 既不是 dubbo 的、也不是 Spring 的:可能已被别的规则改过,人工看 missing_import.append((new_mod, rel)) continue if "isNotEmpty" not in src: # 只用了 isEmpty:换成 commons-collections4 也对, # 但留着 Spring 那份同样能跑。为消除"同一份文件两种来源"的困惑, # 仍统一换掉,保持与 p2_strip_dubbo.py 的映射一致。 stale.append(rel) new_src = src.replace(WRONG_IMPORT, RIGHT_IMPORT, 1) if not args.dry_run: with open(new_path, "w", encoding="utf-8", errors="surrogateescape") as fh: fh.write(new_src) fixed.append(rel) print(f"✔ 纠偏 {len(fixed)} 个文件 -> {RIGHT_IMPORT}") if stale: print(f" 其中 {len(stale)} 个只用了 isEmpty(换成 commons-collections4 同样正确)") print(f" 已是目标 import(重复执行):{len(already)}") if missing_new: print(f"!! 新工程里找不到对应文件 {len(missing_new)} 个:") for m, r in missing_new[:20]: print(f" {m}: {r}") if missing_import: print(f"!! import 既非 dubbo 也非 Spring,需人工确认 {len(missing_import)} 个:") for m, r in missing_import[:20]: print(f" {m}: {r}") if args.dry_run: print("(--dry-run,未落盘)") # ---- 兜底:老仓库自身就编译不过的那批(解析到 Spring 版却调 isNotEmpty)---- fqn_fixed, call_fixed = heal_spring_collectionutils(CLOUD, args.dry_run) if fqn_fixed or call_fixed: print("\n补救「Spring 版 CollectionUtils + isNotEmpty」(老仓库里本就编译不过):") if fqn_fixed: print(f" 老的全限定 Spring 调用改指向 commons-collections4:{len(fqn_fixed)} 个文件") for p in fqn_fixed: print(f" {os.path.relpath(p, CLOUD)}") if call_fixed: print(f" 裸调用改写成 commons-collections4 全限定名(保留 Spring import):{len(call_fixed)} 个文件") for p in call_fixed: print(f" {os.path.relpath(p, CLOUD)}") if __name__ == "__main__": main()