p2_fix_dubbo_collectionutils.py
9.82 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
#!/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"(?<![.\w])CollectionUtils\.isNotEmpty\(")
# 与 p2_import_legacy.py 的 SOURCES 保持一致
MODULE_MAP = {
"zteits-api": "parking-module-api",
"zteits-framework": "parking-framework/parking-legacy-common",
"zteits-park": "parking-module-park",
"zteits-order": "parking-module-order",
"zteits-pay": "parking-module-pay",
"zteits-bill": "parking-module-bill",
"zteits-charge": "parking-module-charge",
"zteits-business": "parking-module-business",
"zteits-sys": "parking-module-system",
}
SRC = "src/main/java"
def heal_spring_collectionutils(root, dry):
"""兜底:修掉「解析到 Spring 版 CollectionUtils 却调用 isNotEmpty」的文件。
这种文件在**老仓库里就已经编译不过**——Spring 那份从来没有 isNotEmpty。
实测 13 个(park 6 个、order 5 个、pay 2 个),是老仓库存量代码自身的构建问题,
与本脚本要修的错误无关,但同样挡着编译,且修法完全一样(isNotEmpty 语义一致)。
两种写法都要治:
1. `org.springframework.util.CollectionUtils.isNotEmpty(...)` —— 全限定,直接改指向
2. `import org.springframework.util.CollectionUtils;` + 裸的 `CollectionUtils.isNotEmpty(...)`
## ⚠️ 第 2 种**不能翻 import**,只能改调用点
早先的实现是把裸调用那批文件的 `import` 从 Spring 换成 commons-collections4。这是错的,
实测已被编译打脸(park 的 ParkingLotQueryServiceImpl 因此冒出 3 条
「不兼容的类型: Map<...>无法转换为 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()