mirror of
https://github.com/hpd840321/starRiverProperty.git
synced 2026-06-10 00:40:30 +08:00
7b2bd307f1
- backend/: 13 Maven modules (cw-elevator-application, cloudwalk-cloud, intelligent-cwoscomponent, ninca-crk, etc.) - frontend/: 4 Vue projects (elevator-front, cwos-portal, alarm-front, front_acs) + decompiled + scripts - scripts/: build, test-env, tools (Docker Compose, service templates, API parity) - docs/: AGENTS.md, superpowers specs, architecture docs - .gitignore: standard Java/Maven exclusions Moved from legacy maven-*/ root layout to backend/ organized structure.
341 lines
13 KiB
Python
341 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Full reactor audit: every *.java -> top-level FQN -> javap -p vs deployment jars/classes.
|
||
|
||
Usage:
|
||
python3 tools/deploy_javap_audit.py [--workers N] [--write-json PATH] [--write-markdown PATH]
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import zipfile
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||
|
||
REACTOR = Path(__file__).resolve().parents[1]
|
||
DEPLOY_ROOT = REACTOR.parents[1] / "部署包" / "ninca_common_component_organization_01-ninca_common_component_organization"
|
||
DEPLOY_UNPACK = DEPLOY_ROOT / "ninca-common-component-organization-V2.9.2_20210730"
|
||
DEPLOY_LIB = DEPLOY_UNPACK / "BOOT-INF" / "lib"
|
||
DEPLOY_CLASSES = DEPLOY_UNPACK / "BOOT-INF" / "classes"
|
||
|
||
DEPLOY_JARS = [
|
||
"cwos-component-organization-interface-v2.9.2_xinghewan.jar",
|
||
"cwos-component-organization-data-v2.9.2_xinghewan.jar",
|
||
"cwos-component-organization-service-v2.9.2_xinghewan.jar",
|
||
"cwos-component-organization-web-v2.9.2_xinghewan.jar",
|
||
]
|
||
|
||
|
||
def deploy_classpath() -> str:
|
||
jars = [str(DEPLOY_LIB / j) for j in DEPLOY_JARS]
|
||
return os.pathsep.join([str(DEPLOY_CLASSES)] + jars)
|
||
|
||
|
||
def local_classpath() -> str:
|
||
parts: List[str] = []
|
||
for mod in [
|
||
"cwos-component-organization-interface",
|
||
"cwos-component-organization-data",
|
||
"cwos-component-organization-service",
|
||
"cwos-component-organization-web",
|
||
"cwos-component-organization-starter",
|
||
]:
|
||
tc = REACTOR / mod / "target" / "classes"
|
||
if tc.is_dir():
|
||
parts.append(str(tc))
|
||
return os.pathsep.join(parts)
|
||
|
||
|
||
def collect_java_files(main_only: bool = True) -> List[Path]:
|
||
out: List[Path] = []
|
||
for root, _, files in os.walk(REACTOR):
|
||
r = root.replace("\\", "/")
|
||
if "/target/" in r:
|
||
continue
|
||
if main_only and "/src/main/java" not in r:
|
||
continue
|
||
for f in files:
|
||
if f.endswith(".java"):
|
||
out.append(Path(root) / f)
|
||
out.sort()
|
||
return out
|
||
|
||
|
||
def fqn_from_path(java_path: Path) -> Optional[str]:
|
||
try:
|
||
parts = java_path.resolve().relative_to(REACTOR).parts
|
||
except ValueError:
|
||
return None
|
||
anchor = None
|
||
for i in range(len(parts) - 1):
|
||
if parts[i : i + 3] == ("src", "main", "java"):
|
||
anchor = i + 3
|
||
break
|
||
if anchor is None:
|
||
return None
|
||
rel = parts[anchor:]
|
||
if not rel:
|
||
return None
|
||
stem = Path(rel[-1]).stem
|
||
pkg_parts = rel[:-1]
|
||
if stem == "package-info":
|
||
return ".".join(pkg_parts) + ".package-info"
|
||
return ".".join(pkg_parts) + "." + stem
|
||
|
||
|
||
def javap(cp: str, fqn: str) -> Tuple[int, str]:
|
||
try:
|
||
p = subprocess.run(
|
||
["javap", "-p", "-classpath", cp, fqn],
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=30,
|
||
)
|
||
err = (p.stderr or "").strip()
|
||
out = (p.stdout or "").strip()
|
||
if p.returncode != 0:
|
||
return p.returncode, err or out
|
||
return 0, out
|
||
except Exception as e:
|
||
return 99, str(e)
|
||
|
||
|
||
def normalize_javap(text: str) -> str:
|
||
lines = []
|
||
for line in text.replace("\r\n", "\n").split("\n"):
|
||
lines.append(line.rstrip())
|
||
return "\n".join(lines).strip() + "\n"
|
||
|
||
|
||
def outer_fqn_from_class_path(entry: str) -> Optional[str]:
|
||
"""BOOT-INF path cn/foo/Bar.class or cn/foo/Bar$1.class -> cn.foo.Bar"""
|
||
if not entry.endswith(".class") or "META-INF" in entry:
|
||
return None
|
||
if not entry.endswith(".class"):
|
||
return None
|
||
rel = entry.replace("/", ".")[: -len(".class")]
|
||
if "$" in rel:
|
||
rel = rel.split("$", 1)[0]
|
||
return rel
|
||
|
||
|
||
def deploy_outer_classes() -> Set[str]:
|
||
"""Union of outer-type FQNs present in deploy jars + BOOT-INF/classes."""
|
||
out: Set[str] = set()
|
||
if DEPLOY_CLASSES.is_dir():
|
||
for p in DEPLOY_CLASSES.rglob("*.class"):
|
||
rel = p.relative_to(DEPLOY_CLASSES).as_posix()
|
||
o = outer_fqn_from_class_path(rel)
|
||
if o:
|
||
out.add(o)
|
||
for jname in DEPLOY_JARS:
|
||
jpath = DEPLOY_LIB / jname
|
||
if not jpath.is_file():
|
||
continue
|
||
with zipfile.ZipFile(jpath, "r") as z:
|
||
for name in z.namelist():
|
||
o = outer_fqn_from_class_path(name)
|
||
if o:
|
||
out.add(o)
|
||
return out
|
||
|
||
|
||
def audit_one(
|
||
args: Tuple[str, str, str, str]
|
||
) -> Dict[str, Any]:
|
||
rel_file, fqn, d_cp, l_cp = args
|
||
rc_d, out_d = javap(d_cp, fqn)
|
||
rc_l, out_l = javap(l_cp, fqn)
|
||
row: Dict[str, Any] = {"file": rel_file, "fqn": fqn}
|
||
if rc_l != 0:
|
||
row["status"] = "MISSING_LOCAL_JAVAP"
|
||
row["note"] = (out_l or "")[:800]
|
||
return row
|
||
if rc_d != 0:
|
||
row["status"] = "MISSING_DEPLOY_JAVAP"
|
||
row["note"] = (out_d or "")[:800]
|
||
return row
|
||
nd, nl = normalize_javap(out_d), normalize_javap(out_l)
|
||
if nd == nl:
|
||
row["status"] = "IDENTICAL"
|
||
row["note"] = ""
|
||
else:
|
||
row["status"] = "DIFFERENT"
|
||
row["note"] = f"lines deploy={len(nd.splitlines())} local={len(nl.splitlines())}"
|
||
return row
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--workers", type=int, default=12)
|
||
ap.add_argument(
|
||
"--all-sources",
|
||
action="store_true",
|
||
help="Include src/test and other trees (deploy JAR will miss most tests).",
|
||
)
|
||
ap.add_argument("--write-json", type=Path, default=None)
|
||
ap.add_argument("--write-markdown", type=Path, default=None)
|
||
args = ap.parse_args()
|
||
|
||
if not DEPLOY_LIB.is_dir():
|
||
print("Deploy lib not found:", DEPLOY_LIB, file=sys.stderr)
|
||
return 2
|
||
|
||
d_cp = deploy_classpath()
|
||
l_cp = local_classpath()
|
||
java_list = collect_java_files(main_only=not args.all_sources)
|
||
|
||
tasks: List[Tuple[str, str, str, str]] = []
|
||
skipped = 0
|
||
for jp in java_list:
|
||
fqn = fqn_from_path(jp)
|
||
rel = str(jp.relative_to(REACTOR))
|
||
if not fqn:
|
||
skipped += 1
|
||
continue
|
||
tasks.append((rel, fqn, d_cp, l_cp))
|
||
|
||
rows: List[Dict[str, Any]] = []
|
||
for jp in java_list:
|
||
fqn = fqn_from_path(jp)
|
||
rel = str(jp.relative_to(REACTOR))
|
||
if not fqn:
|
||
rows.append(
|
||
{
|
||
"file": rel,
|
||
"fqn": None,
|
||
"status": "SKIP_PATH",
|
||
"note": "could not derive FQN",
|
||
}
|
||
)
|
||
|
||
with ThreadPoolExecutor(max_workers=max(1, args.workers)) as ex:
|
||
futs = {ex.submit(audit_one, t): t for t in tasks}
|
||
for fut in as_completed(futs):
|
||
rows.append(fut.result())
|
||
|
||
# preserve stable sort by file path
|
||
rows_sorted = sorted(rows, key=lambda r: (r.get("file") or ""))
|
||
|
||
summary_counts: Dict[str, int] = {}
|
||
for r in rows_sorted:
|
||
s = r.get("status") or "?"
|
||
summary_counts[s] = summary_counts.get(s, 0) + 1
|
||
|
||
source_fqns = {r["fqn"] for r in rows_sorted if r.get("fqn")}
|
||
deploy_outer = deploy_outer_classes()
|
||
only_deploy = sorted(deploy_outer - source_fqns)
|
||
|
||
summary = {
|
||
"reactor": str(REACTOR),
|
||
"deploy_unpack": str(DEPLOY_UNPACK),
|
||
"scope_main_java_only": not args.all_sources,
|
||
"java_files_total": len(java_list),
|
||
"skipped_non_java_path": skipped,
|
||
"status_counts": summary_counts,
|
||
"deploy_only_outer_types": len(only_deploy),
|
||
}
|
||
|
||
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
||
|
||
if args.write_json:
|
||
args.write_json.parent.mkdir(parents=True, exist_ok=True)
|
||
args.write_json.write_text(
|
||
json.dumps(
|
||
{"summary": summary, "rows": rows_sorted, "deploy_only_outer_fqns": only_deploy},
|
||
indent=2,
|
||
ensure_ascii=False,
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
print("Wrote JSON:", args.write_json)
|
||
|
||
if args.write_markdown:
|
||
write_markdown_report(
|
||
args.write_markdown,
|
||
summary,
|
||
rows_sorted,
|
||
only_deploy,
|
||
)
|
||
print("Wrote MD:", args.write_markdown)
|
||
|
||
return 0
|
||
|
||
|
||
def write_markdown_report(
|
||
path: Path,
|
||
summary: Dict[str, Any],
|
||
rows: List[Dict[str, Any]],
|
||
only_deploy: List[str],
|
||
) -> None:
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
lines: List[str] = []
|
||
lines.append("# 组织组件:全量 Java 文件 vs 部署包 javap 核对报告\n")
|
||
lines.append(f"- **反应堆:** `{summary['reactor']}`\n")
|
||
lines.append(f"- **部署解压:** `{summary['deploy_unpack']}`\n")
|
||
scope = "仅 `src/main/java`(与 Fat JAR 生产字节码对齐)" if summary.get("scope_main_java_only") else "含测试等全部 `.java`(`--all-sources`)"
|
||
lines.append(f"- **范围:** {scope}\n")
|
||
lines.append("- **核对方式:** 每个 `*.java` 推导顶层 FQN,`javap -p` 与部署 classpath / 本地 `target/classes` 对比。\n")
|
||
lines.append("\n## 1. 汇总\n\n")
|
||
lines.append("| 状态 | 数量 |\n|------|------|\n")
|
||
for k, v in sorted(summary["status_counts"].items()):
|
||
lines.append(f"| {k} | {v} |\n")
|
||
lines.append(f"\n- **部署包中存在、源码树无对应 `.java` 的顶层类型(估):** {summary['deploy_only_outer_types']} 个(见附录 B)。\n")
|
||
|
||
diff_rows = [r for r in rows if r.get("status") == "DIFFERENT"]
|
||
miss_d = [r for r in rows if r.get("status") == "MISSING_DEPLOY_JAVAP"]
|
||
miss_l = [r for r in rows if r.get("status") == "MISSING_LOCAL_JAVAP"]
|
||
|
||
lines.append("\n## 2. 解决方案与处置建议(按现象)\n\n")
|
||
lines.append("### 2.1 `DIFFERENT`(签名不一致)\n\n")
|
||
lines.append("- **含义:** 同 FQN 下,现场 JAR 与当前编译产物的 **字段/方法列表等对外形状** 不一致(含编译器生成的 synthetic/lambda 差异)。\n")
|
||
lines.append("- **建议:**\n")
|
||
lines.append(" 1. 以业务为准明确「权威版本」:若现场为基线,则 **检出与现场一致源码** 或在 CI 中 **与现场 JAR 做契约测试**;若仓库为权威,则 **升版发布** 替换现场。\n")
|
||
lines.append(" 2. 对差异类做 **关键路径回归**(人员/图库/设备同步 API)。\n")
|
||
lines.append(" 3. 对纯 lambda/synthetic 差异可辅以 **`javap -c` 抽样** 判断是否仅为编译差异。\n")
|
||
lines.append(f"\n**本仓 DIFFERENT 数量:** {len(diff_rows)}(完整列表见 JSON `rows` 或下表节选)。\n")
|
||
|
||
lines.append("\n### 2.2 `MISSING_DEPLOY_JAVAP`\n\n")
|
||
lines.append("- **含义:** 本地可 `javap`,部署 classpath 中 **找不到该类**(多为 **仓库新增类**,现场包尚未包含)。\n")
|
||
lines.append("- **建议:** 纳入发布变更说明,**部署新 Fat JAR** 或通过配置开关控制新功能。\n")
|
||
lines.append(f"\n**数量:** {len(miss_d)}\n")
|
||
|
||
lines.append("\n### 2.3 `MISSING_LOCAL_JAVAP`\n\n")
|
||
lines.append("- **含义:** 源码存在但 **未编译进 target/classes**(工程错误、条件编译、或 `package-info` 等特殊文件)。\n")
|
||
lines.append("- **建议:** `mvn clean compile`;检查模块归属;`package-info` 可忽略或单独标注。\n")
|
||
lines.append(f"\n**数量:** {len(miss_l)}\n")
|
||
|
||
lines.append("\n### 2.4 附录 B:部署侧多出类型\n\n")
|
||
lines.append("- **含义:** 现场 JAR 内含 **Starter 配置类、生成器、旧版独占类等**,当前仓库 **未以 `.java` 形式收录**(尤其 `cwos-component-organization-starter` 仅保留 `OrganizationServer`)。\n")
|
||
lines.append("- **建议:** 从现场 Fat JAR **反编译或回收历史分支** 补齐 Starter 与缺失资源(MyBatis XML、`component-org/messages*.properties`),使仓库可 **重现现场构建**。\n")
|
||
|
||
lines.append("\n## 3. DIFFERENT / MISSING 文件表(全量)\n\n")
|
||
lines.append("| 状态 | FQN | 文件 |\n|------|-----|------|\n")
|
||
for r in rows:
|
||
st = r.get("status", "")
|
||
if st in ("IDENTICAL", "SKIP_PATH"):
|
||
continue
|
||
fq = r.get("fqn") or ""
|
||
fn = r.get("file") or ""
|
||
lines.append(f"| {st} | `{fq}` | `{fn}` |\n")
|
||
|
||
lines.append("\n## 4. 附录 A:IDENTICAL 统计\n\n")
|
||
lines.append(f"- 与部署 `javap` **完全一致** 的顶层类数量:**{summary['status_counts'].get('IDENTICAL', 0)}**(明细见 JSON)。\n")
|
||
|
||
lines.append("\n## 5. 附录 B:部署包顶层类型(源码无同名 `.java` 路径推导)节选\n\n")
|
||
for fq in only_deploy[:200]:
|
||
lines.append(f"- `{fq}`\n")
|
||
if len(only_deploy) > 200:
|
||
lines.append(f"\n… 另有 {len(only_deploy) - 200} 项,见 JSON `deploy_only_outer_fqns`。\n")
|
||
|
||
path.write_text("".join(lines), encoding="utf-8")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|