Add classfile-api module: java.lang.classfile (JEP 484) parse, generate, transform with timing instrumentation, ASM 9.11.1 comparison, version limits, JDK 24/25/26 API diff

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b4VQVEJWetTT1dYzyJ
This commit is contained in:
2026-10-29 04:14:57 +00:00
co-authored by Claude Sonnet 5.5
parent eca73ad544
commit 41101bea40
37 changed files with 1763 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""Owner-aware diff of two `javap -public` dumps (one per JDK). Usage: api_diff.py OLD.txt NEW.txt"""
import sys
def load(p):
cur, d = None, {}
for line in open(p):
line = line.rstrip("\n")
if not line.strip():
continue
if not line.startswith(" "):
cur = line.rstrip(" {").split(" extends ")[0].split(" implements ")[0].rsplit(" ", 1)[-1]
d[cur] = {"__header__": line}
elif line.strip() != "}":
d[cur][line.strip()] = True
return d
a, b = load(sys.argv[1]), load(sys.argv[2])
changes = 0
for c in sorted(set(a) | set(b)):
if c not in a: print(f"+ class {c}"); changes += 1; continue
if c not in b: print(f"- class {c}"); changes += 1; continue
if a[c]["__header__"] != b[c]["__header__"]:
print(f"~ {c}\n was: {a[c]['__header__']}\n now: {b[c]['__header__']}"); changes += 1
for m in sorted(set(a[c]) - set(b[c]) - {"__header__"}): print(f"- {c}: {m}"); changes += 1
for m in sorted(set(b[c]) - set(a[c]) - {"__header__"}): print(f"+ {c}: {m}"); changes += 1
print(f"{len(a)} classes in old, {len(b)} in new, {changes} difference(s)")
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# classfile-api: regenerates every file in output/.
# JDK25=/path/to/jdk-25 [JDK24=/path/to/jdk-24 JDK26=/path/to/jdk-26] ./scripts/run-all.sh
# Needs Maven (or an existing target/lib with the ASM jars) for the ASM comparison. JDK24/JDK26 are optional: output/07 is skipped without them.
set -uo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"
cd "$HERE"
JDK25="${JDK25:-/opt/jdks/jdk-25.0.5+8}"
J="$JDK25/bin"
OUT="$HERE/output"; mkdir -p "$OUT"
CP="target/c:target/lib/*"
JV="$("$J/java" -version 2>&1 | grep -v '^Picked' | sed -n 2p | sed 's/.*(build \(.*\))/\1/')"
# 1. ASM jars (the only dependency) and compile. Unsetting JAVA_TOOL_OPTIONS keeps proxy banners out of the transcripts.
if [ ! -d target/lib ]; then
JAVA_HOME="$JDK25" mvn -B -q -T1 dependency:copy-dependencies -DoutputDirectory=target/lib || exit 1
fi
unset JAVA_TOOL_OPTIONS
rm -rf target/c target/gen target/instrumented; mkdir -p target/c
echo "javac: $("$J/javac" -version 2>&1)"
"$J/javac" --release 25 -Xlint:all -cp "target/lib/*" -d target/c src/main/java/demo/*.java || exit 1
run() { # run <outfile> <main class> [args] -- prints the command, then the output, then the exit code
local f="$1"; shift
{ echo "\$ java -cp classes:asm-9.11.1.jar... $* ($JV)"; "$J/java" -Xmx512m -cp "$CP" "$@" 2>&1; echo "exit=$?"; } > "$OUT/$f"
}
run 01-parse.txt demo.ParseDemo
run 02-generate.txt demo.GenerateDemo target/gen/demo
{ echo "\$ javap -c -p -v demo.Generated (the class GenerateDemo wrote; $JV)"
"$J/javap" -c -p -v -cp target/gen demo.Generated 2>&1 | grep -v -e '^Classfile ' -e '^ Last modified' -e '^ SHA-256'; } > "$OUT/03-javap-generated.txt"
run 04-broken-generate.txt demo.BrokenGenerate
run 05-transform.txt demo.TransformDemo target/instrumented
meth() { awk -v pat="$1" '$0 ~ pat {f=1} f{print} f && /^$/ {f=0}'; } # one method block of javap -c output
{ echo "\$ javap -c -p demo.Target (original, as javac produced it; fib and boom only)"
"$J/javap" -c -p -cp target/c demo.Target | meth 'static int fib'
"$J/javap" -c -p -cp target/c demo.Target | meth 'static void boom'
echo "\$ javap -c -p demo.Target (naive transform: records on normal return only)"
"$J/javap" -c -p -cp target/instrumented/naive demo.Target | meth 'static int fib'
"$J/javap" -c -p -cp target/instrumented/naive demo.Target | meth 'static void boom'
echo "\$ javap -c -p demo.Target (full transform: plus a catch-all handler)"
"$J/javap" -c -p -cp target/instrumented/full demo.Target | meth 'static int fib'
"$J/javap" -c -p -cp target/instrumented/full demo.Target | meth 'static void boom'
} > "$OUT/06-javap-instrumented.txt"
# 2. ASM comparison, same tasks
run 08-asm-parse.txt demo.AsmParse
run 09-asm-generate.txt demo.AsmGenerate
run 10-asm-instrument.txt demo.AsmInstrument
run 15-asm-expand-frames.txt demo.AsmGotcha
run 11-version-limits.txt demo.VersionLimits
# size of the equivalent code, counting only the method(s) that do the job (non-blank, non-comment lines)
body() { awk -v pat="$2" '$0 ~ pat {f=1} f && !/^[[:space:]]*(\/\/|\*|\/\*|$)/ {n++} f && /^ }$/ {f=0} END {print n+0}' "$1"; }
{ echo "Lines of code in the method(s) that do the job (non-blank, non-comment), counted by scripts/run-all.sh:"
printf "%-26s java.lang.classfile %3d ASM %3d\n" "generate demo.Generated" "$(body src/main/java/demo/GenerateDemo.java 'static byte\[\] generate')" "$(body src/main/java/demo/AsmGenerate.java 'static byte\[\] generate')"
printf "%-26s java.lang.classfile %3d ASM %3d\n" "timing instrumentation" "$(( $(body src/main/java/demo/TimingTransform.java 'public static byte\[\] instrument') + $(body src/main/java/demo/TimingTransform.java 'private static final class Timer') ))" "$(body src/main/java/demo/AsmInstrument.java 'public static byte\[\] instrument')"
} > "$OUT/12-line-counts.txt"
# Bench: three separate JVMs, because a single run on a shared machine is not trustworthy
if [ -z "${SKIP_BENCH:-}" ]; then
{ for i in 1 2 3; do echo "--- run $i of 3 ---"; "$J/java" -Xmx512m -cp "$CP" demo.Bench 2>&1 | grep -v '^PASS'; done; } > "$OUT/13-bench.txt"
fi
# 3. Public API of java.lang.classfile across JDK 24, 25, 26 (needs the other JDKs)
if [ -n "${JDK24:-}" ] && [ -n "${JDK26:-}" ]; then
api() { # api <jdk-home> <outfile>
local classes
classes=$("$1/bin/jimage" list "$1/lib/modules" | awk '/^Module: java.base/{f=1;next} /^Module:/{f=0} f' \
| grep -E 'java/lang/classfile/.*\.class$' | grep -v -e package-info -e Snippets | sed 's/\.class$//;s#/#.#g;s/^ *//')
# shellcheck disable=SC2086
"$1/bin/javap" -public -cp none $classes 2>/dev/null | grep -v '^Compiled from' > "$2"
}
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
api "$JDK24" "$T/24.txt"; api "$JDK25" "$T/25.txt"; api "$JDK26" "$T/26.txt"
{ echo "\$ javap -public java.lang.classfile.** on each JDK, then diff by class (24: $("$JDK24/bin/java" -version 2>&1 | grep -v '^Picked' | sed -n 2p), 26: $("$JDK26/bin/java" -version 2>&1 | grep -v '^Picked' | sed -n 2p))"
echo; echo "=== JDK 24 -> JDK 25 ==="; python3 -I scripts/api_diff.py "$T/24.txt" "$T/25.txt"
echo; echo "=== JDK 25 -> JDK 26 ==="; python3 -I scripts/api_diff.py "$T/25.txt" "$T/26.txt"
echo; echo "=== is it a preview API on JDK 24? ==="; echo "\$ javap -v java.lang.classfile.ClassFile | grep -ci PreviewFeature"
for j in "$JDK24" "$JDK25" "$JDK26"; do echo "$("$j/bin/java" -version 2>&1 | grep -v '^Picked' | sed -n 2p | sed 's/.*(build \(.*\))/\1/'): $("$j/bin/javap" -v java.lang.classfile.ClassFile | grep -ci PreviewFeature)"; done
echo; echo "=== @since of ClassFile ==="; for j in "$JDK24" "$JDK25" "$JDK26"; do unzip -p "$j/lib/src.zip" java.base/java/lang/classfile/ClassFile.java | grep -m1 '@since' | sed "s|^|$(basename "$j"): |"; done
} > "$OUT/07-api-diff-24-25-26.txt"
else
echo "JDK24/JDK26 not set: skipped output/07-api-diff-24-25-26.txt"
fi
echo done
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Who is migrating? Look inside spring-core's published jar instead of trusting a blog.
# JDK25=/path/to/jdk-25 ./scripts/spring-core-probe.sh [spring-core-version] -> output/14-spring-core-probe.txt
set -uo pipefail
unset JAVA_TOOL_OPTIONS
HERE="$(cd "$(dirname "$0")/.." && pwd)"; JDK25="${JDK25:-/opt/jdks/jdk-25.0.5+8}"; V="${1:-7.0.10}"
W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT
URL="https://repo1.maven.org/maven2/org/springframework/spring-core/$V/spring-core-$V.jar"
for i in 1 2 3 4 5 6; do c=$(curl -s -o "$W/sc.jar" -w '%{http_code}' "$URL"); [ "$c" = 200 ] && break; sleep 20; done
[ "$c" = 200 ] || { echo "download failed ($c)"; exit 1; }
{
echo "\$ unzip -l spring-core-$V.jar | grep 'META-INF/versions/24/.*\\.class' | wc -l"
unzip -l "$W/sc.jar" | grep -E 'META-INF/versions/24/.*\.class' | wc -l
echo; echo "\$ unzip -l spring-core-$V.jar | grep 'versions/24.*ClassFile'"
unzip -l "$W/sc.jar" | grep -E 'versions/24/.*ClassFile' | awk '{print $4}'
echo; echo "\$ unzip -l spring-core-$V.jar | grep -c 'org/springframework/asm/.*\\.class' (the repackaged ASM is still there)"
unzip -l "$W/sc.jar" | grep -cE 'org/springframework/asm/.*\.class'
echo; echo "\$ javap -v -cp <jar>!/META-INF/versions/24 ...ClassFileMetadataReader | grep java/lang/classfile (does it really call the JDK API?)"
mkdir "$W/x"; unzip -q "$W/sc.jar" 'META-INF/versions/24/org/springframework/core/type/classreading/ClassFileMetadataReader*' -d "$W/x"
"$JDK25/bin/javap" -v -cp "$W/x/META-INF/versions/24" org.springframework.core.type.classreading.ClassFileMetadataReader | grep -o 'java/lang/classfile/[A-Za-z$/]*' | sort -u
echo; echo "\$ unzip -p spring-core-$V.jar META-INF/MANIFEST.MF | grep -i multi-release"
unzip -p "$W/sc.jar" META-INF/MANIFEST.MF | grep -i multi-release
} > "$HERE/output/14-spring-core-probe.txt" 2>&1
cat "$HERE/output/14-spring-core-probe.txt"