p2_wire_spring_di.py
14.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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
#!/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:service ref="...">` 暴露。因此去 Dubbo 要做的只有两件事:
1. **provider 侧**:少数只写在 `<bean id= class=>` 里、类上没有注解的实现,补注解,
让它们仍被 component-scan 注册。(348/390 的 provider 已经有注解了。)
2. **consumer 侧**:老代码用 `private XxxService xxxService;` 裸字段,
Spring 是靠 Dubbo `<dubbo:reference id="xxxService">` 生成 bean 后按**变量名**注入的
(`@Autowired` 缺失是因为老版本 `<context:component-scan>` 配了自动装配,
或干脆就是靠 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"<bean\b([^>]*?)/?>", re.S)
TAG_DUBBO_SERVICE = re.compile(r"<dubbo:service\b([^>]*?)/?>", re.S)
TAG_DUBBO_REF = re.compile(r"<dubbo:reference\b([^>]*?)/?>", 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<anns>(?:@[\w.]+(?:\([^)]*\))?\s*)*)"
r"public\s+(?P<mods>final\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<indent>[ \t]*)"
r"(?P<anns>(?:@[\w.]+(?:\([^)]*\))?[ \t]*\n[ \t]*)*)"
r"(?P<mods>(?:private|protected|public)[ \t]+)"
r"(?!static|final)"
r"(?P<type>[\w.$]+)[ \t]+(?P<name>\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()