GraalVM Native reachability-metadata.json 快速生成 reflect-config.json、resource-config.json

生成 reachability-metadata.json

:warning:注意:自动生成并不能解决所有问题

使用 GraalVM 运行 jar 包后,访问业务 API 接口

${GRAALVM_HOME}/bin/java -agentlib:native-image-agent=config-output-dir=./target/native-image-config -jar ./target/graalvm-demo-0.0.1-SNAPSHOT.jar

业务 API 接口运行完成后,正常终止程序

快速生成 reflect-config.json、resource-config.json

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Merge reachability-metadata.json (generated by GraalVM native-image agent)
into the corresponding native-image config files.

Usage:
    python3 merge_native_image_config.py --target-dir PATH [--source PATH]

    --target-dir  Directory containing native-image config files
                  (reflect-config.json, resource-config.json, etc.).
    --source      Path to reachability-metadata.json (source).
                  Default: target/native-image-config/reachability-metadata.json

The script merges into:
    reflect-config.json   — "reflection" node
    resource-config.json  — "resources" node

Additional top-level keys from reachability-metadata.json can be supported
by extending the KEY_TO_FILE map below.

Rules:
    - Only ADD new entries; existing entries are never deleted.
    - Duplicate detection is keyed on the primary identifier:
        reflection: "type" (source) → "name" (target)
        resources:  "glob" (source) → "pattern" (target)
    - If a target file does not exist, a default (empty) structure is created.
    - Target file format is preserved.
    - After merging, run: mvn spotless:apply -pl server -am
"""

import argparse
import json
import os
import sys
from typing import Any, Dict, List, Optional, Set, Union

# ---------------------------------------------------------------------------
# Path defaults — relative to this script's location
# ---------------------------------------------------------------------------
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SEATA_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "..", ".."))  # script is at script/native/

DEFAULT_SOURCE_FILE = os.path.join(
    SEATA_ROOT, "target", "native-image-config", "reachability-metadata.json"
)

# ---------------------------------------------------------------------------
# Mapping: top-level key in source → (target_filename, id_mapping, format_type)
#
# id_mapping: (source_key, target_key)
#   The field used to detect duplicates (and renamed when needed).
#
# format_type:
#   "list"    — target file is a JSON array (e.g. reflect-config.json)
#   "includes"— target file has {"resources": {"includes": [...]}} (e.g. resource-config.json)
# ---------------------------------------------------------------------------
KEY_TO_FILE: Dict[str, tuple] = {
    "reflection": ("reflect-config.json",
                   ("type", "name"),
                   "list"),
    "resources":  ("resource-config.json",
                   ("glob", "pattern"),
                   "includes"),
}


def load_json(path: str) -> Any:
    """Load and return a JSON file."""
    with open(path, "r", encoding="utf-8") as fh:
        return json.load(fh)


def save_json(path: str, data: Any) -> None:
    """Save data as JSON with consistent formatting.

    Uses the same indent as the source file (2 spaces) and appends a
    trailing newline so Spotless can operate cleanly."""
    with open(path, "w", encoding="utf-8") as fh:
        json.dump(data, fh, indent=2, ensure_ascii=False)
        fh.write("\n")


# ---------------------------------------------------------------------------
# Merge workers
# ---------------------------------------------------------------------------

def merge_list_format(
    source_entries: List[dict],
    target_list: List[dict],
    src_key: str,
    tgt_key: str,
) -> int:
    """Merge source entries into a list-format target (e.g. reflect-config.json).

    Returns the number of newly added entries."""
    # Build existing-id set from target
    existing_ids: Set[str] = set()
    for entry in target_list:
        if isinstance(entry, dict):
            val = entry.get(tgt_key)
            if isinstance(val, str):
                existing_ids.add(val)

    added = 0
    for src_entry in source_entries:
        identifier = src_entry.get(src_key)
        if not isinstance(identifier, str):
            continue
        if identifier in existing_ids:
            continue

        # Build target entry: rename the key, copy everything else
        new_entry = dict(src_entry)
        new_entry[tgt_key] = new_entry.pop(src_key)
        target_list.append(new_entry)
        existing_ids.add(identifier)
        added += 1

    return added


def merge_includes_format(
    source_entries: List[dict],
    target_includes: List[dict],
    src_key: str,
    tgt_key: str,
) -> int:
    """Merge source entries into an includes-format target (e.g. resource-config.json).

    Returns the number of newly added entries."""
    # Build existing-id set from target includes
    existing_ids: Set[str] = set()
    for entry in target_includes:
        if isinstance(entry, dict):
            val = entry.get(tgt_key)
            if isinstance(val, str):
                existing_ids.add(val)

    added = 0
    for src_entry in source_entries:
        identifier = src_entry.get(src_key)
        if not isinstance(identifier, str):
            continue
        if identifier in existing_ids:
            continue

        new_entry: Dict[str, Any] = {tgt_key: identifier}
        target_includes.append(new_entry)
        existing_ids.add(identifier)
        added += 1

    return added


def create_default_data(fmt: str) -> Any:
    """Return a default (empty) data structure for the given format."""
    if fmt == "list":
        return []
    elif fmt == "includes":
        return {"resources": {"includes": []}}
    else:
        raise ValueError(f"Unknown format: {fmt}")


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def build_arg_parser() -> argparse.ArgumentParser:
    """Build and return the argument parser."""
    parser = argparse.ArgumentParser(
        description=(
            "Merge reachability-metadata.json (generated by GraalVM native-image "
            "agent) into the corresponding native-image config files."
        ),
    )
    parser.add_argument(
        "--source",
        default=DEFAULT_SOURCE_FILE,
        help=(
            "Path to reachability-metadata.json (source). "
            "Default: %(default)s"
        ),
    )
    parser.add_argument(
        "--target-dir",
        required=True,
        help=(
            "Directory containing native-image config files "
            "(reflect-config.json, resource-config.json, etc.)."
        ),
    )
    return parser


def main() -> int:
    """Run the merge and report results."""
    parser = build_arg_parser()
    args = parser.parse_args()

    source_file: str = args.source
    target_dir: str = args.target_dir

    # --- Check source ---
    if not os.path.isfile(source_file):
        print(f"[ERROR] Source file not found: {source_file}")
        print("  Run the GraalVM native-image agent first to generate it.")
        return 1

    print(f"[INFO] Source: {source_file}")
    source = load_json(source_file)

    # Validate source structure: must be a JSON object
    if not isinstance(source, dict):
        print("[ERROR] Source file must be a JSON object with top-level keys.")
        return 1

    source_keys_found = set(source.keys())
    supported_keys = set(KEY_TO_FILE.keys())
    unsupported = source_keys_found - supported_keys
    if unsupported:
        print(f"[WARN] Unsupported top-level keys in source (skipped): {unsupported}")
        print(f"       Extend KEY_TO_FILE in this script to add support.")

    total_added = 0

    for key, (filename, (src_key, tgt_key), fmt) in KEY_TO_FILE.items():
        if key not in source:
            print(f"[SKIP] '{key}' — not found in source")
            continue

        target_path = os.path.join(target_dir, filename)
        if not os.path.isfile(target_path):
            print(f"[NEW]  '{filename}' — target file not found, creating default")
            os.makedirs(target_dir, exist_ok=True)
            target_data = create_default_data(fmt)
            save_json(target_path, target_data)
        else:
            target_data = load_json(target_path)

        source_entries = source[key]
        if not isinstance(source_entries, list):
            print(f"[SKIP] '{key}' — expected a list in source")
            continue

        if fmt == "list":
            if not isinstance(target_data, list):
                print(f"[ERROR] '{filename}' expected a JSON array (list).")
                return 1
            added = merge_list_format(source_entries, target_data,
                                       src_key, tgt_key)
            save_json(target_path, target_data)

        elif fmt == "includes":
            # Navigate to the includes list
            includes = target_data.get("resources", {}).get("includes")
            if not isinstance(includes, list):
                print(f"[ERROR] '{filename}' missing resources.includes list.")
                return 1
            added = merge_includes_format(source_entries, includes,
                                           src_key, tgt_key)
            save_json(target_path, target_data)

        else:
            print(f"[WARN] Unknown format '{fmt}' for '{key}' — skipped")
            continue

        print(f"[OK]   {filename:<30s}  {len(source_entries):>4d} source"
              f"  →  +{added} new")
        total_added += added

    print(f"\n[DONE] {total_added} entries added across all files.")

    if total_added > 0:
        print("  Next: run 'mvn spotless:apply -pl server -am' to format the JSON.")

    return 0


if __name__ == "__main__":
    sys.exit(main())