这类错几乎跟你写的业务代码无关:JVM 在真正执行一个类之前,会先校验字节码是否合法、类型是否符合安全规则。一旦某个
.class是坏的、版本不匹配、或被 ASM/Lombok 等工具改坏,就会在校验阶段崩掉。报错里的类名是唯一的线索——它分不清问题在你的代码还是库里,但能告诉你先去查谁。
Issue & Env(案发现场与环境)
- 现象:启动时或首次加载某类时抛
VerifyError、ClassFormatError,或编译/运行时报Unsupported class file major version。 - 环境:常出现在升级 JDK、引入字节码增强(ASM / Javassist / ByteBuddy / Lombok / AOP)或依赖冲突之后。
StackTrace(堆栈日志)
典型的校验失败:
java.lang.VerifyError: Stack map does not match the one at exception handler
at org.springframework.cglib.proxy.Enhancer.setThreadCallbacks(Enhancer.java:158)
版本不匹配的线索在报错文案里:
java.lang.IllegalArgumentException: Unsupported class file major version 69
at org.lombokweb.asm.ClassReader.<init>(ClassReader.java:200)
at org.lombokweb.asm.ClassReader.<init>(ClassReader.java:187)
at lombok.javac.handlers.HandleSneakyThrows.compile(HandleSneakyThrows.java:240)
at lombok.javac.HandlerLibrary.handleAnnotation(HandlerLibrary.java:250)
at lombok.javac.JavacAnnotationHandler.handle(JavacAnnotationHandler.java:157)
at com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1270)
Reproduce(复现代码)
把类声明成 final,又让 Spring 用 CGLIB 去给它做代理:
@Service
public final class LogService { // final 无法被 CGLIB 继承
public void log(String msg) {
System.out.println(msg);
}
}
或用一份老版本 Lombok 去编译高版本 JDK 的代码,@SneakyThrows 处理阶段在读取 class 时直接抛版本不支持。
Root Cause(根因分析)
这两兄弟的分工:ClassFormatError 是 .class 文件结构层面就坏,连解析都过不去;VerifyError 是结构能解析,但里面的字节码违反了 Java 的安全规则(栈类型对不上、跳转目标非法等)。它们都是 JVM 在「加载 → 校验」这步拦下来的,属于 JVM 级 ERROR,不是普通 Exception。
来源集中在四类:
- 字节码增强工具生成了非法字节码——ASM、Javassist、ByteBuddy 用错 API,或生成的栈映射(StackMapTable)和服务端字节码不一致。
- 编译产物版本高于运行 JVM——JDK 21 编译的 class 在 JDK 8 上跑,class file major version 对不上。
- Lombok 等代码生成器与 JDK 版本不兼容——JDK 21 需要 Lombok 1.18.30 以上,老版本读不了新 class 结构。
- ASM / CGLIB 等库在依赖树里出现多个版本,Spring 用的 A 版本,字节码却按 B 版本生成,上下对不齐。
final 类被 CGLIB 代理失败这类,本质也是字节码生成层拿不到可继承的父类,最终以校验或生成异常的形式暴露出来。
The Fix(解决方案)
有一条主线:先看报错里的类名和具体信息,判断问题是「版本错位」还是「字节码坏」。
拿不准是哪个 JAR 冲突,先看依赖树:
mvn dependency:tree -Dverbose
版本不匹配就对齐:升级 Lombok(JDK 21 至少 1.18.30)或对应字节码库到与新 JDK 兼容的版本;把 class file 编译目标降到运行 JVM 支持的 --release;ASM/CGLIB 混版本就统一到 Spring 所用的那个。
确定是字节码坏,ASM 官方提供了一个校验器,能精确指到是哪条指令出了错:
java -cp "asm.jar:asm-tree.jar:asm-analysis.jar:asm-util.jar" \
org.objectweb.asm.util.CheckClassAdapter \
org/domain/package/YourClass.class
输出会像 Error at instruction 2: Argument 1: expected Ljava/lang/String;, but found I 这样,直接定位到出问题的指令。最后,无论哪种原因,改完都做一次干净构建 mvn clean package(或删 target / build 重编),把旧的、可能还引用老字节码的缓存清掉,避免「改完还报一样的错」。
