p2_strip_dubbo.py 9.84 KB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
P2 第二步:把存量代码里的 Dubbo 依赖摘干净。

## 为什么可以机械替换

全仓 656 个文件 import 了 `com.alibaba.dubbo.*`,但**没有一个**是 RPC 用法
(`@Reference` / `@Service(com.alibaba.dubbo...)` 均为 0)——跨模块调用全部写在
31 个 `dubbo/*-consumer.xml` 里,靠 XML 装配。Java 侧用到的只有 Dubbo 自带的三个
工具类,实测调用点如下:

    com.alibaba.dubbo.common.utils.StringUtils     412 文件
        isNotEmpty 450 / isBlank 423 / isEmpty 311 / substringAfterLast 8 / isNotBlank 6 / isEquals 1
    com.alibaba.dubbo.common.utils.CollectionUtils 401 文件
        isNotEmpty 938 / isEmpty 684(老仓库实测口径;只有这两个方法)
    com.alibaba.dubbo.common.json.JSON              12 文件
        parse(String, Class) 17
    com.alibaba.dubbo.common.json.ParseException     8 文件(JSON.parse 的受检异常)
    com.alibaba.dubbo.rpc.RpcException               4 文件(catch 分支,Dubbo 专用)
    com.alibaba.dubbo.rpc.Result                     1 文件(未使用的 import)

替换目标选的都是**语义等价**的标准库,不是随便挑的:

  - StringUtils  → org.apache.commons.lang3.StringUtils
      isEmpty/isNotEmpty/isBlank/isNotBlank/substringAfterLast 方法名与语义完全一致;
      dubbo 的 isEquals 在 commons-lang3 里叫 equals(仅 1 处,单独改)。
  - CollectionUtils → org.apache.commons.collections4.CollectionUtils
      ⚠️ **不能选 Spring 那份**(第一版就是错在这里,见下)。
      Dubbo 的 CollectionUtils 是从 commons-collections 抄的,`isEmpty` 与
      `isNotEmpty` 都有;而 `org.springframework.util.CollectionUtils`
      **只有 `isEmpty`,没有 `isNotEmpty`**——从 3.1 到 7.0 的每一个版本都没有
      (已用 javap 逐个核对 ~/.m2 里的 38 个 spring-core)。存量代码里
      `isNotEmpty` 有 938 处调用,换到 Spring 那份必然编译不过。
      选 commons-collections4 是因为它与 Dubbo 那份**同源**,
      实测调用点只有 `isEmpty` / `isNotEmpty` 两个方法,语义完全一致。
  - JSON.parse(s, Class) → com.alibaba.fastjson.JSON.parseObject(s, Class)
      dubbo 的 JSON 本来就是 fastjson 的一层包装,parse 内部就是 parseObject。
  - ParseException → com.alibaba.fastjson.JSONException
      ⚠️ 关键:dubbo 的 ParseException 是**受检异常**,fastjson 的是 RuntimeException。
      直接把 import 换掉而不动 catch 会编译不过(checked 异常未被抛出)。
      换成 JSONException 后进入 catch 的条件与原来完全一致(都是"解析失败"),
      语义不变,只是不再需要 try 里显式声明。
  - RpcException → 整个 catch 分支删除
      Dubbo 没了,这个类型不可能再出现;本来的 `catch(Exception)` 分支会接住同样的异常。
      ⚠️ 3 个 MQ 监听器里原分支是 `throw e` 让 MQ 重投,删掉后异常改由后面的
      `catch(Exception)` 吞掉(不重投)。这是丢 Dubbo 的必然结果,已记录在
      docs/P2-并入清单核对.md,需回归确认重投语义是否有业务依赖。

## 幂等

全部是"找不到就不改",可重复执行。
"""

import os
import re
import sys

ROOT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
DIRS = [
    "parking-module-api",
    "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",
]

stats = {}


def bump(k, n=1):
    stats[k] = stats.get(k, 0) + n


def java_files():
    for d in DIRS:
        base = os.path.join(ROOT, d)
        for root, _, files in os.walk(base):
            for f in files:
                if f.endswith(".java"):
                    yield os.path.join(root, f)


# ---------------------------------------------------------------------------
# 规则:(说明, 正则, 替换, 计数键)
# ---------------------------------------------------------------------------
RULES = [
    # E1 工具类
    (
        "StringUtils → commons-lang3",
        re.compile(r"^import com\.alibaba\.dubbo\.common\.utils\.StringUtils;\s*$", re.M),
        "import org.apache.commons.lang3.StringUtils;",
        "stringutils",
    ),
    (
        "CollectionUtils → commons-collections4",
        re.compile(r"^import com\.alibaba\.dubbo\.common\.utils\.CollectionUtils;\s*$", re.M),
        "import org.apache.commons.collections4.CollectionUtils;",
        "collectionutils",
    ),
    # E2 JSON
    (
        "dubbo JSON → fastjson JSON",
        re.compile(r"^import com\.alibaba\.dubbo\.common\.json\.JSON;\s*$", re.M),
        "import com.alibaba.fastjson.JSON;",
        "json-import",
    ),
    (
        "全限定 dubbo JSON.parse → fastjson",
        re.compile(r"com\.alibaba\.dubbo\.common\.json\.JSON\.parse\("),
        "com.alibaba.fastjson.JSON.parseObject(",
        "json-fqn-parse",
    ),
    # E3 ParseException
    (
        "dubbo ParseException → fastjson JSONException",
        re.compile(r"^import com\.alibaba\.dubbo\.common\.json\.ParseException;\s*$", re.M),
        "import com.alibaba.fastjson.JSONException;",
        "parseexc-import",
    ),
    # E5 无用 import
    (
        "删除 dubbo rpc.Result import",
        re.compile(r"^import com\.alibaba\.dubbo\.rpc\.Result;\s*$\n?", re.M),
        "",
        "result-import",
    ),
]

# 需要额外改名/删分支的定点改动
TEXT_RULES = [
    # ParseException 类型名 → JSONException(配合上面的 import 替换)
    # ⚠️ 不能全局替换 `ParseException`:有些文件同时 import 了 java.text.ParseException
    # 并在别处用。只处理"导入的是 dubbo 那份"的文件,见 apply_parse_exception()。
]

# ⚠️ 这几个正则**不能吞掉前面的 `}`**——那个 `}` 是 try / if 的收尾,
# 吞掉就变成语法错误。只删 `catch(...) { ... }` 本身,后面紧跟的 `catch`/`else` 自然接上。
#
#   try {  ...  }catch (RpcException e) { log(); throw e; }catch(Exception e){
#                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 只删这一段
#     → try {  ...  }catch(Exception e){          ✅ 结构不变
#
# 分支体实测都不含花括号(只有 log/赋值/throw),所以用 [^{}]* 足够。
RPC_CATCH = re.compile(r"[ \t]*catch\s*\(\s*RpcException\s+\w+\s*\)\s*\{(?:[^{}])*\}[ \t]*", re.M)
RPC_INSTANCEOF = re.compile(
    r"[ \t]*else\s+if\s*\(\s*\w+\s+instanceof\s+RpcException\s*\)\s*\{(?:[^{}])*\}[ \t]*",
    re.M,
)


def apply(path):
    src = open(path, encoding="utf-8", errors="surrogateescape").read()
    orig = src
    touched = []

    for desc, pat, repl, key in RULES:
        new, n = pat.subn(repl, src)
        if n:
            # 去重:替换后若与已有 import 重复,则删掉新加的那行
            src = dedupe_import(new, repl, key)
            bump(key, n)
            touched.append(f"{desc}x{n}")

    # --- E1 特例:StringUtils.isEquals → equals ---
    if "com.alibaba.dubbo.common.utils.StringUtils" in orig or "org.apache.commons.lang3.StringUtils" in src:
        new, n = re.subn(r"\bStringUtils\.isEquals\(", "StringUtils.equals(", src)
        if n:
            src = new
            bump("stringutils-isequals", n)
            touched.append(f"StringUtils.isEquals→equals x{n}")

    # --- E3 特例:把 dubbo ParseException 的类型名改成 JSONException ---
    # ⚠️ 必须确认该文件的 ParseException 就是 dubbo 那一份:
    #    极少数文件同时 import 了 java.text.ParseException 并在别处使用,改名会误伤。
    if "import com.alibaba.dubbo.common.json.ParseException;" in orig:
        if "import java.text.ParseException;" in orig:
            raise SystemExit(f"!! {path}: 同时导入了 dubbo 与 java.text 两个 ParseException,需人工处理")
        src, n = re.subn(r"\bParseException\b", "JSONException", src)
        if n:
            bump("parseexc-rename", n)
            touched.append(f"ParseException→JSONException x{n}")

    # --- E2 特例:dubbo JSON 只有 parse(s, Class),fastjson 对应 parseObject ---
    if "import com.alibaba.dubbo.common.json.JSON;" in orig:
        src, n = re.subn(r"\bJSON\.parse\(", "JSON.parseObject(", src)
        if n:
            bump("json-parse→parseobject", n)
            touched.append(f"JSON.parse→JSON.parseObject x{n}")

    # --- E4 RpcException catch 分支 ---
    if "RpcException" in src:
        src, n1 = RPC_CATCH.subn("", src)
        src, n2 = RPC_INSTANCEOF.subn("", src)
        src, n3 = re.subn(r"^import com\.alibaba\.dubbo\.rpc\.RpcException;\s*$\n?", "", src, flags=re.M)
        if n1 or n2 or n3:
            bump("rpcexception", n1 + n2 + n3)
            touched.append(f"RpcException 分支删除 x{n1+n2+n3}")

    if src != orig:
        open(path, "w", encoding="utf-8", errors="surrogateescape").write(src)
        return touched
    return None


def dedupe_import(src, new_import, key):
    """如果 new_import 在文件里出现 >=2 次,说明原本就有同款 import,保留一处。"""
    lines = src.split("\n")
    seen = 0
    out = []
    for ln in lines:
        if ln.strip() == new_import.strip():
            seen += 1
            if seen > 1:
                continue  # 丢掉后出现的那一条(= 我们刚插入的)
        out.append(ln)
    return "\n".join(out)


def main():
    changed = 0
    for p in java_files():
        r = apply(p)
        if r:
            changed += 1
            print(f"  {os.path.relpath(p, ROOT)}")
            for t in r:
                print(f"      · {t}")
    print(f"\n改动文件 {changed} 个")
    print("规则命中统计:")
    for k, v in sorted(stats.items(), key=lambda x: -x[1]):
        print(f"  {k:<24} {v}")


if __name__ == "__main__":
    main()