p2_audit_mapper_bind.py
12.2 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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
P2 审计脚本:**按接口**去查「这个 Mapper 到底绑没绑」。
## 为什么需要它(第二十七切片的血债)
第二十七切片迁 `/park/xc-car-charge-audit/query-last-xc-car-charge-audit` 时撞上
Invalid bound statement (not found): com.zteits.park.mapper.ParkingLotWhitelistMapper.selectByExample
新栈整包**没有**这个 namespace —— 但**老门户上这条链路是通的**,所以不是存量缺陷(红线⑥
不适用),是搬丢了。根因是**一份 XML 用了别人的文件名**:
zteits-park/src/main/resources/mybatis/mapper/HubParkingEquipmentMapper.xml
└── <mapper namespace="com.zteits.park.mapper.ParkingLotWhitelistMapper"> ← 表也是 parking_lot_whitelist
老仓那一份文件同时是「`ParkingLotWhitelistMapper` 的**唯一**宿主」和「`HubParkingEquipmentMapper`
的一个空壳名」(后者在**老仓压根没绑定** —— 存量缺陷)。新栈按 `hub_parking_equipment` 表
**重新生成**了同名 XML,namespace 与表名都归正、顺手修掉了老仓那个缺陷,但那份文件里唯一装着的
11 条 whitelist 语句也随之被覆盖消失。⇒ 「查某个 mapper 的 XML 在不在」**不能按文件名找**。
`tools/p2_audit_mapper_ns.py` 查的是「XML 的 namespace 在不在 @MapperScan 的包下」,
方向相反 —— 它看不见「某个接口该有的 XML 没搬过来」。本脚本就是补这个方向的缺口。
## 判定口径(必须写死,否则没人能复现)
**扫描面**:两栈各 8 个模块的 `src/`(老仓对应 `zteits-{pay,order,bill,park,charge,sys,business}`),
文件名以 `Mapper.java` 结尾且含 `interface` 的算「Mapper 接口」;XML 取 `<mapper namespace=...>`。
**口径**:只统计 namespace 以 `com.zteits.{bill,business,charge,order,park,pay,sys}.mapper` 打头的 ——
这正是 `ParkingServerApplication` 第 2 条 `@MapperScan` 的 7 个包(与 `p2_audit_mapper_ns.py`
的 `SCAN_PACKAGES` 同源)。这一刀同时把 MyBatis-Plus 的
`com.zteits.parking.module.system.dal.mysql.*`(框架自带、无 XML)和 tk 的
`com.zteits.{sys,business}.util.MyMapper`(标记接口)挡在外面。
**四类结果**:
| 类 | 含义 | 期望 |
|---|---|---|
| OK | 新栈有 `namespace == 接口 FQCN` 的 XML | 610 个左右 |
| **A 丢搬** | 老栈有该 namespace、新栈没有 ⇒ **移植回归** | **必须 0** |
| B 存量就断 | 两栈都没有绑得上的 XML(老仓当年就断) | 留痕,逐个看是否死代码 |
| C 新栈独有 | 接口只在新栈存在(老仓没有)⇒ WIP 或死代码 | 留痕,看调用方 |
**退出码**:A > 0 才 `exit 1`。B/C 是存量事实/WIP,不是本脚本要拦的东西。
## B 类的两个子型(都是存量事实,别当回归修)
* **namespace 写错**:老 XML 在、但 `<mapper namespace>` 与接口所在包不一致。
例:`zteits-pay/.../RealtimeInfoSMapper.xml` 的 namespace 是
`com.zteits.park.mapper.smapper.RealtimeInfoSMapper`,而接口在 `com.zteits.pay.mapper.smapper`
—— 老门户调它同样是 `Invalid bound statement`,新栈**照搬**了这个事实。
* **压根没有 SQL**:接口有、两栈都没有任何 XML。例:`TdBOrderIncomeCalculationSMapper`。
## C 类要当心:不是所有 C 都是死代码
`ParkingLotExtendAdapterMapper` 的**唯一**引用者 `ParkingLotExtendAdapterCache` 本身零引用 ⇒ 真死。
但 `TdCCouponRuleCustidCarnumRelMapper` 被 `TdCCustPersonServiceImpl` 的
`queryPersonAndCarNumForPageByUserPhone` / `saveCardNoAndCustIdAndCarNum` 调用,
而这两个方法**老仓没有**、在新栈是**新写的**(WIP)—— 一旦有人给它配个 Controller,
第一次调用就是 `Invalid bound statement` 500。本脚本把「调用方」列出来,就是为了让这条别被漏掉。
## 用法
python3 -I tools/p2_audit_mapper_bind.py
python3 -I tools/p2_audit_mapper_bind.py --legacy-dir /path/to/zteits-clouds
"""
import argparse
import os
import re
import sys
CLOUD = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEFAULT_LEGACY = "/Users/wangbiao/IdeaProjects/parking/zteits-clouds"
# 与 ParkingServerApplication 的 @MapperScan / p2_audit_mapper_ns.py 的 SCAN_PACKAGES 同源
PKGS = ["bill", "business", "charge", "order", "park", "pay", "sys"]
PREFIXES = tuple("com.zteits.%s.mapper" % k for k in PKGS)
NEW_MODULES = ["parking-module-park", "parking-module-order", "parking-module-pay",
"parking-module-bill", "parking-module-charge", "parking-module-business",
"parking-module-system", "parking-module-infra"]
LEGACY_MODULES = ["zteits-pay", "zteits-order", "zteits-bill", "zteits-park",
"zteits-charge", "zteits-sys", "zteits-business"]
SKIP_DIR_NAMES = {"target", "out", "node_modules", ".git"}
NS_RE = re.compile(r'namespace\s*=\s*"([^"]+)"')
PKG_RE = re.compile(r'package\s+([\w.]+);')
def _walk(roots, ext):
for root0 in roots:
if not os.path.isdir(root0):
continue
for root, dirs, files in os.walk(root0):
dirs[:] = [d for d in dirs if d not in SKIP_DIR_NAMES]
for name in files:
if name.endswith(ext):
yield os.path.join(root, name)
def collect(roots):
"""→ (接口 FQCN 集合, {namespace: [xml 路径]})"""
ifaces, namespaces = set(), {}
for p in _walk(roots, ".java"):
if not p.endswith("Mapper.java"):
continue
src = open(p, encoding="utf-8", errors="surrogateescape").read()
m = PKG_RE.search(src)
if m and re.search(r"\binterface\b", src):
ifaces.add(m.group(1) + "." + os.path.basename(p)[:-5])
for p in _walk(roots, ".xml"):
head = open(p, encoding="utf-8", errors="surrogateescape").read(8192)
m = NS_RE.search(head)
if m:
namespaces.setdefault(m.group(1), []).append(p)
return ifaces, namespaces
def callers_of(simple_name, roots):
"""新栈里引用了这个简单名的文件(排除同名文件自己)。
⚠️ 纯文本匹配、不做语义分析:命中可能是注释、可能是同名的别的类型。所以下面只拿它当
「有没有人提过这个名字」的粗筛,结论要顺着引用链再走一跳(`dead_chain`)。
"""
hits = []
for p in _walk(roots, ".java"):
if os.path.basename(p) == simple_name + ".java":
continue
if simple_name in open(p, encoding="utf-8", errors="surrogateescape").read():
hits.append(p)
return hits
def dead_chain(caller_path, roots):
"""引用方的类名**自己**有没有人引用?没有 ⇒ 整条链是死的(第二十七切片实测:
`ParkingLotExtendAdapterMapper` ← `ParkingLotExtendAdapterCache` ← 无人引用)。"""
return not callers_of(os.path.basename(caller_path)[:-5], roots)
def misnamed_legacy_xml(legacy, namespaces):
"""老仓「文件名 != namespace 末段(忽略大小写)」的 XML —— 按文件名搬运必丢的那批。"""
out = []
for ns, paths in namespaces.items():
simple = ns.rsplit(".", 1)[-1]
for p in paths:
stem = os.path.basename(p)[:-4]
if stem.lower() != simple.lower():
out.append((ns, stem, os.path.relpath(p, legacy)))
return sorted(out)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--legacy-dir", default=DEFAULT_LEGACY)
ap.add_argument("--show-ok", action="store_true", help="连绑好的接口一起列(610 行,一般不用)")
args = ap.parse_args()
new_roots = [os.path.join(CLOUD, m, "src") for m in NEW_MODULES]
legacy_roots = [os.path.join(args.legacy_dir, m, "src") for m in LEGACY_MODULES]
if not os.path.isdir(args.legacy_dir):
print("!! 老仓目录不存在:%s(用 --legacy-dir 指定)" % args.legacy_dir)
return 2
new_if, new_ns = collect(new_roots)
leg_if, leg_ns = collect(legacy_roots)
new_if = {x for x in new_if if x.startswith(PREFIXES)}
leg_if = {x for x in leg_if if x.startswith(PREFIXES)}
bound = sorted(x for x in new_if if x in new_ns)
unbound = sorted(x for x in new_if if x not in new_ns)
cls_a = [x for x in unbound if x in leg_if and x in leg_ns] # 老有、新丢
cls_b = [x for x in unbound if x in leg_if and x not in leg_ns] # 两栈都断
cls_c = [x for x in unbound if x not in leg_if] # 新栈独有
print("口径:namespace 以 %s 打头" % (" / ".join(PREFIXES)))
print(" 新栈 mapper 接口 %d(已绑定 %d,未绑定 %d)" % (len(new_if), len(bound), len(unbound)))
print(" 老栈 mapper 接口 %d,老栈 namespace %d" % (len(leg_if), len(leg_ns)))
if args.show_ok:
print("\n== OK(新栈有同名 namespace 的 XML)==")
for x in bound:
print(" " + x)
print("\n== A 丢搬(老有 namespace、新栈没有)== 必须 0")
if cls_a:
for x in cls_a:
print(" !! %s" % x)
for p in leg_ns[x]:
print(" 老仓 XML:%s" % os.path.relpath(p, args.legacy_dir))
print(" ⇒ 这些是**移植回归**:老门户上通、新栈 500。按文件名找 XML 会漏,")
print(" 要去老仓 grep `namespace=\"<FQCN>\"` 定位真正装它的那份文件。")
else:
print(" ✔ 0 个")
print("\n== B 存量就断(两栈都没有绑得上的 XML)== 存量事实,留痕不修(红线⑥)")
if cls_b:
for x in cls_b:
simple = x.rsplit(".", 1)[-1]
same_name = [p for p in _walk(legacy_roots, ".xml")
if os.path.basename(p)[:-4].lower() == simple.lower()]
if same_name:
wrong = []
for p in same_name:
head = open(p, encoding="utf-8", errors="surrogateescape").read(8192)
m = NS_RE.search(head)
wrong.append(m.group(1) if m else "?")
print(" · %s" % x)
print(" 老仓有同名 XML,但 namespace 写成了:%s" % " / ".join(wrong))
else:
print(" · %s(两栈都没有任何同名 XML:接口有、SQL 压根没写过)" % x)
else:
print(" ✔ 0 个")
print("\n== C 新栈独有(老仓没有这个接口)== 老仓没得抄,看调用方:WIP 还是死代码")
if cls_c:
for x in cls_c:
simple = x.rsplit(".", 1)[-1]
callers = callers_of(simple, new_roots)
print(" · %s" % x)
if not callers:
print(" ✔ 零引用 ⇒ 死代码")
continue
for p in callers:
rel = os.path.relpath(p, CLOUD)
if dead_chain(p, new_roots):
print(" 被引用:%s" % rel)
print(" └ 但**它自己**零引用 ⇒ 死链,不可达(真死代码)")
else:
print(" 被引用:%s" % rel)
print(" └ ★ 它自己有引用方 ⇒ **可达**:谁来调这条链路,")
print(" 第一次就是 `Invalid bound statement` 500(WIP 没补 XML)")
else:
print(" ✔ 0 个")
mis = misnamed_legacy_xml(args.legacy_dir, leg_ns)
print("\n== 附录:老仓「文件名 != namespace」的 XML %d 份(忽略大小写比较)==" % len(mis))
print(" 这份清单 = 「按文件名找 XML 会踩空」的候选。列在这里**不等于** namespace 丢了 ——")
print(" 是否真丢,看上面的 A 类;这里只说明文件名不是绑定关系的可靠索引。")
for ns, stem, rel in mis:
print(" %-50s <- %s" % (ns, rel))
print(" 第二十七切片补搬的正是 `ParkingLotWhitelistMapper` 那条:老文件名是")
print(" HubParkingEquipment(新栈按该表重生成了同名文件 ⇒ 覆盖掉这条 namespace)。")
print(" 另一条 `TbAreaMangeSMapper.xml` 是**拼写**错(Mange/Manage),不是大小写,")
print(" 两栈都原样带着这个文件名 ⇒ 它绑得好好的(A 类里没有它)。")
return 1 if cls_a else 0
if __name__ == "__main__":
sys.exit(main())