Files
starRiverProperty/backend/ninca-common-component-organization/tools/deploy_javap_audit.py
T
hpd840321 7b2bd307f1 Initial commit: reorganized source tree
- 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.
2026-05-09 09:56:45 +08:00

341 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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. 附录 AIDENTICAL 统计\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())