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:
hpd840321
2026-05-09 09:00:12 +08:00
commit 7b2bd307f1
7260 changed files with 612980 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Heuristic static checks aligned with common 阿里巴巴手册 themes (not full P3C).
Outputs markdown to stdout or --out file.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
TAB = re.compile(r"^\t+")
TRAIL = re.compile(r"[ \t]+$")
LONG_LINE = re.compile(r".{121,}")
SYSTEM_OUT = re.compile(r"\bSystem\.(out|err)\.(print|println)\s*\(")
EMPTY_CATCH = re.compile(r"catch\s*\([^)]+\)\s*\{\s*\}", re.MULTILINE)
CATCH_EXCEPTION_PASS = re.compile(
r"catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}", re.MULTILINE
)
TODO_FXXX = re.compile(r"\b(FIXME|XXX)\b", re.IGNORECASE)
def scan_file(path: Path, rel: str) -> list[tuple[str, int, str]]:
issues: list[tuple[str, int, str]] = []
try:
text = path.read_text(encoding="utf-8", errors="replace")
except OSError as e:
return [("ERROR", 0, f"{rel}: {e}")]
lines = text.splitlines()
for i, line in enumerate(lines, 1):
if TAB.search(line):
issues.append(("FORMAT", i, "行首含 TAB,建议仅用空格缩进(手册排版/可读)"))
if TRAIL.search(line):
issues.append(("FORMAT", i, "行尾多余空白"))
if LONG_LINE.search(line):
issues.append(("FORMAT", i, "行宽超过约 120 字符,建议换行(团队规约常见上限)"))
if SYSTEM_OUT.search(line) and "/test/" not in str(path).replace("\\", "/"):
issues.append(("STYLE", i, "使用 System.out/err,生产代码建议改用日志框架"))
if TODO_FXXX.search(line):
issues.append(("STYLE", i, "含 FIXME/XXX 标记,发版前应清理或跟踪"))
body = "\n".join(lines)
if EMPTY_CATCH.search(body):
issues.append(("RISK", 0, "存在空 catch 块 {},手册建议至少记录或处理异常"))
if CATCH_EXCEPTION_PASS.search(body):
issues.append(
("RISK", 0, "存在 catch (Exception) { } 空实现,建议细化异常类型或记录日志")
)
return issues
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument(
"--repo",
type=Path,
default=Path(__file__).resolve().parent.parent,
help="Repo root containing maven-*",
)
ap.add_argument("--out", type=Path, default=None, help="Write markdown report")
args = ap.parse_args()
repo: Path = args.repo
roots = sorted(p for p in repo.glob("maven-*") if p.is_dir())
lines_out: list[str] = []
lines_out.append("# 阿里巴巴手册相关:启发式静态扫描\n")
lines_out.append(
"> 非 P3C 全量规则;仅覆盖 **TAB/行尾空白/超长行/System.out/空 catch/FIXME** 等易自动化项。\n"
)
grand = 0
for root in roots:
name = root.name
lines_out.append(f"\n## `{name}`\n")
mod_issues = 0
for java in sorted(root.rglob("*.java")):
sp = str(java).replace("\\", "/")
if "/target/" in sp:
continue
rel = java.relative_to(repo)
found = scan_file(java, str(rel))
if not found:
continue
mod_issues += len(found)
lines_out.append(f"\n### `{rel}`\n")
lines_out.append("| 级别 | 行 | 说明 |\n|------|----|------|\n")
for sev, ln, msg in found[:30]:
lines_out.append(f"| {sev} | {ln or '-'} | {msg} |\n")
if len(found) > 30:
lines_out.append(f"\n… 另有 {len(found) - 30} 条(同文件省略)\n")
if mod_issues == 0:
lines_out.append("\n(本模块未发现上述启发式命中项)\n")
grand += mod_issues
lines_out.append(f"\n---\n\n**启发式命中条数(含重复行规则)**:{grand}\n")
text = "".join(lines_out)
if args.out:
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(text, encoding="utf-8")
print(args.out, file=sys.stderr)
else:
sys.stdout.write(text)
return 0
if __name__ == "__main__":
sys.exit(main())
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# V1 基准与当前构建 fat-jar 的嵌套 lib 坐标 multiset 门禁。见 scripts/generate_v1_v2_elevator_dependency_diff.py --help
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
exec python3 "$ROOT/scripts/generate_v1_v2_elevator_dependency_diff.py" --gate "$@"
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env bash
set -euo pipefail
# 生产只读证据采集(Maven 发布包内与本脚本同置于部署根目录,与 start.sh / properties 同层):
# - 进程参数/环境/工作目录
# - 本地配置文件
# - jcmd system properties
# - 应用日志关键片段
# - Consul 健康与 KV 快照
# 最终输出 tar.gz,便于离线定位“配置来源 -> Ribbon 实例列表”问题。
APP_DIR="${1:-/data/cwos/cw-elevator-application-V1.0.0.20211103}"
CONSUL_ADDR="${2:-10.0.22.102:8500}"
OUT_ROOT="${3:-${APP_DIR}/evidence}"
APP_NAME="${4:-elevator-app}"
# 现场 JDK(生产 cwos-node 固定路径;不依赖 PATH
CWOS_JAVA_BIN="/data/cwos/java/bin"
JAVA_BIN="${CWOS_JAVA_BIN}/java"
JAR_BIN="${CWOS_JAVA_BIN}/jar"
JCMD_BIN="${CWOS_JAVA_BIN}/jcmd"
DATE_BIN="/bin/date"
timestamp="$(${DATE_BIN} +%Y%m%d-%H%M%S)"
OUT_DIR="${OUT_ROOT}/elevator-evidence-${timestamp}"
mkdir -p "${OUT_DIR}"
log() { echo "[collect] $*"; }
log "APP_DIR=${APP_DIR}"
log "CONSUL_ADDR=${CONSUL_ADDR}"
log "OUT_DIR=${OUT_DIR}"
log "JAVA_BIN=${JAVA_BIN} JAR_BIN=${JAR_BIN} JCMD_BIN=${JCMD_BIN}"
PID="$(ps -ef | awk '/java/ && /cw-elevator-application/ && !/awk/ {print $2; exit}')"
if [[ -z "${PID}" ]]; then
echo "ERROR: 未找到 cw-elevator-application Java 进程" >&2
exit 1
fi
log "PID=${PID}"
echo "${PID}" > "${OUT_DIR}/pid.txt"
# 1) 进程与系统基础信息
ps -ef > "${OUT_DIR}/ps-ef.txt"
uname -a > "${OUT_DIR}/uname.txt"
${DATE_BIN} +%Y-%m-%dT%H:%M:%S%z > "${OUT_DIR}/collected-at.txt"
tr '\0' ' ' < "/proc/${PID}/cmdline" > "${OUT_DIR}/proc-cmdline.txt" || true
tr '\0' '\n' < "/proc/${PID}/environ" > "${OUT_DIR}/proc-environ.txt" || true
ls -l "/proc/${PID}/cwd" > "${OUT_DIR}/proc-cwd.txt" || true
# 2) 本地配置快照(若存在)
for f in bootstrap.properties application.properties application-access-control.properties start.sh stop.sh cw-elevator-application.service; do
if [[ -f "${APP_DIR}/${f}" ]]; then
cp -a "${APP_DIR}/${f}" "${OUT_DIR}/${f}"
fi
done
# 3) JAR 与结构快照
JAR_PATH="$(awk '{print $1}' "${OUT_DIR}/proc-cmdline.txt" | sed 's/[[:space:]]*$//')"
if [[ -f "${APP_DIR}/cw-elevator-application-V1.0.0.20211103.jar" ]]; then
JAR_PATH="${APP_DIR}/cw-elevator-application-V1.0.0.20211103.jar"
fi
echo "${JAR_PATH}" > "${OUT_DIR}/jar-path.txt"
if [[ -f "${JAR_PATH}" ]]; then
sha256sum "${JAR_PATH}" > "${OUT_DIR}/jar.sha256.txt" || true
if [[ -x "${JAR_BIN}" ]]; then
"${JAR_BIN}" tf "${JAR_PATH}" > "${OUT_DIR}/jar-tf.txt" || true
else
echo "jar not found or not executable: ${JAR_BIN}" > "${OUT_DIR}/jar-tf.txt"
fi
unzip -p "${JAR_PATH}" application.properties > "${OUT_DIR}/jar-application.properties.txt" 2>/dev/null || true
unzip -p "${JAR_PATH}" bootstrap.properties > "${OUT_DIR}/jar-bootstrap.properties.txt" 2>/dev/null || true
fi
# 4) jcmd system properties + attach 诊断(不修改应用配置;便于修复 AttachNotSupportedException
{
echo "=== current shell user ==="
id 2>/dev/null || true
echo "=== target java process ==="
ps -o user=,group=,pid=,args= -p "${PID}" 2>/dev/null || true
PROC_USER="$(stat -c '%U' "/proc/${PID}" 2>/dev/null || echo "")"
PROC_UID="$(stat -c '%u' "/proc/${PID}" 2>/dev/null || echo "")"
echo "proc_owner=${PROC_USER} uid=${PROC_UID}"
echo "=== /tmp hsperfdata (HotSpot perf counter; attach 相关) ==="
if [[ -n "${PROC_USER}" && "${PROC_USER}" != "unknown" ]]; then
HS="/tmp/hsperfdata_${PROC_USER}"
if [[ -d "${HS}" ]]; then
ls -la "${HS}" 2>/dev/null | head -30 || true
ls -la "${HS}/${PID}" 2>/dev/null || echo "missing ${HS}/${PID}"
else
echo "no directory ${HS}"
fi
fi
echo "=== cmdline tokens (attach / jdwp) ==="
tr '\0' '\n' < "/proc/${PID}/cmdline" 2>/dev/null | grep -E 'DisableAttach|Attach|jdwp|agentpath' || echo "(none matched)"
} > "${OUT_DIR}/jcmd-attach-diagnose.txt" 2>&1
if [[ -x "${JCMD_BIN}" ]]; then
"${JCMD_BIN}" "${PID}" VM.system_properties > "${OUT_DIR}/jcmd-system-properties.txt" 2>&1 || true
if grep -q 'AttachNotSupportedException\|Unable to open socket file' "${OUT_DIR}/jcmd-system-properties.txt" 2>/dev/null; then
{
echo ""
echo "HINT: jcmd attach 失败常见原因:"
echo " 1) 与 Java 进程不同用户执行 jcmd(请用与进程相同用户,例如: sudo -u <java_user> ${JCMD_BIN} ${PID} VM.system_properties"
echo " 2) /tmp/hsperfdata_<user>/<pid> 缺失或权限异常"
echo " 3) JVM 启动参数含 -XX:+DisableAttachMechanism(见 jcmd-attach-diagnose.txt 中 cmdline"
echo " 4) 进程非 HotSpot 或尚未完全初始化(极少见于长期运行的 Spring Boot"
} >> "${OUT_DIR}/jcmd-system-properties.txt"
fi
else
echo "jcmd not found or not executable: ${JCMD_BIN}" > "${OUT_DIR}/jcmd-system-properties.txt"
fi
# 4b) java 版本(与现场 JDK 一致性的旁证)
if [[ -x "${JAVA_BIN}" ]]; then
"${JAVA_BIN}" -version > "${OUT_DIR}/java-version.txt" 2>&1 || true
else
echo "java not found or not executable: ${JAVA_BIN}" > "${OUT_DIR}/java-version.txt"
fi
# 5) 应用日志关键行
LOG_FILE="${APP_DIR}/logs/elevator-app.log"
if [[ -f "${LOG_FILE}" ]]; then
cp -a "${LOG_FILE}" "${OUT_DIR}/elevator-app.log.full"
awk '
/CONFIG SOURCE PROBE START|CONFIG SOURCE PROBE END|probe key=|ConfigurationBasedServerList|Load balancer does not have available server|DynamicServerListLoadBalancer|ConsulServiceRegistry|Registering service with consul/ { print }
' "${LOG_FILE}" > "${OUT_DIR}/elevator-app.log.keylines.txt"
fi
# 6) Consul 快照
CURL="curl -sS --max-time 8"
${CURL} "http://${CONSUL_ADDR}/v1/health/service/${APP_NAME}?passing=true" > "${OUT_DIR}/consul-health-${APP_NAME}.json" || true
for svc in cwos-portal ninca-common ninca-common-component-organization ninca-crk-std cloudwalk-device-thirdparty; do
${CURL} "http://${CONSUL_ADDR}/v1/health/service/${svc}?passing=true" > "${OUT_DIR}/consul-health-${svc}.json" || true
done
${CURL} "http://${CONSUL_ADDR}/v1/kv/config/${APP_NAME}/data?raw" > "${OUT_DIR}/consul-kv-${APP_NAME}.properties" || true
${CURL} "http://${CONSUL_ADDR}/v1/kv/config/${APP_NAME},access-control/data?raw" > "${OUT_DIR}/consul-kv-${APP_NAME},access-control.properties" || true
# 7) 现场可达性快照(已知主机名)
for host in 0837a70b5fab47569391828f5feb2561 371bfca4972c43d2aefcf302d0a4a277 44700995ee904679a7ad5afddcf93bb5; do
getent hosts "${host}" > "${OUT_DIR}/getent-${host}.txt" 2>&1 || true
curl -I --max-time 5 "http://${host}:8089/" > "${OUT_DIR}/curl-head-${host}-8089.txt" 2>&1 || true
done
ARCHIVE="${OUT_DIR}.tar.gz"
tar -czf "${ARCHIVE}" -C "$(dirname "${OUT_DIR}")" "$(basename "${OUT_DIR}")"
log "DONE archive=${ARCHIVE}"
echo "${ARCHIVE}"
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Compare a JAR's .class entries to decompiled .java tree (Maven src/main/java)."""
from __future__ import annotations
import argparse
import sys
import zipfile
from pathlib import Path
def jar_class_entries(jar_path: Path) -> set[str]:
"""FQN-like names for each .class (uses /), inner as Outer$Inner."""
out: set[str] = set()
with zipfile.ZipFile(jar_path, "r") as zf:
for name in zf.namelist():
if not name.endswith(".class") or "META-INF" in name:
continue
rel = name[:-6] # drop .class
out.add(rel.replace("/", "."))
return out
def java_fqns(src_root: Path) -> set[str]:
"""Best-effort FQN from path .../src/main/java/a/b/C.java -> a.b.C."""
src_root = src_root.resolve()
java_root = src_root / "src/main/java"
if not java_root.is_dir():
if src_root.name == "java" and (src_root.parent / "main").exists():
java_root = src_root
else:
return set()
fqns: set[str] = set()
for f in java_root.rglob("*.java"):
rel = f.relative_to(java_root).with_suffix("")
fqns.add(".".join(rel.parts))
return fqns
def outer_java_for_class(fqn: str, java_fqns: set[str]) -> str | None:
"""Map class FQN to expected .java outer name (strip $ inner)."""
if "$" in fqn:
outer = fqn.split("$", 1)[0]
else:
outer = fqn
if outer in java_fqns:
return outer
return None
def analyze(jar: Path, src_module_roots: list[Path]) -> dict:
classes = jar_class_entries(jar)
all_java: set[str] = set()
for root in src_module_roots:
if root.is_dir():
all_java |= java_fqns(root)
missing_outer: list[str] = []
for c in sorted(classes):
if c.startswith("module-info"):
continue
if outer_java_for_class(c, all_java) is None:
missing_outer.append(c)
# "extra" java: outer types not present as any class in jar (rough)
jar_outers = {x.split("$", 1)[0] for x in classes if not x.startswith("module-info")}
extra_java = sorted(x for x in all_java if x not in jar_outers and "$" not in x)
return {
"jar": str(jar),
"class_count": len(classes),
"java_outer_count": len(all_java),
"missing_classes": missing_outer,
"possibly_extra_sources": extra_java[:200], # cap
"possibly_extra_sources_total": len(extra_java),
}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("jar", type=Path)
ap.add_argument("src_roots", nargs="+", type=Path, help="Module dirs containing src/main/java")
args = ap.parse_args()
r = analyze(args.jar, args.src_roots)
print(f"JAR: {r['jar']}")
print(f"Classes in JAR: {r['class_count']}; outer .java types under src/main/java: {r['java_outer_count']}")
miss = r["missing_classes"]
print(f"Missing (no matching outer .java): {len(miss)}")
for line in miss[:80]:
print(f" - {line}")
if len(miss) > 80:
print(f" ... and {len(miss) - 80} more")
ex = r["possibly_extra_sources"]
tot = r["possibly_extra_sources_total"]
print(f"Possibly extra .java (not as outer in JAR): {tot} (showing up to {len(ex)})")
for line in ex[:40]:
print(f" + {line}")
if tot > 40:
print(f" ...")
return 1 if miss else 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
从 V1 运行包目录读取 lib/ 与 cw_lib/ 的 JAR 清单,并与 V2 Maven 反应堆
`cw-elevator-application-reactor/pom.xml` 中显式声明的版本属性做对照。
用法(在仓库根目录):
python3 scripts/compare_v1_v2_elevator_dependencies.py
可选环境变量:
V1_ROOT 默认: cw-elevator-application-V1.0.0.20211103
REPO_ROOT 默认: 脚本所在目录的上一级(仓库根)
"""
from __future__ import annotations
import os
import re
import sys
from collections import defaultdict
from pathlib import Path
def repo_root() -> Path:
return Path(__file__).resolve().parents[1]
def list_jars(d: Path) -> list[str]:
if not d.is_dir():
return []
return sorted(p.name for p in d.glob("*.jar"))
def jar_stem_versions(jars: list[str]) -> dict[str, list[str]]:
"""artifactId -> [version-ish suffixes from filename]."""
by_artifact: dict[str, list[str]] = defaultdict(list)
for name in jars:
if not name.endswith(".jar"):
continue
base = name[:-4]
m = re.match(r"^(.+)-(\d[\d.\w-]*)$", base)
if m:
by_artifact[m.group(1)].append(m.group(2))
else:
by_artifact[base].append("")
return dict(by_artifact)
def parse_pom_properties(pom: Path) -> dict[str, str]:
text = pom.read_text(encoding="utf-8", errors="replace")
m = re.search(r"<properties>(.*?)</properties>", text, re.DOTALL)
if not m:
return {}
block = m.group(1)
props = {}
for mm in re.finditer(r"<([a-zA-Z0-9_.-]+)>\s*([^<]+?)\s*</\1>", block):
k, v = mm.group(1), mm.group(2).strip()
if k and v and "${" not in v:
props[k] = v
return props
def main() -> int:
root = Path(os.environ.get("REPO_ROOT", repo_root()))
v1 = root / os.environ.get(
"V1_ROOT", "cw-elevator-application-V1.0.0.20211103"
)
lib = v1 / "lib"
cw = v1 / "cw_lib"
pom = root / "maven-cw-elevator-application" / "pom.xml"
if not pom.is_file():
print("missing", pom, file=sys.stderr)
return 2
jars_lib = list_jars(lib)
jars_cw = list_jars(cw)
props = parse_pom_properties(pom)
print("## V1 目录\n")
print(f"- lib: {lib} {len(jars_lib)} 个 JAR")
print(f"- cw_lib: {cw} {len(jars_cw)} 个 JAR\n")
print("## V2 反应堆版本属性(节选,与 cw_lib 对齐项)\n")
keys = [
"cloudwalk.internal.version",
"cloudwalk.legacy.public.version",
"intelligent.cwoscomponent.version",
"davinci.manager.storage.version",
"intelligent.lock.version",
"cwos.sdk.resource.version",
"cwos.sdk.event.version",
"spring-cloud.version",
]
for k in keys:
if k in props:
print(f"- `{k}` → **{props[k]}**")
dup = {a: vs for a, vs in jar_stem_versions(jars_lib).items() if len(vs) > 1}
print("\n## V1 lib 中「同 artifact 前缀、多版本」示例( Classpath 冲突风险)\n")
shown = 0
for art in sorted(dup.keys()):
vers = sorted(set(dup[art]))
if len(vers) <= 1:
continue
print(f"- `{art}`: {', '.join(vers)}")
shown += 1
if shown >= 25:
print("- …(其余略,可对本脚本输出重定向后全文检索)")
break
print("\n## 与 cw_lib 文件名逐字对照(V2 POM 显式坐标)\n")
mapping = [
("cloudwalk-common-event", "cloudwalk.internal.version"),
("cloudwalk-common-result", "cloudwalk.legacy.public.version"),
("cloudwalk-common-serial", "cloudwalk.legacy.public.version"),
("cloudwalk-common-service", "cloudwalk.internal.version"),
("cloudwalk-common-web", "cloudwalk.legacy.public.version"),
("cloudwalk-intelligent-component-lock", "intelligent.lock.version"),
("davinci-manager-storage", "davinci.manager.storage.version"),
("intelligent-cwoscomponent-rest", "intelligent.cwoscomponent.version"),
("intelligent-cwoscomponent-interface", "intelligent.cwoscomponent.version"),
("cwos-java-sdk-resource", "cwos.sdk.resource.version"),
("cwos-sdk-event", "cwos.sdk.event.version"),
]
cw_set = set(jars_cw)
for prefix, prop in mapping:
ver = props.get(prop, "?")
expect = f"{prefix}-{ver}.jar"
hit = expect if expect in cw_set else None
if not hit:
# SNAPSHOT / classifier 简判:取 cw_lib 中以 prefix- 开头的文件
cand = [j for j in jars_cw if j.startswith(prefix + "-")]
cand_s = cand[0] if len(cand) == 1 else str(cand)
print(f"- **{prefix}**: V2 属性 `{prop}`=`{ver}` → 期望 `{expect}`V1 cw_lib 实际: `{cand_s}`")
else:
print(f"- **{prefix}**: V1=`{hit}`V2 属性 `{prop}`=`{ver}` ✓")
print("\n## 电梯自研模块 JAR 版本\n")
print(
"- V1 cw_lib: `cw-elevator-application-*-**1.0-SNAPSHOT**.jar`(四模块)\n"
"- V2 Maven: `cn.cloudwalk.elevator:*:**2.0-SNAPSHOT**`(反应堆版本,与 1.0 并存为正常升级)"
)
print("\n完整清单文件: `docs/architecture/data/v1-elevator-lib-jars.txt` 与 `v1-elevator-cw-lib-jars.txt`。")
print(
"\n生成 **V2 全量传递依赖树** 请在可解析私服的环境执行:\n"
" cd maven-cw-elevator-application && "
"mvn -pl cw-elevator-application-starter -am dependency:tree "
"-DoutputFile=target/v2-starter-dependency-tree.txt"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,269 @@
#!/usr/bin/env python3
"""
Extract listed ninca-crk-* lib jars from fat jar, CFR-decompile each into maven-ninca-crk-from-lib/<artifactId>/,
emit parent + child POMs (embedded pom from jar when present, sanitized parent).
"""
from __future__ import annotations
import os
import re
import shutil
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
FAT_JAR_DEFAULT = Path(
"/media/zebra/9e8fa357-7db6-4d70-88ed-d5de5a059a663/"
"星河湾星中星/星中心/ninca_crk_std_01-ninca_crk_std_backend/ninca-crk-std-backend-V2.9.2_20210730.jar"
)
REPO_ROOT = Path(__file__).resolve().parents[1]
OUT_ROOT = REPO_ROOT / "maven-ninca-crk-from-lib"
CFR_JAR = Path("/tmp/cfr-0.152.jar")
TARGET_NAMES = [
"ninca-crk-access-control-biz-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-common-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-data-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-facade-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-interface-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-service-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-access-control-web-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-biz-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-common-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-data-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-facade-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-interface-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-service-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-conference-attendance-web-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-biz-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-common-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-data-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-facade-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-interface-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-service-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-smart-attendance-web-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-biz-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-common-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-data-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-facade-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-interface-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-service-2.9.1_210630-SNAPSHOT.jar",
"ninca-crk-visitor-management-web-2.9.1_210630-SNAPSHOT.jar",
]
GROUP_ID = "cn.cloudwalk.ninca"
REACTOR_VERSION = "2.9.1_210630-SNAPSHOT"
JAVA_VERSION = "1.8"
def artifact_dir(jar_name: str) -> str:
return jar_name.replace(".jar", "").replace("-2.9.1_210630-SNAPSHOT", "")
def ensure_cfr() -> Path:
if CFR_JAR.is_file():
return CFR_JAR
raise SystemExit(
"Missing CFR at {} — download:\n"
" curl -fsSL -o {} "
"https://github.com/leibnitz27/cfr/releases/download/0.152/cfr-0.152.jar".format(CFR_JAR, CFR_JAR)
)
def extract_embedded_pom(zf: zipfile.ZipFile, inner_path: str) -> str | None:
try:
return zf.read(inner_path).decode("utf-8", errors="replace")
except KeyError:
return None
def find_embedded_pom_xml(zf: zipfile.ZipFile) -> tuple[str | None, str | None]:
"""Return (pom_xml_content, relative_path_in_zip)."""
for n in zf.namelist():
if n.endswith("/pom.xml") and "/META-INF/maven/" in n and "/ninca-crk-" in n:
return extract_embedded_pom(zf, n), n
return None, None
def sanitize_pom_xml(raw: str, artifact_id: str) -> str:
"""Remove embedded parent; attach reactor parent after modelVersion."""
raw = raw.lstrip("\ufeff").strip()
raw = re.sub(r"<parent\b[^>]*>[\s\S]*?</parent>\s*", "", raw, count=1, flags=re.I)
parent_block = (
" <parent>\n"
" <groupId>%s</groupId>\n"
" <artifactId>ninca-crk-from-lib-reactor</artifactId>\n"
" <version>%s</version>\n"
" <relativePath>../pom.xml</relativePath>\n"
" </parent>\n" % (GROUP_ID, REACTOR_VERSION)
)
m = re.search(r"</modelVersion>\s*", raw, flags=re.I)
if not m:
raise ValueError("embedded pom missing modelVersion")
pos = m.end()
return raw[:pos] + parent_block + raw[pos:]
def minimal_pom(artifact_id: str, description: str) -> str:
return """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>{gid}</groupId>
<artifactId>ninca-crk-from-lib-reactor</artifactId>
<version>{ver}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>{aid}</artifactId>
<name>{aid}</name>
<description>{desc}</description>
<properties>
<maven.compiler.source>{jv}</maven.compiler.source>
<maven.compiler.target>{jv}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
""".format(
gid=GROUP_ID,
ver=REACTOR_VERSION,
aid=artifact_id,
desc=description,
jv=JAVA_VERSION,
)
def reactor_pom(module_dirs: list[str]) -> str:
mods = "".join(" <module>{}</module>\n".format(m) for m in module_dirs)
return """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>{gid}</groupId>
<artifactId>ninca-crk-from-lib-reactor</artifactId>
<version>{ver}</version>
<packaging>pom</packaging>
<name>ninca-crk-from-lib-reactor</name>
<description>
CFR 反编译自交付 fat jar lib/ 内 ninca-crk 四条业务线(access-control / conference-attendance /
smart-attendance / visitor-management)共 28 个构件;用于源码走查与对照,不代表官方原始工程结构。
</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>{jv}</java.version>
<maven.compiler.source>${{java.version}}</maven.compiler.source>
<maven.compiler.target>${{java.version}}</maven.compiler.target>
</properties>
<modules>
{mods} </modules>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${{java.version}}</source>
<target>${{java.version}}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
""".format(
gid=GROUP_ID,
ver=REACTOR_VERSION,
mods=mods,
jv=JAVA_VERSION,
)
def run_cfr(jar_path: Path, java_out: Path) -> None:
java_out.mkdir(parents=True, exist_ok=True)
cmd = [
"java",
"-jar",
str(ensure_cfr()),
str(jar_path),
"--outputdir",
str(java_out),
"--silent",
"true",
]
subprocess.run(cmd, check=True)
def main() -> int:
fat_jar = Path(sys.argv[1]) if len(sys.argv) > 1 else FAT_JAR_DEFAULT
if not fat_jar.is_file():
print("Fat jar not found:", fat_jar, file=sys.stderr)
return 1
if OUT_ROOT.exists():
shutil.rmtree(OUT_ROOT)
OUT_ROOT.mkdir(parents=True)
module_dirs: list[str] = []
tmpdir = Path(tempfile.mkdtemp(prefix="crk_lib_extract_"))
try:
with zipfile.ZipFile(fat_jar, "r") as zf:
for name in TARGET_NAMES:
inner = "lib/" + name
if inner not in zf.namelist():
print("MISSING in fat jar:", inner, file=sys.stderr)
return 2
extract_path = tmpdir / name
with zf.open(inner) as src, open(extract_path, "wb") as dst:
shutil.copyfileobj(src, dst)
aid = artifact_dir(name)
mod_path = OUT_ROOT / aid
mod_path.mkdir(parents=True)
java_root = mod_path / "src" / "main" / "java"
java_root.mkdir(parents=True)
run_cfr(extract_path, java_root)
with zipfile.ZipFile(extract_path, "r") as inner_zf:
embedded, emb_path = find_embedded_pom_xml(inner_zf)
desc = "CFR from {} (embedded pom: {})".format(name, emb_path or "none")
if embedded:
try:
pom_body = sanitize_pom_xml(embedded, aid)
(mod_path / "pom.xml").write_text(pom_body, encoding="utf-8")
except Exception as exc:
print("WARN sanitize pom:", aid, exc, file=sys.stderr)
(mod_path / "pom.xml").write_text(minimal_pom(aid, desc), encoding="utf-8")
else:
(mod_path / "pom.xml").write_text(minimal_pom(aid, desc), encoding="utf-8")
module_dirs.append(aid)
(OUT_ROOT / "pom.xml").write_text(reactor_pom(module_dirs), encoding="utf-8")
readme = OUT_ROOT / "README.txt"
readme.write_text(
"Generated by scripts/decompile_ninca_crk_lib_modules.py\n"
"Source fat jar: {}\n".format(fat_jar)
+ "CFR: {}\n".format(CFR_JAR)
+ "\n反编译产物仅供走查;首次编译需在私服可用的环境下补齐依赖。\n",
encoding="utf-8",
)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
print("OK ->", OUT_ROOT)
return 0
if __name__ == "__main__":
sys.exit(main())
+555
View File
@@ -0,0 +1,555 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""从 V1/V2 fat-jar 二进制解析嵌套 lib,读取 META-INF/maven/**/pom.properties,生成与 Maven 坐标可比对的数据。
子命令/模式:
- 默认:写 docs/testing/cw-elevator-v1-v2-dependency-diff.md
- --gate:以 V1 为基准,对 candidate fat-jar 做嵌套 JAR 坐标 multiset 比对,不一致时非零退出(可配允许列表)。
"""
from __future__ import annotations
import argparse
import io
import sys
import zipfile
from collections import Counter, defaultdict
from pathlib import Path
def _parse_props(raw: str) -> dict[str, str]:
props: dict[str, str] = {}
for line in raw.splitlines():
line = line.strip()
if "=" in line and not line.startswith("#"):
k, _, v = line.partition("=")
props[k.strip()] = v.strip()
return props
def coords_from_nested_jar(data: bytes, jar_basename: str) -> tuple[str, str, str]:
"""Returns (groupId, artifactId, version) using META-INF/maven/**/pom.properties."""
try:
z = zipfile.ZipFile(io.BytesIO(data))
except zipfile.BadZipFile:
return ("?", "?", "?")
candidates: list[tuple[str, str, str]] = []
for name in z.namelist():
if not name.endswith("/pom.properties"):
continue
if "META-INF/maven/" not in name:
continue
try:
raw = z.read(name).decode("utf-8", errors="replace")
except KeyError:
continue
props = _parse_props(raw)
gid = props.get("groupId")
aid = props.get("artifactId")
ver = props.get("version")
if gid and aid and ver:
candidates.append((gid, aid, ver))
if not candidates:
return ("?", "?", "?")
stem = jar_basename[: -len(".jar")] if jar_basename.endswith(".jar") else jar_basename
for g, a, v in candidates:
if stem.startswith(a) or a in stem or stem.startswith(a.split(".")[-1]):
return (g, a, v)
return candidates[0]
def list_outer_nested(
outer: Path, inner_dir_prefix: str
) -> dict[str, tuple[str, str, str]]:
"""path_in_zip -> (g,a,v)"""
result: dict[str, tuple[str, str, str]] = {}
z = zipfile.ZipFile(outer)
for name in z.namelist():
if not name.startswith(inner_dir_prefix):
continue
if not name.endswith(".jar"):
continue
if name.endswith(".jar.original"):
continue
try:
data = z.read(name)
except KeyError:
continue
base = Path(name).name
g, a, v = coords_from_nested_jar(data, base)
result[name] = (g, a, v)
return result
def detect_lib_prefix(outer: Path) -> str:
"""可执行包为 V1 风格 lib/ 或 Spring Boot repackage 的 BOOT-INF/lib/。"""
z = zipfile.ZipFile(outer)
has_boot = any(n.startswith("BOOT-INF/lib/") and n.endswith(".jar") for n in z.namelist())
has_lib = any(n.startswith("lib/") and n.endswith(".jar") for n in z.namelist())
if has_boot and not has_lib:
return "BOOT-INF/lib/"
if has_lib:
return "lib/"
return "lib/"
def key_ga(path: str, t: tuple[str, str, str]) -> str:
g, a, v = t
if g == "?" or a == "?" or v == "?":
return f"unresolved:{Path(path).name}"
return f"{g}:{a}:{v}"
def multiset_from_jar(outer: Path) -> tuple[Counter[str], str, dict[str, tuple[str, str, str]]]:
prefix = detect_lib_prefix(outer)
jmap = list_outer_nested(outer, prefix)
ctr: Counter[str] = Counter()
for path, t in jmap.items():
ctr[key_ga(path, t)] += 1
return ctr, prefix, jmap
def _read_allow_counter(path: Path | None) -> Counter[str]:
if path is None or not path.is_file():
return Counter()
c: Counter[str] = Counter()
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split()
if len(parts) >= 2 and parts[-1].isdigit():
key, cnt = " ".join(parts[:-1]), int(parts[-1])
else:
key, cnt = line, 1
c[key] += cnt
return c
def _subtract_allow(diff: Counter[str], allow: Counter[str]) -> Counter[str]:
"""从 multiset 差值中扣减允许的盈余额度(每行 `coord` 或 `coord N`)。"""
out = Counter(diff)
for k, n in allow.items():
if k not in out:
continue
out[k] -= min(out[k], n)
if out[k] <= 0:
del out[k]
return out
def run_lib_gate(
baseline_jar: Path,
candidate_jar: Path,
allow_baseline_only: Path | None,
allow_candidate_only: Path | None,
) -> int:
"""Multiset 门禁:在应用允许偏差后 baseline 与 candidate 须一致。"""
if not baseline_jar.is_file():
print("Missing baseline jar:", baseline_jar, file=sys.stderr)
return 2
if not candidate_jar.is_file():
print("Missing candidate jar:", candidate_jar, file=sys.stderr)
return 2
b_ctr, b_prefix, _ = multiset_from_jar(baseline_jar)
c_ctr, c_prefix, _ = multiset_from_jar(candidate_jar)
allow_b = _read_allow_counter(allow_baseline_only)
allow_c = _read_allow_counter(allow_candidate_only)
only_b = _subtract_allow(b_ctr - c_ctr, allow_b)
only_c = _subtract_allow(c_ctr - b_ctr, allow_c)
if not only_b and not only_c:
print(
"lib parity OK:",
f"baseline={baseline_jar.name} ({b_prefix} n={sum(b_ctr.values())})",
f"candidate={candidate_jar.name} ({c_prefix} n={sum(c_ctr.values())})",
)
return 0
print("lib parity FAILED (multiset diff after allowlists)", file=sys.stderr)
print(f" baseline: {baseline_jar} prefix={b_prefix} keys={len(b_ctr)}", file=sys.stderr)
print(f" candidate: {candidate_jar} prefix={c_prefix} keys={len(c_ctr)}", file=sys.stderr)
if only_b:
print(" only in baseline (or excess count):", file=sys.stderr)
for k in sorted(only_b.keys()):
print(f" {k} x{only_b[k]}", file=sys.stderr)
if only_c:
print(" only in candidate (or excess count):", file=sys.stderr)
for k in sorted(only_c.keys()):
print(f" {k} x{only_c[k]}", file=sys.stderr)
return 1
def parse_maven_dependency_list(path: Path) -> list[tuple[str, str, str, str]]:
"""Parse mvn dependency:list output lines like 'groupId:artifactId:jar:version:compile'."""
rows: list[tuple[str, str, str, str]] = []
text = path.read_text(encoding="utf-8", errors="replace")
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("The following"):
continue
if line == "none":
continue
parts = line.split(":")
if len(parts) >= 5 and parts[2] == "jar":
gid, aid, _, ver, scope = parts[0], parts[1], parts[2], parts[3], parts[4]
rows.append((gid, aid, ver, scope))
return sorted(rows, key=lambda x: (x[0], x[1], x[2]))
def collect_ga_versions_from_jar_map(
jmap: dict[str, tuple[str, str, str]],
) -> dict[tuple[str, str], set[str]]:
"""嵌套 jar -> 每个 (groupId, artifactId) 在 fat-jar 中出现的 version 集合(仅 pom.properties 可解析条目)。"""
ga_ver: dict[tuple[str, str], set[str]] = defaultdict(set)
for _path, (g, a, v) in jmap.items():
if g != "?" and a != "?" and v != "?":
ga_ver[(g, a)].add(v)
return ga_ver
def ga_version_skew_rows(
v1_ga_v: dict[tuple[str, str], set[str]],
v2_ga_v: dict[tuple[str, str], set[str]],
) -> list[tuple[tuple[str, str], list[str], list[str]]]:
"""两侧均能解析到 version、且 version 集合不一致的 GA。"""
out: list[tuple[tuple[str, str], list[str], list[str]]] = []
all_ga = set(v1_ga_v.keys()) | set(v2_ga_v.keys())
for ga in sorted(all_ga, key=lambda x: (x[0], x[1])):
s1 = v1_ga_v.get(ga, set())
s2 = v2_ga_v.get(ga, set())
if s1 and s2 and s1 != s2:
out.append((ga, sorted(s1), sorted(s2)))
return out
def main() -> int:
root = Path(__file__).resolve().parents[1]
ap = argparse.ArgumentParser(description="V1/V2 fat-jar 依赖对比或 lib multiset 门禁")
ap.add_argument(
"--gate",
action="store_true",
help="运行 multiset 门禁(默认基准 V1 jar,候选为 releases 或 starter/target",
)
ap.add_argument("--baseline-jar", type=Path, default=None, help="门禁基准 fat-jar")
ap.add_argument("--candidate-jar", type=Path, default=None, help="门禁待测 fat-jar")
ap.add_argument(
"--allow-baseline-only",
type=Path,
default=None,
help="允许仅出现在 baseline 的坐标(每行 `g:a:v` 或 `unresolved:name.jar`,可选末尾计数)",
)
ap.add_argument(
"--allow-candidate-only",
type=Path,
default=None,
help="允许仅出现在 candidate 的坐标(格式同上)",
)
args = ap.parse_args()
if args.gate:
b = (
args.baseline_jar
if args.baseline_jar
else root
/ "cw-elevator-application-V1.0.0.20211103"
/ "cw-elevator-application-V1.0.0.20211103.jar"
)
cand = args.candidate_jar
if cand is None:
# 优先本地 package 产物(与当前父 POM / 插件一致);其次历史 releases 归档
cand = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "cw-elevator-application-2.0.7.jar"
)
if not cand.is_file():
cand = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "cw-elevator-application-2.0.0.jar"
)
if not cand.is_file():
cand = (
root
/ "maven-cw-elevator-application"
/ "releases"
/ "cw-elevator-application-V2.0.6.20260430"
/ "cw-elevator-application-2.0.6.jar"
)
allow_b = args.allow_baseline_only
if allow_b is None:
p = root / "docs" / "testing" / "cw-elevator-fatjar-lib-parity-allow-baseline-only.txt"
allow_b = p if p.is_file() else None
allow_c = args.allow_candidate_only
if allow_c is None:
p = root / "docs" / "testing" / "cw-elevator-fatjar-lib-parity-allow-candidate-only.txt"
allow_c = p if p.is_file() else None
return run_lib_gate(b, cand, allow_b, allow_c)
v1_jar = root / "cw-elevator-application-V1.0.0.20211103" / "cw-elevator-application-V1.0.0.20211103.jar"
v2_jar = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "cw-elevator-application-2.0.7.jar"
)
if not v2_jar.is_file():
v2_jar = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "cw-elevator-application-2.0.0.jar"
)
if not v2_jar.is_file():
v2_jar = (
root
/ "maven-cw-elevator-application"
/ "releases"
/ "cw-elevator-application-V2.0.6.20260430"
/ "cw-elevator-application-2.0.6.jar"
)
# reactor + dependency:list 时各模块写各自 target/starter 模块输出才是入口 fat-jar 的 runtime 列表
mvn_list = (
root
/ "maven-cw-elevator-application"
/ "cw-elevator-application-starter"
/ "target"
/ "v2-maven-deps.txt"
)
if not mvn_list.is_file():
mvn_list = root / "maven-cw-elevator-application" / "target" / "v2-maven-deps.txt"
if not mvn_list.is_file():
mvn_list = Path("/tmp/v2-maven-deps.txt")
out_md = (
root
/ "docs"
/ "testing"
/ "cw-elevator-v1-v2-dependency-diff.md"
)
if not v1_jar.is_file():
print("Missing V1 jar:", v1_jar, file=sys.stderr)
return 1
if not v2_jar.is_file():
print("Missing V2 jar:", v2_jar, file=sys.stderr)
return 1
v1_map = list_outer_nested(v1_jar, "lib/")
v2_prefix = detect_lib_prefix(v2_jar)
v2_map = list_outer_nested(v2_jar, v2_prefix)
v1_by_ga: dict[str, list[str]] = defaultdict(list)
for path, t in v1_map.items():
v1_by_ga[key_ga(path, t)].append(path)
v2_by_ga: dict[str, list[str]] = defaultdict(list)
for path, t in v2_map.items():
v2_by_ga[key_ga(path, t)].append(path)
keys1 = set(v1_by_ga.keys())
keys2 = set(v2_by_ga.keys())
only_v1 = sorted(keys1 - keys2)
only_v2 = sorted(keys2 - keys1)
both = sorted(keys1 & keys2)
mvn_rows: list[tuple[str, str, str, str]] = []
if mvn_list.is_file():
mvn_rows = parse_maven_dependency_list(mvn_list)
mvn_ga = {f"{g}:{a}:{v}" for g, a, v, _ in mvn_rows}
lines: list[str] = []
lines.append("# cw-elevator-application V1 fat-jar 与 V2 fat-jar 依赖差异核对")
lines.append("")
lines.append("**生成方式**:脚本 `scripts/generate_v1_v2_elevator_dependency_diff.py`(可重复执行覆盖本文件)。")
lines.append("")
lines.append("## 样本路径")
lines.append("")
lines.append(f"- **V1**`{v1_jar.relative_to(root)}`")
lines.append(f"- **V2**`{v2_jar.relative_to(root)}`")
lines.append("")
lines.append("| 指标 | V1 | V2 |")
lines.append("|------|----|----|")
lines.append(f"| 嵌套 jar 条目数(lib / BOOT-INF/lib | {len(v1_map)} | {len(v2_map)} |")
lines.append(f"| 解析出唯一坐标 `groupId:artifactId:version` 数 | {len(keys1)} | {len(keys2)} |")
v1_ga_v = collect_ga_versions_from_jar_map(v1_map)
v2_ga_v = collect_ga_versions_from_jar_map(v2_map)
skew_ga = ga_version_skew_rows(v1_ga_v, v2_ga_v)
lines.append(
f"| 同名 GA、两侧均有解析且 version 集合不一致(§2.2.1)条数 | — | **{len(skew_ga)}** |"
)
lines.append(
f"| 与 Maven `dependency:list`runtime)条目数 | — | {len(mvn_rows)} |"
)
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 1. Maven 方式(仅 V2 reactor")
lines.append("")
lines.append(
"在 `maven-cw-elevator-application` 下执行:`mvn -pl cw-elevator-application-starter -am "
"dependency:list -DincludeScope=runtime -Dsort=true "
"-DoutputFile=target/v2-maven-deps.txt`。`-am` 时每个子模块写各自的 `target/`;"
"**§1.1 使用 starter 模块文件**`cw-elevator-application-starter/target/v2-maven-deps.txt`。"
)
lines.append("")
lines.append(
"**说明**:历史 **V1 运行包** 当前仓库无对应 **1.0** 聚合工程可一键 `dependency:list`"
"V1 的 Maven 坐标视图见 **§2 二进制嵌套 JAR 的 pom.properties**。"
)
lines.append("")
lines.append("### 1.1 V2 `dependency:list` 全量(runtime")
lines.append("")
lines.append("| # | groupId | artifactId | version | scope |")
lines.append("|---|---------|--------------|---------|-------|")
for i, (g, a, v, s) in enumerate(mvn_rows, 1):
lines.append(f"| {i} | `{g}` | `{a}` | `{v}` | `{s}` |")
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 2. 二进制方式(嵌套 JAR + pom.properties")
lines.append("")
lines.append(
f"- **V1**`lib/*.jar`。\n"
f"- **V2**:自动检测为 `{v2_prefix}*.jar`(与 spring-boot-maven-plugin 1.3.x + Boot 1.5 一致时为 `lib/`)。"
)
lines.append("")
lines.append(
"对每个嵌套 jar 读取 `META-INF/maven/**/pom.properties` 得到 `groupId:artifactId:version`"
"无法读取时记为 `?:?:?`(多为无 Maven 元数据的第三方包)。"
)
lines.append("")
lines.append("### 2.1 仅在 V1 出现的坐标(相对 V2 二进制集合)")
lines.append("")
lines.append(f"**共 {len(only_v1)} 项**。")
lines.append("")
lines.append("| groupId:artifactId:version | V1 嵌套路径 |")
lines.append("|------------------------------|-------------|")
for k in only_v1:
paths = ", ".join(f"`{p}`" for p in sorted(v1_by_ga[k]))
lines.append(f"| `{k}` | {paths} |")
lines.append("")
lines.append("### 2.2 仅在 V2 出现的坐标(相对 V1 二进制集合)")
lines.append("")
lines.append(f"**共 {len(only_v2)} 项**。")
lines.append("")
lines.append("| groupId:artifactId:version | V2 嵌套路径 |")
lines.append("|------------------------------|-------------|")
for k in only_v2:
paths = ", ".join(f"`{p}`" for p in sorted(v2_by_ga[k]))
lines.append(f"| `{k}` | {paths} |")
lines.append("")
lines.append("### 2.2.1 同名构件(groupId:artifactId)在 V1 与 V2 中的版本集合差异")
lines.append("")
lines.append(
"由嵌套 jar 的 `pom.properties` 聚合:若同一 **GA** 在 V1、V2 中均能解析出版本,且 **version 集合不同**"
"则单独列出(与 §2.1 / §2.2 中分列的 `g:a:v` 键互为补充)。**不含**仅一侧出现的 GA。"
)
lines.append("")
lines.append(f"**共 {len(skew_ga)} 项**。")
lines.append("")
lines.append("| groupId:artifactId | V1 version(s) | V2 version(s) |")
lines.append("|--------------------|---------------|---------------|")
for (g, a), v1s, v2s in skew_ga:
ga_s = f"`{g}:{a}`"
lines.append(
f"| {ga_s} | `{', '.join(v1s)}` | `{', '.join(v2s)}` |"
)
lines.append("")
lines.append("### 2.3 两边均存在且坐标一致的依赖")
lines.append("")
lines.append(f"**共 {len(both)} 项**(名称版本完全一致)。")
lines.append("")
lines.append("<details>")
lines.append("<summary>展开长表</summary>")
lines.append("")
lines.append("| groupId:artifactId:version |")
lines.append("|------------------------------|")
for k in both:
lines.append(f"| `{k}` |")
lines.append("")
lines.append("</details>")
lines.append("")
lines.append("### 2.4 V2 二进制坐标 vs Maven dependency:list")
lines.append("")
lines.append(
"- **版本字符串不一致**:例如 reactor 在 `dependency:list` 中为 **`2.0-SNAPSHOT`**"
"而 fat-jar 内嵌模块 **`cw-elevator-application-*-2.0.6.jar`** 的 `pom.properties` 为 **`2.0.6`**"
"字符串比对会视为「仅一侧存在」,属**同名构件不同表述**,非缺失依赖。"
)
lines.append(
"- **在 dependency:list 中但不在嵌套 jar 元数据中的**:多为 **仅存在于解析树、与本模块 jar 文件命名不一致**,需对照 §1 表格。"
)
lines.append(
"- **未解析 `unresolved:*`**:见 §3,此类条目不参与坐标相等判断。"
)
lines.append("")
only_mvn = sorted(mvn_ga - keys2)
only_bin = sorted(keys2 - mvn_ga)
lines.append(f"- **仅在 Maven listruntime**{len(only_mvn)}")
lines.append("")
if only_mvn:
lines.append("|坐标|")
lines.append("|----|")
for k in only_mvn[:80]:
lines.append(f"| `{k}` |")
if len(only_mvn) > 80:
lines.append(f"| … 其余 {len(only_mvn) - 80} 项省略 |")
lines.append("")
lines.append(f"- **仅在二进制坐标集合**{len(only_bin)}")
lines.append("")
if only_bin:
lines.append("|坐标|")
lines.append("|----|")
for k in only_bin[:80]:
lines.append(f"| `{k}` |")
if len(only_bin) > 80:
lines.append(f"| … 其余 {len(only_bin) - 80} 项省略 |")
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 3. 无法解析 pom.properties 的嵌套 JAR(仅列文件名)")
lines.append("")
bad_v1 = [p for p, t in v1_map.items() if t[0] == "?" or t[2] == "?"]
bad_v2 = [p for p, t in v2_map.items() if t[0] == "?" or t[2] == "?"]
lines.append(f"- **V1** 未解析条目:**{len(bad_v1)}**")
for p in sorted(bad_v1)[:50]:
lines.append(f" - `{Path(p).name}`")
if len(bad_v1) > 50:
lines.append(f" - … 省略 {len(bad_v1) - 50}")
lines.append(f"- **V2** 未解析条目:**{len(bad_v2)}**")
for p in sorted(bad_v2)[:50]:
lines.append(f" - `{Path(p).name}`")
if len(bad_v2) > 50:
lines.append(f" - … 省略 {len(bad_v2) - 50}")
lines.append("")
out_md.parent.mkdir(parents=True, exist_ok=True)
out_md.write_text("\n".join(lines) + "\n", encoding="utf-8")
print("Wrote", out_md)
return 0
if __name__ == "__main__":
sys.exit(main())
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# 转发至 maven-cw-elevator-application 内的一键 API 套件脚本。
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
exec bash "${ROOT}/maven-cw-elevator-application/scripts/run_full_elevator_api_suite.sh" "$@"
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# 转发至 maven-cw-elevator-application:一键构建 V2、启动 V1/V2、跑完整 API 套件。
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
exec bash "${ROOT}/maven-cw-elevator-application/scripts/run_v1v2_parity_automated.sh" "$@"
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Scan *.java for block comments in the last N lines (tail noise / orphan */). Read-only."""
from __future__ import annotations
import sys
from pathlib import Path
TAIL_LINES = 50
REPO = Path(__file__).resolve().parent.parent
def main() -> int:
suspicious: list[tuple[Path, str]] = []
with_slash: list[Path] = []
for p in REPO.rglob("*.java"):
sp = str(p)
if "/target/" in sp:
continue
try:
t = p.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
lines = t.splitlines()
if not lines:
continue
tail = "\n".join(lines[-TAIL_LINES:])
if "/*" in tail or "*/" in tail:
with_slash.append(p)
low = tail.lower()
if "location" in low and "/*" in tail:
suspicious.append((p, "Location in tail block"))
if "jd-core" in low or "jd core" in low:
suspicious.append((p, "jd-core in tail"))
if "decompil" in low and "/*" in tail:
suspicious.append((p, "decompil+block in tail"))
# Trailing */ only: last non-empty line is */
nonws = [ln for ln in lines[-20:] if ln.strip()]
if len(nonws) >= 1 and nonws[-1].strip() == "*/" and "/**" not in "\n".join(lines[-5:]):
# might be orphan closer (rare)
if "*/" in tail and tail.count("/*") < tail.count("*/"):
suspicious.append((p, "possible orphan */ at EOF"))
print(f"Scanned under {REPO}")
print(f"Files with /* or */ in last {TAIL_LINES} lines: {len(with_slash)}")
print(f"Flagged suspicious: {len(suspicious)}")
for p, reason in suspicious[:80]:
print(f" {reason}: {p.relative_to(REPO)}")
if len(suspicious) > 80:
print(f" ... and {len(suspicious) - 80} more")
return 0
if __name__ == "__main__":
sys.exit(main())
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""
Remove JD-Core decompiler noise from Java sources under maven-*:
- Line prefix comments: /* */ and /* N */ / /* N */ (line numbers; spaces around N vary)
- Trailing metadata block: /* Location: ... JD-Core Version: ... */
Does not strip normal Javadoc /** ... */ or arbitrary block comments.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
RE_LINE_EMPTY_PREFIX = re.compile(r"^/\* +\*/\s*")
# JD-Core often emits "/* 90 */" (extra spaces after /* and/or before */)
RE_LINE_NUM_PREFIX = re.compile(r"^/\* *\d+ *\*/\s*")
# Ends with line like " */" (space + */) after "* JD-Core Version: ..."
RE_TAIL_META = re.compile(
r"(?:^|\n)/\* Location:.*?\n\s*\*/\s*",
re.DOTALL,
)
def strip_content(text: str) -> str:
text = text.replace("\r\n", "\n").replace("\r", "\n")
lines = text.split("\n")
stripped = []
for line in lines:
line = RE_LINE_EMPTY_PREFIX.sub("", line)
line = RE_LINE_NUM_PREFIX.sub("", line)
stripped.append(line)
joined = "\n".join(stripped)
joined = RE_TAIL_META.sub("\n", joined)
if joined and not joined.endswith("\n"):
joined += "\n"
return joined
def process_file(path: Path, dry_run: bool) -> bool:
raw = path.read_text(encoding="utf-8", errors="replace")
new = strip_content(raw)
raw_norm = raw.replace("\r\n", "\n").replace("\r", "\n")
if raw_norm and not raw_norm.endswith("\n"):
raw_norm += "\n"
if new == raw_norm:
return False
if not dry_run:
path.write_text(new, encoding="utf-8", newline="\n")
return True
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument(
"roots",
nargs="*",
type=Path,
help="Optional roots (default: all maven-* under repo root next to scripts/)",
)
ap.add_argument("--dry-run", action="store_true", help="Report only, do not write")
args = ap.parse_args()
script_dir = Path(__file__).resolve().parent
repo = script_dir.parent
if args.roots:
roots = [Path(p).resolve() for p in args.roots]
else:
roots = sorted(repo.glob("maven-*"))
roots = [p for p in roots if p.is_dir()]
changed = 0
scanned = 0
for root in roots:
if not root.exists():
print(f"skip missing: {root}", file=sys.stderr)
continue
for path in root.rglob("*.java"):
if "/target/" in str(path).replace("\\", "/"):
continue
scanned += 1
if process_file(path, args.dry_run):
changed += 1
if args.dry_run:
print(f"would update: {path}")
print(f"Scanned: {scanned} Java files under {len(roots)} root(s)")
print(f"{'Would change' if args.dry_run else 'Changed'}: {changed} file(s)")
return 0
if __name__ == "__main__":
sys.exit(main())
+252
View File
@@ -0,0 +1,252 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
前端运行时自动验收(Playwright):
1) 打开页面并等待首屏稳定
2) 采集 console/pageerror/requestfailed
3) 统计重复资源请求(按 URL 去 query 后聚合)
4) 输出 markdown 报告 + 截图
"""
from __future__ import annotations
import argparse
import asyncio
import datetime as dt
import json
import re
import sys
from collections import Counter
from pathlib import Path
from typing import Dict, List, Tuple
from urllib.parse import urlsplit, urlunsplit
from playwright.async_api import Error as PlaywrightError
from playwright.async_api import async_playwright
def _strip_query(url: str) -> str:
p = urlsplit(url)
return urlunsplit((p.scheme, p.netloc, p.path, "", ""))
def _now_tag() -> str:
return dt.datetime.now().strftime("%Y%m%d-%H%M%S")
def _classify_resource(url: str) -> str:
path = urlsplit(url).path.lower()
if path.endswith(".js"):
return "js"
if path.endswith(".css"):
return "css"
if re.search(r"\.(png|jpg|jpeg|gif|svg|ico|webp)$", path):
return "image"
if "/api/" in path or "/elevator/" in path:
return "api"
return "other"
async def run_check(url: str, wait_ms: int, screenshot_path: Path) -> Dict[str, object]:
console_errors: List[str] = []
console_warnings: List[str] = []
page_errors: List[str] = []
failed_requests: List[str] = []
requests: List[Tuple[str, str]] = []
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(ignore_https_errors=True)
page = await context.new_page()
page.set_default_timeout(5_000)
page.on(
"console",
lambda msg: (
console_errors.append(msg.text)
if msg.type == "error"
else console_warnings.append(msg.text)
if msg.type in {"warning", "warn"}
else None
),
)
page.on("pageerror", lambda err: page_errors.append(str(err)))
def _on_request_failed(req) -> None:
failure = req.failure
if isinstance(failure, dict):
err_text = failure.get("errorText", "unknown")
elif isinstance(failure, str):
err_text = failure
elif failure is None:
err_text = "unknown"
else:
err_text = getattr(failure, "error_text", str(failure))
failed_requests.append(f"{req.url} | {err_text}")
page.on("requestfailed", _on_request_failed)
page.on("requestfinished", lambda req: requests.append((req.method, req.url)))
navigation_mode = "domcontentloaded"
try:
try:
await page.goto(url, wait_until="domcontentloaded", timeout=12_000)
except PlaywrightError:
# 某些历史页面会因脚本阻塞导致 domcontentloaded 不触发,降级到 commit 保证可诊断
navigation_mode = "commit(fallback)"
await page.goto(url, wait_until="commit", timeout=20_000)
await page.wait_for_timeout(wait_ms)
try:
# 避免 full_page 在超长文档上触发超时,先抓取可视区域。
await page.screenshot(path=str(screenshot_path), full_page=False, timeout=15_000)
except Exception:
screenshot_path.write_bytes(b"")
try:
title = await page.title()
except Exception:
title = ""
try:
body_non_empty = bool(await page.evaluate("Boolean(document.body && document.body.innerText && document.body.innerText.trim())"))
except Exception:
body_non_empty = False
try:
app_count = await page.locator("#app *").count()
except Exception:
app_count = -1
except PlaywrightError as e:
await browser.close()
return {
"fatal_error": str(e),
"console_errors": console_errors,
"console_warnings": console_warnings,
"page_errors": page_errors,
"failed_requests": failed_requests,
"requests": requests,
}
await browser.close()
normalized_urls = [_strip_query(u) for _, u in requests]
url_counter = Counter(normalized_urls)
duplicate_urls = [(u, c) for u, c in url_counter.items() if c > 1]
duplicate_urls.sort(key=lambda x: x[1], reverse=True)
resource_counter = Counter(_classify_resource(u) for u in normalized_urls)
return {
"fatal_error": "",
"navigation_mode": navigation_mode,
"title": title,
"body_non_empty": body_non_empty,
"app_count": app_count,
"console_errors": console_errors,
"console_warnings": console_warnings,
"page_errors": page_errors,
"failed_requests": failed_requests,
"requests_total": len(requests),
"requests_unique": len(url_counter),
"resource_counter": dict(resource_counter),
"duplicate_urls": duplicate_urls[:30],
}
def _render_report(url: str, result: Dict[str, object], screenshot_path: Path) -> str:
lines: List[str] = []
lines.append("# 前端运行时自动验收报告")
lines.append("")
lines.append(f"- URL: `{url}`")
lines.append(f"- 时间: `{dt.datetime.now().isoformat(timespec='seconds')}`")
lines.append(f"- 截图: `{screenshot_path}`")
lines.append("")
fatal = str(result.get("fatal_error", "")).strip()
if fatal:
lines.append("## 结论")
lines.append("- **失败**:页面打开阶段异常")
lines.append(f"- 异常: `{fatal}`")
return "\n".join(lines) + "\n"
lines.append("## 页面状态")
lines.append(f"- 导航模式: `{result.get('navigation_mode', '')}`")
lines.append(f"- title: `{result.get('title', '')}`")
lines.append(f"- body 是否非空: `{result.get('body_non_empty', False)}`")
lines.append(f"- `#app` 子节点数: `{result.get('app_count', 0)}`")
lines.append("")
lines.append("## 请求统计")
lines.append(f"- 总请求数: `{result.get('requests_total', 0)}`")
lines.append(f"- 去重后 URL 数: `{result.get('requests_unique', 0)}`")
lines.append(f"- 类型分布: `{json.dumps(result.get('resource_counter', {}), ensure_ascii=False)}`")
lines.append("")
dup = result.get("duplicate_urls", [])
lines.append("## 重复请求(Top")
if dup:
for u, c in dup:
lines.append(f"- `{c}x` {u}")
else:
lines.append("- 无重复 URL 请求")
lines.append("")
def _section(title: str, rows: List[str]) -> None:
lines.append(f"## {title}")
if rows:
for row in rows[:50]:
lines.append(f"- {row}")
else:
lines.append("- 无")
lines.append("")
_section("Console Error", result.get("console_errors", []))
_section("Page Error", result.get("page_errors", []))
_section("Request Failed", result.get("failed_requests", []))
_section("Console Warning", result.get("console_warnings", []))
return "\n".join(lines)
async def _amain() -> int:
parser = argparse.ArgumentParser(description="自动检测前端运行时错误/重复加载")
parser.add_argument("--url", default="http://127.0.0.1:8090/login", help="待检测 URL")
parser.add_argument(
"--wait-ms", type=int, default=5000, help="页面打开后额外等待毫秒数(默认 5000)"
)
parser.add_argument(
"--out-dir",
default="artifacts/frontend-check",
help="报告与截图输出目录(默认 artifacts/frontend-check",
)
args = parser.parse_args()
out_dir = Path(args.out_dir).resolve()
out_dir.mkdir(parents=True, exist_ok=True)
tag = _now_tag()
screenshot = out_dir / f"frontend-{tag}.png"
report = out_dir / f"frontend-{tag}.md"
result = await run_check(args.url, args.wait_ms, screenshot)
report.write_text(_render_report(args.url, result, screenshot), encoding="utf-8")
print(f"[frontend-check] report={report}")
print(f"[frontend-check] screenshot={screenshot}")
if result.get("fatal_error"):
return 2
if result.get("console_errors") or result.get("page_errors") or result.get("failed_requests"):
return 1
return 0
def main() -> int:
try:
return asyncio.run(_amain())
except KeyboardInterrupt:
return 130
except Exception as e:
print(f"[frontend-check] unexpected error: {e}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
V1_DIR="${ROOT_DIR}/cw-elevator-application-V1.0.0.20211103"
V2_DIR="${ROOT_DIR}/maven-cw-elevator-application/deploy/v2-maven"
V2_RELEASES_DIR="${ROOT_DIR}/maven-cw-elevator-application/releases"
V1_JAR="${V1_DIR}/cw-elevator-application-V1.0.0.20211103.jar"
V2_JAR=""
for candidate in $(ls -1t "${V2_RELEASES_DIR}"/v*/cw-elevator-application-*.jar 2>/dev/null || true); do
if [[ -f "${candidate}" ]]; then
V2_JAR="${candidate}"
break
fi
done
if [[ -z "${V2_JAR}" ]]; then
for candidate in $(ls -1t "${V2_DIR}"/cw-elevator-application-*.jar 2>/dev/null || true); do
if [[ -f "${candidate}" ]]; then
V2_JAR="${candidate}"
break
fi
done
fi
if [[ ! -f "${V1_JAR}" ]]; then
echo "ERROR: V1 jar not found: ${V1_JAR}" >&2
exit 1
fi
if [[ -z "${V2_JAR}" || ! -f "${V2_JAR}" ]]; then
echo "ERROR: V2 jar not found under ${V2_DIR}" >&2
exit 1
fi
python3 - "$V1_DIR" "$V2_DIR" "$V1_JAR" "$V2_JAR" <<'PY'
import subprocess
import sys
from pathlib import Path
v1_dir = Path(sys.argv[1])
v2_dir = Path(sys.argv[2])
v1_jar = Path(sys.argv[3])
v2_jar = Path(sys.argv[4])
files = [
"bootstrap.properties",
"application.properties",
"application-access-control.properties",
]
keys = [
"spring.cloud.consul.host",
"spring.cloud.consul.port",
"spring.cloud.consul.discovery.enabled",
"spring.cloud.consul.config.enabled",
"feign.cwos-portal.name",
"feign.ninca-common.name",
"feign.component-organization.name",
]
def parse_properties(text):
result = {}
for raw_line in text.splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or line.startswith("!"):
continue
if "=" in line:
k, v = line.split("=", 1)
elif ":" in line:
k, v = line.split(":", 1)
else:
continue
result[k.strip()] = v.strip()
return result
def read_external(dir_path, name):
p = dir_path / name
if not p.exists():
return None
return parse_properties(p.read_text(encoding="utf-8", errors="ignore"))
def read_jar(jar_path, name):
try:
out = subprocess.check_output(
["jar", "tf", str(jar_path)],
text=True,
errors="ignore",
).splitlines()
except subprocess.CalledProcessError:
return None
candidates = []
suffix = "/" + name
for item in out:
if item == name or item.endswith(suffix):
candidates.append(item)
if not candidates:
return None
preferred = None
for c in candidates:
if c.startswith("BOOT-INF/classes/"):
preferred = c
break
if preferred is None:
preferred = candidates[0]
try:
content = subprocess.check_output(
["unzip", "-p", str(jar_path), preferred],
text=True,
errors="ignore",
)
except subprocess.CalledProcessError:
return None
return parse_properties(content)
def describe_source(external_map, internal_map, key):
ext_val = None if external_map is None else external_map.get(key)
int_val = None if internal_map is None else internal_map.get(key)
if ext_val is not None:
return "external", ext_val
if int_val is not None:
return "jar-internal", int_val
return "unset", ""
def report_version(tag, base_dir, jar_path):
print(f"\n==== {tag} ====")
print(f"base_dir: {base_dir}")
print(f"jar: {jar_path}")
ext_maps = {name: read_external(base_dir, name) for name in files}
int_maps = {name: read_jar(jar_path, name) for name in files}
print("\n[1] 配置文件存在性")
for name in files:
ext_exists = ext_maps[name] is not None
int_exists = int_maps[name] is not None
print(f"- {name}: external={'Y' if ext_exists else 'N'}, jar={'Y' if int_exists else 'N'}")
print("\n[2] 关键键值最终命中来源(按常见优先级:external > jar")
merged_ext = {}
merged_int = {}
for name in files:
if ext_maps[name]:
merged_ext.update(ext_maps[name])
if int_maps[name]:
merged_int.update(int_maps[name])
for key in keys:
src, val = describe_source(merged_ext, merged_int, key)
print(f"- {key}: source={src}, value={val}")
print("\n[3] 加载顺序结论")
has_any_internal = any(int_maps[name] is not None for name in files)
if has_any_internal:
print("- 该版本具备 jar 内置配置兜底。")
else:
print("- 该版本无 jar 内置配置兜底,依赖 external/远端配置。")
print("- 在未显式传 --spring.config.location 时,外部同名配置通常优先于 jar 内置。")
report_version("V1", v1_dir, v1_jar)
report_version("V2", v2_dir, v2_jar)
print("\n==== 总结对比 ====")
print("- V1: external + jar 内置(有兜底)")
v2_internal = any(read_jar(v2_jar, name) is not None for name in files)
if v2_internal:
print("- V2: external + jar 内置(有兜底)")
else:
print("- V2: external 优先,当前样本无 jar 内置同名配置(无兜底)")
print("- 若 external 与远端配置中心并存,最终以运行时属性源优先级为准。")
PY