p2_audit_mapper_ns.py 4.38 KB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
存量 mapper XML 的 namespace 审计 —— 用来自证 @MapperScan 的包清单是完备的。

## 为什么要审计

`ParkingServerApplication` 上的第 2 条 `@MapperScan` 写死了 7 个包:

    com.zteits.{bill,business,charge,order,park,pay,sys}.mapper

这条清单不能靠印象,只能靠**把全部存量 XML 的 namespace 枚举出来**:少一个包,
那个包下的 Mapper 接口就不会注册,运行时报「无此 Bean」;多一个包,
可能把 tk.mybatis 的 `MyMapper<T>` 之类的标记接口扫进来(老代码注释写着会出错)。

## 判读

* 「越界 namespace」= 不在 7 个包下的,必须逐个看(要么补进扫描清单,要么确认是死 XML)
* 「无 Java 接口的 namespace」= XML 有、接口没了。这类是老仓库自带的死 XML,
  不会导致启动失败(MyBatis 只有在你真正调用该 statement 时才会报错),
  但要列出来留痕,别在排查「XML 没加载」时被它们带偏。

用法:python3 -I tools/p2_audit_mapper_ns.py
"""

import os
import re
import sys
from collections import Counter

CLOUD = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# 与 ParkingServerApplication 的 @MapperScan 保持一致
SCAN_PACKAGES = [
    "com.zteits.bill.mapper",
    "com.zteits.business.mapper",
    "com.zteits.charge.mapper",
    "com.zteits.order.mapper",
    "com.zteits.park.mapper",
    "com.zteits.pay.mapper",
    "com.zteits.sys.mapper",
]

# 存量业务模块(mapper XML 只可能在这几个里)
MODULES = [
    "parking-module-park",
    "parking-module-order",
    "parking-module-pay",
    "parking-module-bill",
    "parking-module-charge",
    "parking-module-business",
    "parking-module-system",
]

NS_RE = re.compile(r'namespace\s*=\s*"([^"]+)"')
SKIP_DIR_NAMES = {"target", "out"}


def main():
    xml_hits = []   # (namespace, xml路径)
    for mod in MODULES:
        base = os.path.join(CLOUD, mod, "src/main/java")
        res = os.path.join(CLOUD, mod, "src/main/resources")
        for root0 in (base, res):
            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 f in files:
                    if not f.endswith(".xml"):
                        continue
                    p = os.path.join(root, f)
                    with open(p, encoding="utf-8", errors="surrogateescape") as fh:
                        head = fh.read(8192)
                    m = NS_RE.search(head)
                    if m:
                        xml_hits.append((m.group(1), p))

    # 现存 Java 接口(FQN 集合)
    java_types = set()
    for mod in MODULES + ["parking-module-api"]:
        base = os.path.join(CLOUD, mod, "src/main/java")
        if not os.path.isdir(base):
            continue
        for root, dirs, files in os.walk(base):
            dirs[:] = [d for d in dirs if d not in SKIP_DIR_NAMES]
            for f in files:
                if f.endswith(".java"):
                    java_types.add(f[:-5])

    print(f"扫到带 namespace 的 XML {len(xml_hits)} 个;Java 类型 {len(java_types)} 个\n")

    outside, missing, by_pkg = [], [], Counter()
    for ns, p in xml_hits:
        pkg = ns.rsplit(".", 1)[0]
        by_pkg[pkg] += 1
        if not any(ns.startswith(sp + ".") for sp in SCAN_PACKAGES):
            outside.append((ns, p))
        simple = ns.rsplit(".", 1)[-1]
        if simple not in java_types:
            missing.append((ns, p))

    print("== namespace 所属包分布 ==")
    for pkg, n in by_pkg.most_common():
        flag = "" if any(pkg == sp or pkg.startswith(sp + ".") for sp in SCAN_PACKAGES) else "   <== 越界"
        print(f"  {n:>4}  {pkg}{flag}")

    if outside:
        print(f"\n!! 越界 namespace {len(outside)} 个(不在 7 个 @MapperScan 包下):")
        for ns, p in outside:
            print(f"    {ns}\n        {os.path.relpath(p, CLOUD)}")
    else:
        print("\n✔ 全部 namespace 都落在 7 个 @MapperScan 包下")

    if missing:
        print(f"\n· 有 XML 无 Java 接口 {len(missing)} 个(老仓库自带的死 XML,不会影响启动):")
        for ns, p in missing:
            print(f"    {ns}\n        {os.path.relpath(p, CLOUD)}")

    return 0 if not outside else 1


if __name__ == "__main__":
    sys.exit(main())