這類錯幾乎跟你寫的業務程式碼無關: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 重編),把舊的、可能還引用老字節碼的緩存清掉,避免「改完還報一樣的錯」。
