bigywater.tech
Java · 228

Class-File API:用结构化方式生成和转换字节码

JDK 27 里不需要预览开关就能使用的 Class-File API 让 Java 程序可以自己读写 class 文件,不必再依赖 ASM 之类的库。这篇在 JDK 27 上真实解析、生成、转换,并看栈映射与验证。

467 → 669
给 3 个方法加入口日志前后的 class 字节数
2 个条目
带分支的 sign 方法自动生成的 StackMapTable 条目数
ClassNotFoundException
JDK 21 上加载 java.lang.classfile.ClassFile 的结果

解析 class 文件

实测 · 本机执行输出
  类 Main$Calc,主版本号 71,访问标志 [PUBLIC, SUPER],方法 4 个,字段 0 个,父类 java/lang/Object
  方法 <init> ()V            指令  3 条:[aload_0, invokespecial, return]
  方法 sum    (II)I          指令  4 条:[iload_1, iload_2, iadd, ireturn]
  方法 abs    (I)I           指令  7 条:[iload_1, ifge, iload_1, ineg, ireturn, iload_1, ireturn]
  方法 hi     ()Ljava/lang/String; 指令  2 条:[ldc, areturn]
  abs 的代码元素种类(含标签、行号):[LineNumber, UnboundLoadInstruction, BoundBranchInstruction, UnboundOperatorInstruction, UnboundReturnInstruction, Label]
  方法 abs 的 maxStack = 1,maxLocals = 2

从零生成一个类

实测 · 本机执行输出
  生成的 class:253 字节,魔数 cafebabe,主版本号 71(当前运行时 JDK 27 的版本号 = 71)
  加载并调用:twice(21) = 42,sign(-3) = 负,sign(3) = 非负
  sign 方法里的 StackMapTable 属性:有,条目 2 个(带分支,API 自动生成)
  twice(无分支)的 StackMapTable:无(没有分支不需要)

转换:入口日志与删除方法

实测 · 本机执行输出
  原 class 467 字节 → 转换后 669 字节
进入 sum
  调用 sum(2, 3) = 5
进入 abs
  调用 abs(-7) = 7
进入 hi
  调用静态 hi() = hi
  用 ClassTransform.dropping 去掉 abs 之后,方法列表:[hi, sum]
  ACCEPT_ALL 原样转换:467 字节,与原文件字节相同 = true

栈映射与验证

实测 · 本机执行输出
  JDK 27:java.lang.classfile.ClassFile 可用 = true;包是否需要 --enable-preview:不需要(本次没有加)
  StackMapsOption.STACK_MAPS_WHEN_REQUIRED:类大小 138 字节,调用 f(5) = 1
  StackMapsOption.DROP_STACK_MAPS:VerifyError: Expecting a stackmap frame at branch target 6
  故意制造类型错误:返回类型是 String 的方法里写 iconst_1 再 areturn
  API 生成类型错误的方法:成功,API 没有报错;类大小 123 字节
  defineClass:成功(此时还没有链接、没有验证)
  第一次使用(链接时验证)→ VerifyError: Bad type on operand stack

JDK 21 对比:
  JDK 21:java.lang.classfile.ClassFile 不存在(ClassNotFoundException)
  jdk.internal.classfile 存在(内部包)

分步演示

Class-File API
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 27(java.lang.classfile 在 27 上无需预览开关;本篇只在 27 上运行);JDK 21 只做存在性检查
    • 没有验证:与 ASM 的性能或易用性对比、其它 JDK 版本(24–26)上 API 的差别、常量池共享与 ClassHierarchyResolver 的细节、jdk.internal.classfile 在 JDK 21 内部的 API 形态、复杂的转换(try/catch、invokedynamic)
    京ICP备2026067502号-1京公网安备11010502063487号