mirror of
https://github.com/hpd840321/starRiverProperty.git
synced 2026-06-10 17:00:30 +08:00
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.
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user